mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add support for attribute caching to the media_player platform (#106257)
This commit is contained in:
parent
b757984031
commit
f097e2a2f6
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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()}
|
||||
|
@ -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"},
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user