1
mirror of https://github.com/home-assistant/core synced 2024-08-28 03:36:46 +02:00

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

View File

@ -95,6 +95,8 @@ ENTITY_IDS_BY_NUMBER = {
"24": "media_player.kitchen",
"25": "light.office_rgbw_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()}

View File

@ -237,6 +237,26 @@ DEMO_DEVICES = [
"type": "action.devices.types.SETTOP",
"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",
"name": {"name": "Living Room Fan"},

View File

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