Add support for attribute caching to the media_player platform (#106257)

This commit is contained in:
J. Nick Koston 2023-12-23 13:33:11 -10:00 committed by GitHub
parent b757984031
commit f097e2a2f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 126 additions and 55 deletions

View File

@ -38,6 +38,8 @@ async def async_setup_entry(
DemoMusicPlayer(), DemoMusicPlayer(),
DemoMusicPlayer("Kitchen"), DemoMusicPlayer("Kitchen"),
DemoTVShowPlayer(), DemoTVShowPlayer(),
DemoBrowsePlayer("Browse"),
DemoGroupPlayer("Group"),
] ]
) )
@ -90,6 +92,8 @@ NETFLIX_PLAYER_SUPPORT = (
| MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.STOP
) )
BROWSE_PLAYER_SUPPORT = MediaPlayerEntityFeature.BROWSE_MEDIA
class AbstractDemoPlayer(MediaPlayerEntity): class AbstractDemoPlayer(MediaPlayerEntity):
"""A demo media players.""" """A demo media players."""
@ -379,3 +383,19 @@ class DemoTVShowPlayer(AbstractDemoPlayer):
"""Set the input source.""" """Set the input source."""
self._attr_source = source self._attr_source = source
self.schedule_update_ha_state() self.schedule_update_ha_state()
class DemoBrowsePlayer(AbstractDemoPlayer):
"""A Demo media player that supports browse."""
_attr_supported_features = BROWSE_PLAYER_SUPPORT
class DemoGroupPlayer(AbstractDemoPlayer):
"""A Demo media player that supports grouping."""
_attr_supported_features = (
YOUTUBE_PLAYER_SUPPORT
| MediaPlayerEntityFeature.GROUPING
| MediaPlayerEntityFeature.TURN_OFF
)

View File

@ -12,7 +12,7 @@ import hashlib
from http import HTTPStatus from http import HTTPStatus
import logging import logging
import secrets import secrets
from typing import Any, Final, Required, TypedDict, final from typing import TYPE_CHECKING, Any, Final, Required, TypedDict, final
from urllib.parse import quote, urlparse from urllib.parse import quote, urlparse
from aiohttp import web from aiohttp import web
@ -131,6 +131,11 @@ from .const import ( # noqa: F401
) )
from .errors import BrowseError from .errors import BrowseError
if TYPE_CHECKING:
from functools import cached_property
else:
from homeassistant.backports.functools import cached_property
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ENTITY_ID_FORMAT = DOMAIN + ".{}" ENTITY_ID_FORMAT = DOMAIN + ".{}"
@ -455,7 +460,43 @@ class MediaPlayerEntityDescription(EntityDescription, frozen_or_thawed=True):
volume_step: float | None = None volume_step: float | None = None
class MediaPlayerEntity(Entity): CACHED_PROPERTIES_WITH_ATTR_ = {
"device_class",
"state",
"volume_level",
"volume_step",
"is_volume_muted",
"media_content_id",
"media_content_type",
"media_duration",
"media_position",
"media_position_updated_at",
"media_image_url",
"media_image_remotely_accessible",
"media_title",
"media_artist",
"media_album_name",
"media_album_artist",
"media_track",
"media_series_title",
"media_season",
"media_episode",
"media_channel",
"media_playlist",
"app_id",
"app_name",
"source",
"source_list",
"sound_mode",
"sound_mode_list",
"shuffle",
"repeat",
"group_members",
"supported_features",
}
class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""ABC for media player entities.""" """ABC for media player entities."""
_entity_component_unrecorded_attributes = frozenset( _entity_component_unrecorded_attributes = frozenset(
@ -507,7 +548,7 @@ class MediaPlayerEntity(Entity):
_attr_volume_step: float _attr_volume_step: float
# Implement these for your media player # Implement these for your media player
@property @cached_property
def device_class(self) -> MediaPlayerDeviceClass | None: def device_class(self) -> MediaPlayerDeviceClass | None:
"""Return the class of this entity.""" """Return the class of this entity."""
if hasattr(self, "_attr_device_class"): if hasattr(self, "_attr_device_class"):
@ -516,7 +557,7 @@ class MediaPlayerEntity(Entity):
return self.entity_description.device_class return self.entity_description.device_class
return None return None
@property @cached_property
def state(self) -> MediaPlayerState | None: def state(self) -> MediaPlayerState | None:
"""State of the player.""" """State of the player."""
return self._attr_state return self._attr_state
@ -528,12 +569,12 @@ class MediaPlayerEntity(Entity):
self._access_token = secrets.token_hex(32) self._access_token = secrets.token_hex(32)
return self._access_token return self._access_token
@property @cached_property
def volume_level(self) -> float | None: def volume_level(self) -> float | None:
"""Volume level of the media player (0..1).""" """Volume level of the media player (0..1)."""
return self._attr_volume_level return self._attr_volume_level
@property @cached_property
def volume_step(self) -> float: def volume_step(self) -> float:
"""Return the step to be used by the volume_up and volume_down services.""" """Return the step to be used by the volume_up and volume_down services."""
if hasattr(self, "_attr_volume_step"): if hasattr(self, "_attr_volume_step"):
@ -545,32 +586,32 @@ class MediaPlayerEntity(Entity):
return volume_step return volume_step
return 0.1 return 0.1
@property @cached_property
def is_volume_muted(self) -> bool | None: def is_volume_muted(self) -> bool | None:
"""Boolean if volume is currently muted.""" """Boolean if volume is currently muted."""
return self._attr_is_volume_muted return self._attr_is_volume_muted
@property @cached_property
def media_content_id(self) -> str | None: def media_content_id(self) -> str | None:
"""Content ID of current playing media.""" """Content ID of current playing media."""
return self._attr_media_content_id return self._attr_media_content_id
@property @cached_property
def media_content_type(self) -> MediaType | str | None: def media_content_type(self) -> MediaType | str | None:
"""Content type of current playing media.""" """Content type of current playing media."""
return self._attr_media_content_type return self._attr_media_content_type
@property @cached_property
def media_duration(self) -> int | None: def media_duration(self) -> int | None:
"""Duration of current playing media in seconds.""" """Duration of current playing media in seconds."""
return self._attr_media_duration return self._attr_media_duration
@property @cached_property
def media_position(self) -> int | None: def media_position(self) -> int | None:
"""Position of current playing media in seconds.""" """Position of current playing media in seconds."""
return self._attr_media_position return self._attr_media_position
@property @cached_property
def media_position_updated_at(self) -> dt.datetime | None: def media_position_updated_at(self) -> dt.datetime | None:
"""When was the position of the current playing media valid. """When was the position of the current playing media valid.
@ -578,12 +619,12 @@ class MediaPlayerEntity(Entity):
""" """
return self._attr_media_position_updated_at return self._attr_media_position_updated_at
@property @cached_property
def media_image_url(self) -> str | None: def media_image_url(self) -> str | None:
"""Image url of current playing media.""" """Image url of current playing media."""
return self._attr_media_image_url return self._attr_media_image_url
@property @cached_property
def media_image_remotely_accessible(self) -> bool: def media_image_remotely_accessible(self) -> bool:
"""If the image url is remotely accessible.""" """If the image url is remotely accessible."""
return self._attr_media_image_remotely_accessible return self._attr_media_image_remotely_accessible
@ -618,102 +659,102 @@ class MediaPlayerEntity(Entity):
""" """
return None, None return None, None
@property @cached_property
def media_title(self) -> str | None: def media_title(self) -> str | None:
"""Title of current playing media.""" """Title of current playing media."""
return self._attr_media_title return self._attr_media_title
@property @cached_property
def media_artist(self) -> str | None: def media_artist(self) -> str | None:
"""Artist of current playing media, music track only.""" """Artist of current playing media, music track only."""
return self._attr_media_artist return self._attr_media_artist
@property @cached_property
def media_album_name(self) -> str | None: def media_album_name(self) -> str | None:
"""Album name of current playing media, music track only.""" """Album name of current playing media, music track only."""
return self._attr_media_album_name return self._attr_media_album_name
@property @cached_property
def media_album_artist(self) -> str | None: def media_album_artist(self) -> str | None:
"""Album artist of current playing media, music track only.""" """Album artist of current playing media, music track only."""
return self._attr_media_album_artist return self._attr_media_album_artist
@property @cached_property
def media_track(self) -> int | None: def media_track(self) -> int | None:
"""Track number of current playing media, music track only.""" """Track number of current playing media, music track only."""
return self._attr_media_track return self._attr_media_track
@property @cached_property
def media_series_title(self) -> str | None: def media_series_title(self) -> str | None:
"""Title of series of current playing media, TV show only.""" """Title of series of current playing media, TV show only."""
return self._attr_media_series_title return self._attr_media_series_title
@property @cached_property
def media_season(self) -> str | None: def media_season(self) -> str | None:
"""Season of current playing media, TV show only.""" """Season of current playing media, TV show only."""
return self._attr_media_season return self._attr_media_season
@property @cached_property
def media_episode(self) -> str | None: def media_episode(self) -> str | None:
"""Episode of current playing media, TV show only.""" """Episode of current playing media, TV show only."""
return self._attr_media_episode return self._attr_media_episode
@property @cached_property
def media_channel(self) -> str | None: def media_channel(self) -> str | None:
"""Channel currently playing.""" """Channel currently playing."""
return self._attr_media_channel return self._attr_media_channel
@property @cached_property
def media_playlist(self) -> str | None: def media_playlist(self) -> str | None:
"""Title of Playlist currently playing.""" """Title of Playlist currently playing."""
return self._attr_media_playlist return self._attr_media_playlist
@property @cached_property
def app_id(self) -> str | None: def app_id(self) -> str | None:
"""ID of the current running app.""" """ID of the current running app."""
return self._attr_app_id return self._attr_app_id
@property @cached_property
def app_name(self) -> str | None: def app_name(self) -> str | None:
"""Name of the current running app.""" """Name of the current running app."""
return self._attr_app_name return self._attr_app_name
@property @cached_property
def source(self) -> str | None: def source(self) -> str | None:
"""Name of the current input source.""" """Name of the current input source."""
return self._attr_source return self._attr_source
@property @cached_property
def source_list(self) -> list[str] | None: def source_list(self) -> list[str] | None:
"""List of available input sources.""" """List of available input sources."""
return self._attr_source_list return self._attr_source_list
@property @cached_property
def sound_mode(self) -> str | None: def sound_mode(self) -> str | None:
"""Name of the current sound mode.""" """Name of the current sound mode."""
return self._attr_sound_mode return self._attr_sound_mode
@property @cached_property
def sound_mode_list(self) -> list[str] | None: def sound_mode_list(self) -> list[str] | None:
"""List of available sound modes.""" """List of available sound modes."""
return self._attr_sound_mode_list return self._attr_sound_mode_list
@property @cached_property
def shuffle(self) -> bool | None: def shuffle(self) -> bool | None:
"""Boolean if shuffle is enabled.""" """Boolean if shuffle is enabled."""
return self._attr_shuffle return self._attr_shuffle
@property @cached_property
def repeat(self) -> RepeatMode | str | None: def repeat(self) -> RepeatMode | str | None:
"""Return current repeat mode.""" """Return current repeat mode."""
return self._attr_repeat return self._attr_repeat
@property @cached_property
def group_members(self) -> list[str] | None: def group_members(self) -> list[str] | None:
"""List of members which are currently grouped together.""" """List of members which are currently grouped together."""
return self._attr_group_members return self._attr_group_members
@property @cached_property
def supported_features(self) -> MediaPlayerEntityFeature: def supported_features(self) -> MediaPlayerEntityFeature:
"""Flag media player features that are supported.""" """Flag media player features that are supported."""
return self._attr_supported_features return self._attr_supported_features

View File

@ -95,6 +95,8 @@ ENTITY_IDS_BY_NUMBER = {
"24": "media_player.kitchen", "24": "media_player.kitchen",
"25": "light.office_rgbw_lights", "25": "light.office_rgbw_lights",
"26": "light.living_room_rgbww_lights", "26": "light.living_room_rgbww_lights",
"27": "media_player.group",
"28": "media_player.browse",
} }
ENTITY_NUMBERS_BY_ID = {v: k for k, v in ENTITY_IDS_BY_NUMBER.items()} ENTITY_NUMBERS_BY_ID = {v: k for k, v in ENTITY_IDS_BY_NUMBER.items()}

View File

@ -237,6 +237,26 @@ DEMO_DEVICES = [
"type": "action.devices.types.SETTOP", "type": "action.devices.types.SETTOP",
"willReportState": False, "willReportState": False,
}, },
{
"id": "media_player.browse",
"name": {"name": "Browse"},
"traits": ["action.devices.traits.MediaState", "action.devices.traits.OnOff"],
"type": "action.devices.types.SETTOP",
"willReportState": False,
},
{
"id": "media_player.group",
"name": {"name": "Group"},
"traits": [
"action.devices.traits.OnOff",
"action.devices.traits.Volume",
"action.devices.traits.Modes",
"action.devices.traits.TransportControl",
"action.devices.traits.MediaState",
],
"type": "action.devices.types.SETTOP",
"willReportState": False,
},
{ {
"id": "fan.living_room_fan", "id": "fan.living_room_fan",
"name": {"name": "Living Room Fan"}, "name": {"name": "Living Room Fan"},

View File

@ -10,7 +10,6 @@ from homeassistant.components.media_player import (
BrowseMedia, BrowseMedia,
MediaClass, MediaClass,
MediaPlayerEnqueue, MediaPlayerEnqueue,
MediaPlayerEntityFeature,
) )
from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF
@ -159,9 +158,6 @@ async def test_media_browse(
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
with patch( with patch(
"homeassistant.components.demo.media_player.MediaPlayerEntity.supported_features",
MediaPlayerEntityFeature.BROWSE_MEDIA,
), patch(
"homeassistant.components.media_player.MediaPlayerEntity.async_browse_media", "homeassistant.components.media_player.MediaPlayerEntity.async_browse_media",
return_value=BrowseMedia( return_value=BrowseMedia(
media_class=MediaClass.DIRECTORY, media_class=MediaClass.DIRECTORY,
@ -176,7 +172,7 @@ async def test_media_browse(
{ {
"id": 5, "id": 5,
"type": "media_player/browse_media", "type": "media_player/browse_media",
"entity_id": "media_player.bedroom", "entity_id": "media_player.browse",
"media_content_type": "album", "media_content_type": "album",
"media_content_id": "abcd", "media_content_id": "abcd",
} }
@ -202,9 +198,6 @@ async def test_media_browse(
assert mock_browse_media.mock_calls[0][1] == ("album", "abcd") assert mock_browse_media.mock_calls[0][1] == ("album", "abcd")
with patch( with patch(
"homeassistant.components.demo.media_player.MediaPlayerEntity.supported_features",
MediaPlayerEntityFeature.BROWSE_MEDIA,
), patch(
"homeassistant.components.media_player.MediaPlayerEntity.async_browse_media", "homeassistant.components.media_player.MediaPlayerEntity.async_browse_media",
return_value={"bla": "yo"}, return_value={"bla": "yo"},
): ):
@ -212,7 +205,7 @@ async def test_media_browse(
{ {
"id": 6, "id": 6,
"type": "media_player/browse_media", "type": "media_player/browse_media",
"entity_id": "media_player.bedroom", "entity_id": "media_player.browse",
} }
) )
@ -231,19 +224,14 @@ async def test_group_members_available_when_off(hass: HomeAssistant) -> None:
) )
await hass.async_block_till_done() await hass.async_block_till_done()
# Fake group support for DemoYoutubePlayer
with patch(
"homeassistant.components.demo.media_player.MediaPlayerEntity.supported_features",
MediaPlayerEntityFeature.GROUPING | MediaPlayerEntityFeature.TURN_OFF,
):
await hass.services.async_call( await hass.services.async_call(
"media_player", "media_player",
"turn_off", "turn_off",
{ATTR_ENTITY_ID: "media_player.bedroom"}, {ATTR_ENTITY_ID: "media_player.group"},
blocking=True, blocking=True,
) )
state = hass.states.get("media_player.bedroom") state = hass.states.get("media_player.group")
assert state.state == STATE_OFF assert state.state == STATE_OFF
assert "group_members" in state.attributes assert "group_members" in state.attributes