Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Component to interface with various media players."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import collections
7 from collections.abc import Callable
8 from contextlib import suppress
9 import datetime as dt
10 from enum import StrEnum
11 import functools as ft
12 from functools import lru_cache
13 import hashlib
14 from http import HTTPStatus
15 import logging
16 import secrets
17 from typing import Any, Final, Required, TypedDict, final
18 from urllib.parse import quote, urlparse
19 
20 import aiohttp
21 from aiohttp import web
22 from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE
23 from aiohttp.typedefs import LooseHeaders
24 from propcache import cached_property
25 import voluptuous as vol
26 from yarl import URL
27 
28 from homeassistant.components import websocket_api
29 from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
30 from homeassistant.components.websocket_api import ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR
31 from homeassistant.config_entries import ConfigEntry
32 from homeassistant.const import ( # noqa: F401
33  ATTR_ENTITY_PICTURE,
34  SERVICE_MEDIA_NEXT_TRACK,
35  SERVICE_MEDIA_PAUSE,
36  SERVICE_MEDIA_PLAY,
37  SERVICE_MEDIA_PLAY_PAUSE,
38  SERVICE_MEDIA_PREVIOUS_TRACK,
39  SERVICE_MEDIA_SEEK,
40  SERVICE_MEDIA_STOP,
41  SERVICE_REPEAT_SET,
42  SERVICE_SHUFFLE_SET,
43  SERVICE_TOGGLE,
44  SERVICE_TURN_OFF,
45  SERVICE_TURN_ON,
46  SERVICE_VOLUME_DOWN,
47  SERVICE_VOLUME_MUTE,
48  SERVICE_VOLUME_SET,
49  SERVICE_VOLUME_UP,
50  STATE_IDLE,
51  STATE_OFF,
52  STATE_PLAYING,
53  STATE_STANDBY,
54 )
55 from homeassistant.core import HomeAssistant
56 from homeassistant.helpers import config_validation as cv
57 from homeassistant.helpers.aiohttp_client import async_get_clientsession
59  DeprecatedConstantEnum,
60  all_with_deprecated_constants,
61  check_if_deprecated_constant,
62  dir_with_deprecated_constants,
63 )
64 from homeassistant.helpers.entity import Entity, EntityDescription
65 from homeassistant.helpers.entity_component import EntityComponent
66 from homeassistant.helpers.network import get_url
67 from homeassistant.helpers.typing import ConfigType
68 from homeassistant.loader import bind_hass
69 from homeassistant.util.hass_dict import HassKey
70 
71 from .browse_media import BrowseMedia, async_process_play_media_url # noqa: F401
72 from .const import ( # noqa: F401
73  _DEPRECATED_MEDIA_CLASS_DIRECTORY,
74  _DEPRECATED_SUPPORT_BROWSE_MEDIA,
75  _DEPRECATED_SUPPORT_CLEAR_PLAYLIST,
76  _DEPRECATED_SUPPORT_GROUPING,
77  _DEPRECATED_SUPPORT_NEXT_TRACK,
78  _DEPRECATED_SUPPORT_PAUSE,
79  _DEPRECATED_SUPPORT_PLAY,
80  _DEPRECATED_SUPPORT_PLAY_MEDIA,
81  _DEPRECATED_SUPPORT_PREVIOUS_TRACK,
82  _DEPRECATED_SUPPORT_REPEAT_SET,
83  _DEPRECATED_SUPPORT_SEEK,
84  _DEPRECATED_SUPPORT_SELECT_SOUND_MODE,
85  _DEPRECATED_SUPPORT_SELECT_SOURCE,
86  _DEPRECATED_SUPPORT_SHUFFLE_SET,
87  _DEPRECATED_SUPPORT_STOP,
88  _DEPRECATED_SUPPORT_TURN_OFF,
89  _DEPRECATED_SUPPORT_TURN_ON,
90  _DEPRECATED_SUPPORT_VOLUME_MUTE,
91  _DEPRECATED_SUPPORT_VOLUME_SET,
92  _DEPRECATED_SUPPORT_VOLUME_STEP,
93  ATTR_APP_ID,
94  ATTR_APP_NAME,
95  ATTR_ENTITY_PICTURE_LOCAL,
96  ATTR_GROUP_MEMBERS,
97  ATTR_INPUT_SOURCE,
98  ATTR_INPUT_SOURCE_LIST,
99  ATTR_MEDIA_ALBUM_ARTIST,
100  ATTR_MEDIA_ALBUM_NAME,
101  ATTR_MEDIA_ANNOUNCE,
102  ATTR_MEDIA_ARTIST,
103  ATTR_MEDIA_CHANNEL,
104  ATTR_MEDIA_CONTENT_ID,
105  ATTR_MEDIA_CONTENT_TYPE,
106  ATTR_MEDIA_DURATION,
107  ATTR_MEDIA_ENQUEUE,
108  ATTR_MEDIA_EPISODE,
109  ATTR_MEDIA_EXTRA,
110  ATTR_MEDIA_PLAYLIST,
111  ATTR_MEDIA_POSITION,
112  ATTR_MEDIA_POSITION_UPDATED_AT,
113  ATTR_MEDIA_REPEAT,
114  ATTR_MEDIA_SEASON,
115  ATTR_MEDIA_SEEK_POSITION,
116  ATTR_MEDIA_SERIES_TITLE,
117  ATTR_MEDIA_SHUFFLE,
118  ATTR_MEDIA_TITLE,
119  ATTR_MEDIA_TRACK,
120  ATTR_MEDIA_VOLUME_LEVEL,
121  ATTR_MEDIA_VOLUME_MUTED,
122  ATTR_SOUND_MODE,
123  ATTR_SOUND_MODE_LIST,
124  CONTENT_AUTH_EXPIRY_TIME,
125  DOMAIN,
126  REPEAT_MODES,
127  SERVICE_CLEAR_PLAYLIST,
128  SERVICE_JOIN,
129  SERVICE_PLAY_MEDIA,
130  SERVICE_SELECT_SOUND_MODE,
131  SERVICE_SELECT_SOURCE,
132  SERVICE_UNJOIN,
133  MediaClass,
134  MediaPlayerEntityFeature,
135  MediaPlayerState,
136  MediaType,
137  RepeatMode,
138 )
139 from .errors import BrowseError
140 
141 _LOGGER = logging.getLogger(__name__)
142 
143 DATA_COMPONENT: HassKey[EntityComponent[MediaPlayerEntity]] = HassKey(DOMAIN)
144 ENTITY_ID_FORMAT = DOMAIN + ".{}"
145 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
146 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
147 SCAN_INTERVAL = dt.timedelta(seconds=10)
148 
149 CACHE_IMAGES: Final = "images"
150 CACHE_MAXSIZE: Final = "maxsize"
151 CACHE_LOCK: Final = "lock"
152 CACHE_URL: Final = "url"
153 CACHE_CONTENT: Final = "content"
154 
155 
156 class MediaPlayerEnqueue(StrEnum):
157  """Enqueue types for playing media."""
158 
159  # add given media item to end of the queue
160  ADD = "add"
161  # play the given media item next, keep queue
162  NEXT = "next"
163  # play the given media item now, keep queue
164  PLAY = "play"
165  # play the given media item now, clear queue
166  REPLACE = "replace"
167 
168 
169 class MediaPlayerDeviceClass(StrEnum):
170  """Device class for media players."""
171 
172  TV = "tv"
173  SPEAKER = "speaker"
174  RECEIVER = "receiver"
175 
176 
177 DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(MediaPlayerDeviceClass))
178 
179 
180 # DEVICE_CLASS* below are deprecated as of 2021.12
181 # use the MediaPlayerDeviceClass enum instead.
182 _DEPRECATED_DEVICE_CLASS_TV = DeprecatedConstantEnum(
183  MediaPlayerDeviceClass.TV, "2025.10"
184 )
185 _DEPRECATED_DEVICE_CLASS_SPEAKER = DeprecatedConstantEnum(
186  MediaPlayerDeviceClass.SPEAKER, "2025.10"
187 )
188 _DEPRECATED_DEVICE_CLASS_RECEIVER = DeprecatedConstantEnum(
189  MediaPlayerDeviceClass.RECEIVER, "2025.10"
190 )
191 DEVICE_CLASSES = [cls.value for cls in MediaPlayerDeviceClass]
192 
193 
194 MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = {
195  vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
196  vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
197  vol.Exclusive(ATTR_MEDIA_ENQUEUE, "enqueue_announce"): vol.Any(
198  cv.boolean, vol.Coerce(MediaPlayerEnqueue)
199  ),
200  vol.Exclusive(ATTR_MEDIA_ANNOUNCE, "enqueue_announce"): cv.boolean,
201  vol.Optional(ATTR_MEDIA_EXTRA, default={}): dict,
202 }
203 
204 ATTR_TO_PROPERTY = [
205  ATTR_MEDIA_VOLUME_LEVEL,
206  ATTR_MEDIA_VOLUME_MUTED,
207  ATTR_MEDIA_CONTENT_ID,
208  ATTR_MEDIA_CONTENT_TYPE,
209  ATTR_MEDIA_DURATION,
210  ATTR_MEDIA_POSITION,
211  ATTR_MEDIA_POSITION_UPDATED_AT,
212  ATTR_MEDIA_TITLE,
213  ATTR_MEDIA_ARTIST,
214  ATTR_MEDIA_ALBUM_NAME,
215  ATTR_MEDIA_ALBUM_ARTIST,
216  ATTR_MEDIA_TRACK,
217  ATTR_MEDIA_SERIES_TITLE,
218  ATTR_MEDIA_SEASON,
219  ATTR_MEDIA_EPISODE,
220  ATTR_MEDIA_CHANNEL,
221  ATTR_MEDIA_PLAYLIST,
222  ATTR_APP_ID,
223  ATTR_APP_NAME,
224  ATTR_INPUT_SOURCE,
225  ATTR_SOUND_MODE,
226  ATTR_MEDIA_SHUFFLE,
227  ATTR_MEDIA_REPEAT,
228 ]
229 
230 # mypy: disallow-any-generics
231 
232 
233 class _CacheImage(TypedDict, total=False):
234  """Class to hold a cached image."""
235 
236  lock: Required[asyncio.Lock]
237  content: tuple[bytes | None, str | None]
238 
239 
240 class _ImageCache(TypedDict):
241  """Class to hold a cached image."""
242 
243  images: collections.OrderedDict[str, _CacheImage]
244  maxsize: int
245 
246 
247 _ENTITY_IMAGE_CACHE = _ImageCache(images=collections.OrderedDict(), maxsize=16)
248 
249 
250 @bind_hass
251 def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool:
252  """Return true if specified media player entity_id is on.
253 
254  Check all media player if no entity_id specified.
255  """
256  entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN)
257  return any(
258  not hass.states.is_state(entity_id, MediaPlayerState.OFF)
259  for entity_id in entity_ids
260  )
261 
262 
263 def _rename_keys(**keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]:
264  """Create validator that renames keys.
265 
266  Necessary because the service schema names do not match the command parameters.
267 
268  Async friendly.
269  """
270 
271  def rename(value: dict[str, Any]) -> dict[str, Any]:
272  for to_key, from_key in keys.items():
273  if from_key in value:
274  value[to_key] = value.pop(from_key)
275  return value
276 
277  return rename
278 
279 
280 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
281  """Track states and offer events for media_players."""
282  component = hass.data[DATA_COMPONENT] = EntityComponent[MediaPlayerEntity](
283  logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL
284  )
285 
286  websocket_api.async_register_command(hass, websocket_browse_media)
287  hass.http.register_view(MediaPlayerImageView(component))
288 
289  await component.async_setup(config)
290 
291  component.async_register_entity_service(
292  SERVICE_TURN_ON, None, "async_turn_on", [MediaPlayerEntityFeature.TURN_ON]
293  )
294  component.async_register_entity_service(
295  SERVICE_TURN_OFF, None, "async_turn_off", [MediaPlayerEntityFeature.TURN_OFF]
296  )
297  component.async_register_entity_service(
298  SERVICE_TOGGLE,
299  None,
300  "async_toggle",
301  [MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.TURN_ON],
302  )
303  component.async_register_entity_service(
304  SERVICE_VOLUME_UP,
305  None,
306  "async_volume_up",
307  [MediaPlayerEntityFeature.VOLUME_SET, MediaPlayerEntityFeature.VOLUME_STEP],
308  )
309  component.async_register_entity_service(
310  SERVICE_VOLUME_DOWN,
311  None,
312  "async_volume_down",
313  [MediaPlayerEntityFeature.VOLUME_SET, MediaPlayerEntityFeature.VOLUME_STEP],
314  )
315  component.async_register_entity_service(
316  SERVICE_MEDIA_PLAY_PAUSE,
317  None,
318  "async_media_play_pause",
319  [MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE],
320  )
321  component.async_register_entity_service(
322  SERVICE_MEDIA_PLAY, None, "async_media_play", [MediaPlayerEntityFeature.PLAY]
323  )
324  component.async_register_entity_service(
325  SERVICE_MEDIA_PAUSE, None, "async_media_pause", [MediaPlayerEntityFeature.PAUSE]
326  )
327  component.async_register_entity_service(
328  SERVICE_MEDIA_STOP, None, "async_media_stop", [MediaPlayerEntityFeature.STOP]
329  )
330  component.async_register_entity_service(
331  SERVICE_MEDIA_NEXT_TRACK,
332  None,
333  "async_media_next_track",
334  [MediaPlayerEntityFeature.NEXT_TRACK],
335  )
336  component.async_register_entity_service(
337  SERVICE_MEDIA_PREVIOUS_TRACK,
338  None,
339  "async_media_previous_track",
340  [MediaPlayerEntityFeature.PREVIOUS_TRACK],
341  )
342  component.async_register_entity_service(
343  SERVICE_CLEAR_PLAYLIST,
344  None,
345  "async_clear_playlist",
346  [MediaPlayerEntityFeature.CLEAR_PLAYLIST],
347  )
348  component.async_register_entity_service(
349  SERVICE_VOLUME_SET,
350  vol.All(
351  cv.make_entity_service_schema(
352  {vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float}
353  ),
354  _rename_keys(volume=ATTR_MEDIA_VOLUME_LEVEL),
355  ),
356  "async_set_volume_level",
357  [MediaPlayerEntityFeature.VOLUME_SET],
358  )
359  component.async_register_entity_service(
360  SERVICE_VOLUME_MUTE,
361  vol.All(
362  cv.make_entity_service_schema(
363  {vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean}
364  ),
365  _rename_keys(mute=ATTR_MEDIA_VOLUME_MUTED),
366  ),
367  "async_mute_volume",
368  [MediaPlayerEntityFeature.VOLUME_MUTE],
369  )
370  component.async_register_entity_service(
371  SERVICE_MEDIA_SEEK,
372  vol.All(
373  cv.make_entity_service_schema(
374  {vol.Required(ATTR_MEDIA_SEEK_POSITION): cv.positive_float}
375  ),
376  _rename_keys(position=ATTR_MEDIA_SEEK_POSITION),
377  ),
378  "async_media_seek",
379  [MediaPlayerEntityFeature.SEEK],
380  )
381  component.async_register_entity_service(
382  SERVICE_JOIN,
383  {vol.Required(ATTR_GROUP_MEMBERS): vol.All(cv.ensure_list, [cv.entity_id])},
384  "async_join_players",
385  [MediaPlayerEntityFeature.GROUPING],
386  )
387  component.async_register_entity_service(
388  SERVICE_SELECT_SOURCE,
389  {vol.Required(ATTR_INPUT_SOURCE): cv.string},
390  "async_select_source",
391  [MediaPlayerEntityFeature.SELECT_SOURCE],
392  )
393  component.async_register_entity_service(
394  SERVICE_SELECT_SOUND_MODE,
395  {vol.Required(ATTR_SOUND_MODE): cv.string},
396  "async_select_sound_mode",
397  [MediaPlayerEntityFeature.SELECT_SOUND_MODE],
398  )
399 
400  # Remove in Home Assistant 2022.9
401  def _rewrite_enqueue(value: dict[str, Any]) -> dict[str, Any]:
402  """Rewrite the enqueue value."""
403  if ATTR_MEDIA_ENQUEUE not in value:
404  pass
405  elif value[ATTR_MEDIA_ENQUEUE] is True:
406  value[ATTR_MEDIA_ENQUEUE] = MediaPlayerEnqueue.ADD
407  _LOGGER.warning(
408  "Playing media with enqueue set to True is deprecated. Use 'add'"
409  " instead"
410  )
411  elif value[ATTR_MEDIA_ENQUEUE] is False:
412  value[ATTR_MEDIA_ENQUEUE] = MediaPlayerEnqueue.PLAY
413  _LOGGER.warning(
414  "Playing media with enqueue set to False is deprecated. Use 'play'"
415  " instead"
416  )
417 
418  return value
419 
420  component.async_register_entity_service(
421  SERVICE_PLAY_MEDIA,
422  vol.All(
423  cv.make_entity_service_schema(MEDIA_PLAYER_PLAY_MEDIA_SCHEMA),
424  _rewrite_enqueue,
425  _rename_keys(
426  media_type=ATTR_MEDIA_CONTENT_TYPE,
427  media_id=ATTR_MEDIA_CONTENT_ID,
428  enqueue=ATTR_MEDIA_ENQUEUE,
429  ),
430  ),
431  "async_play_media",
432  [MediaPlayerEntityFeature.PLAY_MEDIA],
433  )
434  component.async_register_entity_service(
435  SERVICE_SHUFFLE_SET,
436  {vol.Required(ATTR_MEDIA_SHUFFLE): cv.boolean},
437  "async_set_shuffle",
438  [MediaPlayerEntityFeature.SHUFFLE_SET],
439  )
440  component.async_register_entity_service(
441  SERVICE_UNJOIN, None, "async_unjoin_player", [MediaPlayerEntityFeature.GROUPING]
442  )
443 
444  component.async_register_entity_service(
445  SERVICE_REPEAT_SET,
446  {vol.Required(ATTR_MEDIA_REPEAT): vol.Coerce(RepeatMode)},
447  "async_set_repeat",
448  [MediaPlayerEntityFeature.REPEAT_SET],
449  )
450 
451  return True
452 
453 
454 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
455  """Set up a config entry."""
456  return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
457 
458 
459 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
460  """Unload a config entry."""
461  return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
462 
463 
464 class MediaPlayerEntityDescription(EntityDescription, frozen_or_thawed=True):
465  """A class that describes media player entities."""
466 
467  device_class: MediaPlayerDeviceClass | None = None
468  volume_step: float | None = None
469 
470 
471 CACHED_PROPERTIES_WITH_ATTR_ = {
472  "device_class",
473  "state",
474  "volume_level",
475  "volume_step",
476  "is_volume_muted",
477  "media_content_id",
478  "media_content_type",
479  "media_duration",
480  "media_position",
481  "media_position_updated_at",
482  "media_image_url",
483  "media_image_remotely_accessible",
484  "media_title",
485  "media_artist",
486  "media_album_name",
487  "media_album_artist",
488  "media_track",
489  "media_series_title",
490  "media_season",
491  "media_episode",
492  "media_channel",
493  "media_playlist",
494  "app_id",
495  "app_name",
496  "source",
497  "source_list",
498  "sound_mode",
499  "sound_mode_list",
500  "shuffle",
501  "repeat",
502  "group_members",
503  "supported_features",
504 }
505 
506 
507 @lru_cache
508 def _url_hash(url: str) -> str:
509  """Create hash for media image url."""
510  return hashlib.sha256(url.encode("utf-8")).hexdigest()[:16]
511 
512 
513 class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
514  """ABC for media player entities."""
515 
516  _entity_component_unrecorded_attributes = frozenset(
517  {
518  ATTR_ENTITY_PICTURE_LOCAL,
519  ATTR_ENTITY_PICTURE,
520  ATTR_INPUT_SOURCE_LIST,
521  ATTR_MEDIA_POSITION_UPDATED_AT,
522  ATTR_MEDIA_POSITION,
523  ATTR_SOUND_MODE_LIST,
524  }
525  )
526 
527  entity_description: MediaPlayerEntityDescription
528  _access_token: str | None = None
529 
530  _attr_app_id: str | None = None
531  _attr_app_name: str | None = None
532  _attr_device_class: MediaPlayerDeviceClass | None
533  _attr_group_members: list[str] | None = None
534  _attr_is_volume_muted: bool | None = None
535  _attr_media_album_artist: str | None = None
536  _attr_media_album_name: str | None = None
537  _attr_media_artist: str | None = None
538  _attr_media_channel: str | None = None
539  _attr_media_content_id: str | None = None
540  _attr_media_content_type: MediaType | str | None = None
541  _attr_media_duration: int | None = None
542  _attr_media_episode: str | None = None
543  _attr_media_image_hash: str | None
544  _attr_media_image_remotely_accessible: bool = False
545  _attr_media_image_url: str | None = None
546  _attr_media_playlist: str | None = None
547  _attr_media_position_updated_at: dt.datetime | None = None
548  _attr_media_position: int | None = None
549  _attr_media_season: str | None = None
550  _attr_media_series_title: str | None = None
551  _attr_media_title: str | None = None
552  _attr_media_track: int | None = None
553  _attr_repeat: RepeatMode | str | None = None
554  _attr_shuffle: bool | None = None
555  _attr_sound_mode_list: list[str] | None = None
556  _attr_sound_mode: str | None = None
557  _attr_source_list: list[str] | None = None
558  _attr_source: str | None = None
559  _attr_state: MediaPlayerState | None = None
560  _attr_supported_features: MediaPlayerEntityFeature = MediaPlayerEntityFeature(0)
561  _attr_volume_level: float | None = None
562  _attr_volume_step: float
563 
564  # Implement these for your media player
565  @cached_property
566  def device_class(self) -> MediaPlayerDeviceClass | None:
567  """Return the class of this entity."""
568  if hasattr(self, "_attr_device_class"):
569  return self._attr_device_class
570  if hasattr(self, "entity_description"):
571  return self.entity_description.device_class
572  return None
573 
574  @cached_property
575  def state(self) -> MediaPlayerState | None:
576  """State of the player."""
577  return self._attr_state
578 
579  @property
580  def access_token(self) -> str:
581  """Access token for this media player."""
582  if self._access_token_access_token is None:
583  self._access_token_access_token = secrets.token_hex(32)
584  return self._access_token_access_token
585 
586  @cached_property
587  def volume_level(self) -> float | None:
588  """Volume level of the media player (0..1)."""
589  return self._attr_volume_level
590 
591  @cached_property
592  def volume_step(self) -> float:
593  """Return the step to be used by the volume_up and volume_down services."""
594  if hasattr(self, "_attr_volume_step"):
595  return self._attr_volume_step
596  if (
597  hasattr(self, "entity_description")
598  and (volume_step := self.entity_description.volume_step) is not None
599  ):
600  return volume_step
601  return 0.1
602 
603  @cached_property
604  def is_volume_muted(self) -> bool | None:
605  """Boolean if volume is currently muted."""
606  return self._attr_is_volume_muted
607 
608  @cached_property
609  def media_content_id(self) -> str | None:
610  """Content ID of current playing media."""
611  return self._attr_media_content_id
612 
613  @cached_property
614  def media_content_type(self) -> MediaType | str | None:
615  """Content type of current playing media."""
616  return self._attr_media_content_type
617 
618  @cached_property
619  def media_duration(self) -> int | None:
620  """Duration of current playing media in seconds."""
621  return self._attr_media_duration
622 
623  @cached_property
624  def media_position(self) -> int | None:
625  """Position of current playing media in seconds."""
626  return self._attr_media_position
627 
628  @cached_property
629  def media_position_updated_at(self) -> dt.datetime | None:
630  """When was the position of the current playing media valid.
631 
632  Returns value from homeassistant.util.dt.utcnow().
633  """
634  return self._attr_media_position_updated_at
635 
636  @cached_property
637  def media_image_url(self) -> str | None:
638  """Image url of current playing media."""
639  return self._attr_media_image_url
640 
641  @cached_property
643  """If the image url is remotely accessible."""
644  return self._attr_media_image_remotely_accessible
645 
646  @property
647  def media_image_hash(self) -> str | None:
648  """Hash value for media image."""
649  if hasattr(self, "_attr_media_image_hash"):
650  return self._attr_media_image_hash
651 
652  if (url := self.media_image_urlmedia_image_url) is not None:
653  return _url_hash(url)
654 
655  return None
656 
657  async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
658  """Fetch media image of current playing image."""
659  if (url := self.media_image_urlmedia_image_url) is None:
660  return None, None
661 
662  return await self._async_fetch_image_from_cache_async_fetch_image_from_cache(url)
663 
665  self,
666  media_content_type: str,
667  media_content_id: str,
668  media_image_id: str | None = None,
669  ) -> tuple[bytes | None, str | None]:
670  """Optionally fetch internally accessible image for media browser.
671 
672  Must be implemented by integration.
673  """
674  return None, None
675 
676  @cached_property
677  def media_title(self) -> str | None:
678  """Title of current playing media."""
679  return self._attr_media_title
680 
681  @cached_property
682  def media_artist(self) -> str | None:
683  """Artist of current playing media, music track only."""
684  return self._attr_media_artist
685 
686  @cached_property
687  def media_album_name(self) -> str | None:
688  """Album name of current playing media, music track only."""
689  return self._attr_media_album_name
690 
691  @cached_property
692  def media_album_artist(self) -> str | None:
693  """Album artist of current playing media, music track only."""
694  return self._attr_media_album_artist
695 
696  @cached_property
697  def media_track(self) -> int | None:
698  """Track number of current playing media, music track only."""
699  return self._attr_media_track
700 
701  @cached_property
702  def media_series_title(self) -> str | None:
703  """Title of series of current playing media, TV show only."""
704  return self._attr_media_series_title
705 
706  @cached_property
707  def media_season(self) -> str | None:
708  """Season of current playing media, TV show only."""
709  return self._attr_media_season
710 
711  @cached_property
712  def media_episode(self) -> str | None:
713  """Episode of current playing media, TV show only."""
714  return self._attr_media_episode
715 
716  @cached_property
717  def media_channel(self) -> str | None:
718  """Channel currently playing."""
719  return self._attr_media_channel
720 
721  @cached_property
722  def media_playlist(self) -> str | None:
723  """Title of Playlist currently playing."""
724  return self._attr_media_playlist
725 
726  @cached_property
727  def app_id(self) -> str | None:
728  """ID of the current running app."""
729  return self._attr_app_id
730 
731  @cached_property
732  def app_name(self) -> str | None:
733  """Name of the current running app."""
734  return self._attr_app_name
735 
736  @cached_property
737  def source(self) -> str | None:
738  """Name of the current input source."""
739  return self._attr_source
740 
741  @cached_property
742  def source_list(self) -> list[str] | None:
743  """List of available input sources."""
744  return self._attr_source_list
745 
746  @cached_property
747  def sound_mode(self) -> str | None:
748  """Name of the current sound mode."""
749  return self._attr_sound_mode
750 
751  @cached_property
752  def sound_mode_list(self) -> list[str] | None:
753  """List of available sound modes."""
754  return self._attr_sound_mode_list
755 
756  @cached_property
757  def shuffle(self) -> bool | None:
758  """Boolean if shuffle is enabled."""
759  return self._attr_shuffle
760 
761  @cached_property
762  def repeat(self) -> RepeatMode | str | None:
763  """Return current repeat mode."""
764  return self._attr_repeat
765 
766  @cached_property
767  def group_members(self) -> list[str] | None:
768  """List of members which are currently grouped together."""
769  return self._attr_group_members
770 
771  @cached_property
772  def supported_features(self) -> MediaPlayerEntityFeature:
773  """Flag media player features that are supported."""
774  return self._attr_supported_features
775 
776  @property
777  def supported_features_compat(self) -> MediaPlayerEntityFeature:
778  """Return the supported features as MediaPlayerEntityFeature.
779 
780  Remove this compatibility shim in 2025.1 or later.
781  """
782  features = self.supported_featuressupported_featuressupported_features
783  if type(features) is int: # noqa: E721
784  new_features = MediaPlayerEntityFeature(features)
785  self._report_deprecated_supported_features_values_report_deprecated_supported_features_values(new_features)
786  return new_features
787  return features
788 
789  def turn_on(self) -> None:
790  """Turn the media player on."""
791  raise NotImplementedError
792 
793  async def async_turn_on(self) -> None:
794  """Turn the media player on."""
795  await self.hasshass.async_add_executor_job(self.turn_onturn_on)
796 
797  def turn_off(self) -> None:
798  """Turn the media player off."""
799  raise NotImplementedError
800 
801  async def async_turn_off(self) -> None:
802  """Turn the media player off."""
803  await self.hasshass.async_add_executor_job(self.turn_offturn_off)
804 
805  def mute_volume(self, mute: bool) -> None:
806  """Mute the volume."""
807  raise NotImplementedError
808 
809  async def async_mute_volume(self, mute: bool) -> None:
810  """Mute the volume."""
811  await self.hasshass.async_add_executor_job(self.mute_volumemute_volume, mute)
812 
813  def set_volume_level(self, volume: float) -> None:
814  """Set volume level, range 0..1."""
815  raise NotImplementedError
816 
817  async def async_set_volume_level(self, volume: float) -> None:
818  """Set volume level, range 0..1."""
819  await self.hasshass.async_add_executor_job(self.set_volume_levelset_volume_level, volume)
820 
821  def media_play(self) -> None:
822  """Send play command."""
823  raise NotImplementedError
824 
825  async def async_media_play(self) -> None:
826  """Send play command."""
827  await self.hasshass.async_add_executor_job(self.media_playmedia_play)
828 
829  def media_pause(self) -> None:
830  """Send pause command."""
831  raise NotImplementedError
832 
833  async def async_media_pause(self) -> None:
834  """Send pause command."""
835  await self.hasshass.async_add_executor_job(self.media_pausemedia_pause)
836 
837  def media_stop(self) -> None:
838  """Send stop command."""
839  raise NotImplementedError
840 
841  async def async_media_stop(self) -> None:
842  """Send stop command."""
843  await self.hasshass.async_add_executor_job(self.media_stopmedia_stop)
844 
845  def media_previous_track(self) -> None:
846  """Send previous track command."""
847  raise NotImplementedError
848 
849  async def async_media_previous_track(self) -> None:
850  """Send previous track command."""
851  await self.hasshass.async_add_executor_job(self.media_previous_trackmedia_previous_track)
852 
853  def media_next_track(self) -> None:
854  """Send next track command."""
855  raise NotImplementedError
856 
857  async def async_media_next_track(self) -> None:
858  """Send next track command."""
859  await self.hasshass.async_add_executor_job(self.media_next_trackmedia_next_track)
860 
861  def media_seek(self, position: float) -> None:
862  """Send seek command."""
863  raise NotImplementedError
864 
865  async def async_media_seek(self, position: float) -> None:
866  """Send seek command."""
867  await self.hasshass.async_add_executor_job(self.media_seekmedia_seek, position)
868 
870  self, media_type: MediaType | str, media_id: str, **kwargs: Any
871  ) -> None:
872  """Play a piece of media."""
873  raise NotImplementedError
874 
875  async def async_play_media(
876  self, media_type: MediaType | str, media_id: str, **kwargs: Any
877  ) -> None:
878  """Play a piece of media."""
879  await self.hasshass.async_add_executor_job(
880  ft.partial(self.play_mediaplay_media, media_type, media_id, **kwargs)
881  )
882 
883  def select_source(self, source: str) -> None:
884  """Select input source."""
885  raise NotImplementedError
886 
887  async def async_select_source(self, source: str) -> None:
888  """Select input source."""
889  await self.hasshass.async_add_executor_job(self.select_sourceselect_source, source)
890 
891  def select_sound_mode(self, sound_mode: str) -> None:
892  """Select sound mode."""
893  raise NotImplementedError
894 
895  async def async_select_sound_mode(self, sound_mode: str) -> None:
896  """Select sound mode."""
897  await self.hasshass.async_add_executor_job(self.select_sound_modeselect_sound_mode, sound_mode)
898 
899  def clear_playlist(self) -> None:
900  """Clear players playlist."""
901  raise NotImplementedError
902 
903  async def async_clear_playlist(self) -> None:
904  """Clear players playlist."""
905  await self.hasshass.async_add_executor_job(self.clear_playlistclear_playlist)
906 
907  def set_shuffle(self, shuffle: bool) -> None:
908  """Enable/disable shuffle mode."""
909  raise NotImplementedError
910 
911  async def async_set_shuffle(self, shuffle: bool) -> None:
912  """Enable/disable shuffle mode."""
913  await self.hasshass.async_add_executor_job(self.set_shuffleset_shuffle, shuffle)
914 
915  def set_repeat(self, repeat: RepeatMode) -> None:
916  """Set repeat mode."""
917  raise NotImplementedError
918 
919  async def async_set_repeat(self, repeat: RepeatMode) -> None:
920  """Set repeat mode."""
921  await self.hasshass.async_add_executor_job(self.set_repeatset_repeat, repeat)
922 
923  # No need to overwrite these.
924  @final
925  @property
926  def support_play(self) -> bool:
927  """Boolean if play is supported."""
928  return MediaPlayerEntityFeature.PLAY in self.supported_features_compatsupported_features_compat
929 
930  @final
931  @property
932  def support_pause(self) -> bool:
933  """Boolean if pause is supported."""
934  return MediaPlayerEntityFeature.PAUSE in self.supported_features_compatsupported_features_compat
935 
936  @final
937  @property
938  def support_stop(self) -> bool:
939  """Boolean if stop is supported."""
940  return MediaPlayerEntityFeature.STOP in self.supported_features_compatsupported_features_compat
941 
942  @final
943  @property
944  def support_seek(self) -> bool:
945  """Boolean if seek is supported."""
946  return MediaPlayerEntityFeature.SEEK in self.supported_features_compatsupported_features_compat
947 
948  @final
949  @property
950  def support_volume_set(self) -> bool:
951  """Boolean if setting volume is supported."""
952  return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compatsupported_features_compat
953 
954  @final
955  @property
956  def support_volume_mute(self) -> bool:
957  """Boolean if muting volume is supported."""
958  return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features_compatsupported_features_compat
959 
960  @final
961  @property
962  def support_previous_track(self) -> bool:
963  """Boolean if previous track command supported."""
964  return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features_compatsupported_features_compat
965 
966  @final
967  @property
968  def support_next_track(self) -> bool:
969  """Boolean if next track command supported."""
970  return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features_compatsupported_features_compat
971 
972  @final
973  @property
974  def support_play_media(self) -> bool:
975  """Boolean if play media command supported."""
976  return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features_compatsupported_features_compat
977 
978  @final
979  @property
980  def support_select_source(self) -> bool:
981  """Boolean if select source command supported."""
982  return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features_compatsupported_features_compat
983 
984  @final
985  @property
986  def support_select_sound_mode(self) -> bool:
987  """Boolean if select sound mode command supported."""
988  return (
989  MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features_compatsupported_features_compat
990  )
991 
992  @final
993  @property
994  def support_clear_playlist(self) -> bool:
995  """Boolean if clear playlist command supported."""
996  return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features_compatsupported_features_compat
997 
998  @final
999  @property
1000  def support_shuffle_set(self) -> bool:
1001  """Boolean if shuffle is supported."""
1002  return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features_compatsupported_features_compat
1003 
1004  @final
1005  @property
1006  def support_grouping(self) -> bool:
1007  """Boolean if player grouping is supported."""
1008  return MediaPlayerEntityFeature.GROUPING in self.supported_features_compatsupported_features_compat
1009 
1010  async def async_toggle(self) -> None:
1011  """Toggle the power on the media player."""
1012  if hasattr(self, "toggle"):
1013  await self.hasshass.async_add_executor_job(self.toggle)
1014  return
1015 
1016  if self.statestatestatestate in {
1017  MediaPlayerState.OFF,
1018  MediaPlayerState.IDLE,
1019  MediaPlayerState.STANDBY,
1020  }:
1021  await self.async_turn_onasync_turn_on()
1022  else:
1023  await self.async_turn_offasync_turn_off()
1024 
1025  async def async_volume_up(self) -> None:
1026  """Turn volume up for media player.
1027 
1028  This method is a coroutine.
1029  """
1030  if hasattr(self, "volume_up"):
1031  await self.hasshass.async_add_executor_job(self.volume_up)
1032  return
1033 
1034  if (
1035  self.volume_levelvolume_level is not None
1036  and self.volume_levelvolume_level < 1
1037  and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compatsupported_features_compat
1038  ):
1039  await self.async_set_volume_levelasync_set_volume_level(
1040  min(1, self.volume_levelvolume_level + self.volume_stepvolume_step)
1041  )
1042 
1043  async def async_volume_down(self) -> None:
1044  """Turn volume down for media player.
1045 
1046  This method is a coroutine.
1047  """
1048  if hasattr(self, "volume_down"):
1049  await self.hasshass.async_add_executor_job(self.volume_down)
1050  return
1051 
1052  if (
1053  self.volume_levelvolume_level is not None
1054  and self.volume_levelvolume_level > 0
1055  and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compatsupported_features_compat
1056  ):
1057  await self.async_set_volume_levelasync_set_volume_level(
1058  max(0, self.volume_levelvolume_level - self.volume_stepvolume_step)
1059  )
1060 
1061  async def async_media_play_pause(self) -> None:
1062  """Play or pause the media player."""
1063  if hasattr(self, "media_play_pause"):
1064  await self.hasshass.async_add_executor_job(self.media_play_pause)
1065  return
1066 
1067  if self.statestatestatestate == MediaPlayerState.PLAYING:
1068  await self.async_media_pauseasync_media_pause()
1069  else:
1070  await self.async_media_playasync_media_play()
1071 
1072  @property
1073  def entity_picture(self) -> str | None:
1074  """Return image of the media playing."""
1075  if self.statestatestatestate == MediaPlayerState.OFF:
1076  return None
1077 
1078  if self.media_image_remotely_accessiblemedia_image_remotely_accessible:
1079  return self.media_image_urlmedia_image_url
1080 
1081  return self.media_image_localmedia_image_local
1082 
1083  @property
1084  def media_image_local(self) -> str | None:
1085  """Return local url to media image."""
1086  if (image_hash := self.media_image_hashmedia_image_hash) is None:
1087  return None
1088 
1089  return (
1090  f"/api/media_player_proxy/{self.entity_id}?"
1091  f"token={self.access_token}&cache={image_hash}"
1092  )
1093 
1094  @property
1095  def capability_attributes(self) -> dict[str, Any]:
1096  """Return capability attributes."""
1097  data: dict[str, Any] = {}
1098  supported_features = self.supported_features_compatsupported_features_compat
1099 
1100  if (
1101  source_list := self.source_listsource_list
1102  ) and MediaPlayerEntityFeature.SELECT_SOURCE in supported_features:
1103  data[ATTR_INPUT_SOURCE_LIST] = source_list
1104 
1105  if (
1106  sound_mode_list := self.sound_mode_listsound_mode_list
1107  ) and MediaPlayerEntityFeature.SELECT_SOUND_MODE in supported_features:
1108  data[ATTR_SOUND_MODE_LIST] = sound_mode_list
1109 
1110  return data
1111 
1112  @final
1113  @property
1114  def state_attributes(self) -> dict[str, Any]:
1115  """Return the state attributes."""
1116  state_attr: dict[str, Any] = {}
1117 
1118  if self.support_groupingsupport_grouping:
1119  state_attr[ATTR_GROUP_MEMBERS] = self.group_membersgroup_members
1120 
1121  if self.statestatestatestate == MediaPlayerState.OFF:
1122  return state_attr
1123 
1124  for attr in ATTR_TO_PROPERTY:
1125  if (value := getattr(self, attr)) is not None:
1126  state_attr[attr] = value
1127 
1128  if self.media_image_remotely_accessiblemedia_image_remotely_accessible:
1129  state_attr[ATTR_ENTITY_PICTURE_LOCAL] = self.media_image_localmedia_image_local
1130 
1131  return state_attr
1132 
1134  self,
1135  media_content_type: MediaType | str | None = None,
1136  media_content_id: str | None = None,
1137  ) -> BrowseMedia:
1138  """Return a BrowseMedia instance.
1139 
1140  The BrowseMedia instance will be used by the
1141  "media_player/browse_media" websocket command.
1142  """
1143  raise NotImplementedError
1144 
1145  def join_players(self, group_members: list[str]) -> None:
1146  """Join `group_members` as a player group with the current player."""
1147  raise NotImplementedError
1148 
1149  async def async_join_players(self, group_members: list[str]) -> None:
1150  """Join `group_members` as a player group with the current player."""
1151  await self.hasshass.async_add_executor_job(self.join_playersjoin_players, group_members)
1152 
1153  def unjoin_player(self) -> None:
1154  """Remove this player from any group."""
1155  raise NotImplementedError
1156 
1157  async def async_unjoin_player(self) -> None:
1158  """Remove this player from any group."""
1159  await self.hasshass.async_add_executor_job(self.unjoin_playerunjoin_player)
1160 
1162  self, url: str
1163  ) -> tuple[bytes | None, str | None]:
1164  """Fetch image.
1165 
1166  Images are cached in memory (the images are typically 10-100kB in size).
1167  """
1168  cache_images = _ENTITY_IMAGE_CACHE[CACHE_IMAGES]
1169  cache_maxsize = _ENTITY_IMAGE_CACHE[CACHE_MAXSIZE]
1170 
1171  if urlparse(url).hostname is None:
1172  url = f"{get_url(self.hass)}{url}"
1173 
1174  if url not in cache_images:
1175  cache_images[url] = {CACHE_LOCK: asyncio.Lock()}
1176 
1177  async with cache_images[url][CACHE_LOCK]:
1178  if CACHE_CONTENT in cache_images[url]:
1179  return cache_images[url][CACHE_CONTENT]
1180 
1181  (content, content_type) = await self._async_fetch_image_async_fetch_image(url)
1182 
1183  async with cache_images[url][CACHE_LOCK]:
1184  cache_images[url][CACHE_CONTENT] = content, content_type
1185  while len(cache_images) > cache_maxsize:
1186  cache_images.popitem(last=False)
1187 
1188  return content, content_type
1189 
1190  async def _async_fetch_image(self, url: str) -> tuple[bytes | None, str | None]:
1191  """Retrieve an image."""
1192  return await async_fetch_image(_LOGGER, self.hasshass, url)
1193 
1195  self,
1196  media_content_type: str,
1197  media_content_id: str,
1198  media_image_id: str | None = None,
1199  ) -> str:
1200  """Generate an url for a media browser image."""
1201  url_path = (
1202  f"/api/media_player_proxy/{self.entity_id}/browse_media"
1203  # quote the media_content_id as it may contain url unsafe characters
1204  # aiohttp will unquote the path automatically
1205  f"/{media_content_type}/{quote(media_content_id)}"
1206  )
1207 
1208  url_query = {"token": self.access_tokenaccess_token}
1209  if media_image_id:
1210  url_query["media_image_id"] = media_image_id
1211 
1212  return str(URL(url_path).with_query(url_query))
1213 
1214 
1215 class MediaPlayerImageView(HomeAssistantView):
1216  """Media player view to serve an image."""
1217 
1218  requires_auth = False
1219  url = "/api/media_player_proxy/{entity_id}"
1220  name = "api:media_player:image"
1221  extra_urls = [
1222  # Need to modify the default regex for media_content_id as it may
1223  # include arbitrary characters including '/','{', or '}'
1224  url + "/browse_media/{media_content_type}/{media_content_id:.+}",
1225  ]
1226 
1227  def __init__(self, component: EntityComponent[MediaPlayerEntity]) -> None:
1228  """Initialize a media player view."""
1229  self.componentcomponent = component
1230 
1231  async def get(
1232  self,
1233  request: web.Request,
1234  entity_id: str,
1235  media_content_type: MediaType | str | None = None,
1236  media_content_id: str | None = None,
1237  ) -> web.Response:
1238  """Start a get request."""
1239  if (player := self.componentcomponent.get_entity(entity_id)) is None:
1240  status = (
1241  HTTPStatus.NOT_FOUND
1242  if request[KEY_AUTHENTICATED]
1243  else HTTPStatus.UNAUTHORIZED
1244  )
1245  return web.Response(status=status)
1246 
1247  assert isinstance(player, MediaPlayerEntity)
1248  authenticated = (
1249  request[KEY_AUTHENTICATED]
1250  or request.query.get("token") == player.access_token
1251  )
1252 
1253  if not authenticated:
1254  return web.Response(status=HTTPStatus.UNAUTHORIZED)
1255 
1256  if media_content_type and media_content_id:
1257  media_image_id = request.query.get("media_image_id")
1258  data, content_type = await player.async_get_browse_image(
1259  media_content_type, media_content_id, media_image_id
1260  )
1261  else:
1262  data, content_type = await player.async_get_media_image()
1263 
1264  if data is None:
1265  return web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)
1266 
1267  headers: LooseHeaders = {CACHE_CONTROL: "max-age=3600"}
1268  return web.Response(body=data, content_type=content_type, headers=headers)
1269 
1270 
1271 @websocket_api.websocket_command( { vol.Required("type"): "media_player/browse_media",
1272  vol.Required("entity_id"): cv.entity_id,
1273  vol.Inclusive(
1274  ATTR_MEDIA_CONTENT_TYPE,
1275  "media_ids",
1276  "media_content_type and media_content_id must be provided together",
1277  ): str,
1278  vol.Inclusive(
1279  ATTR_MEDIA_CONTENT_ID,
1280  "media_ids",
1281  "media_content_type and media_content_id must be provided together",
1282  ): str,
1283  }
1284 )
1285 @websocket_api.async_response
1286 async def websocket_browse_media(
1287  hass: HomeAssistant,
1289  msg: dict[str, Any],
1290 ) -> None:
1291  """Browse media available to the media_player entity.
1292 
1293  To use, media_player integrations can implement
1294  MediaPlayerEntity.async_browse_media()
1295  """
1296  player = hass.data[DATA_COMPONENT].get_entity(msg["entity_id"])
1297 
1298  if player is None:
1299  connection.send_error(msg["id"], "entity_not_found", "Entity not found")
1300  return
1301 
1302  if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features_compat:
1303  connection.send_message(
1304  websocket_api.error_message(
1305  msg["id"], ERR_NOT_SUPPORTED, "Player does not support browsing media"
1306  )
1307  )
1308  return
1309 
1310  media_content_type = msg.get(ATTR_MEDIA_CONTENT_TYPE)
1311  media_content_id = msg.get(ATTR_MEDIA_CONTENT_ID)
1312 
1313  try:
1314  payload = await player.async_browse_media(media_content_type, media_content_id)
1315  except NotImplementedError:
1316  assert player.platform
1317  _LOGGER.error(
1318  "%s allows media browsing but its integration (%s) does not",
1319  player.entity_id,
1320  player.platform.platform_name,
1321  )
1322  connection.send_message(
1323  websocket_api.error_message(
1324  msg["id"],
1325  ERR_NOT_SUPPORTED,
1326  "Integration does not support browsing media",
1327  )
1328  )
1329  return
1330  except BrowseError as err:
1331  connection.send_message(
1332  websocket_api.error_message(msg["id"], ERR_UNKNOWN_ERROR, str(err))
1333  )
1334  return
1335 
1336  # For backwards compat
1337  if isinstance(payload, BrowseMedia):
1338  result = payload.as_dict()
1339  else:
1340  result = payload # type: ignore[unreachable]
1341  _LOGGER.warning("Browse Media should use new BrowseMedia class")
1342 
1343  connection.send_result(msg["id"], result)
1344 
1345 
1346 _FETCH_TIMEOUT = aiohttp.ClientTimeout(total=10)
1347 
1349 async def async_fetch_image(
1350  logger: logging.Logger, hass: HomeAssistant, url: str
1351 ) -> tuple[bytes | None, str | None]:
1352  """Retrieve an image."""
1353  content, content_type = (None, None)
1354  websession = async_get_clientsession(hass)
1355  with suppress(TimeoutError):
1356  response = await websession.get(url, timeout=_FETCH_TIMEOUT)
1357  if response.status == HTTPStatus.OK:
1358  content = await response.read()
1359  if content_type := response.headers.get(CONTENT_TYPE):
1360  content_type = content_type.split(";")[0]
1361 
1362  if content is None:
1363  url_parts = URL(url)
1364  if url_parts.user is not None:
1365  url_parts = url_parts.with_user("xxxx")
1366  if url_parts.password is not None:
1367  url_parts = url_parts.with_password("xxxxxxxx")
1368  url = str(url_parts)
1369  logger.warning("Error retrieving proxied image from %s", url)
1370 
1371  return content, content_type
1372 
1373 
1374 # As we import deprecated constants from the const module, we need to add these two functions
1375 # otherwise this module will be logged for using deprecated constants and not the custom component
1376 # These can be removed if no deprecated constant are in this module anymore
1377 __getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals())
1378 __dir__ = ft.partial(
1379  dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
1381 __all__ = all_with_deprecated_constants(globals())
1382 
MediaPlayerEntityFeature supported_features_compat(self)
Definition: __init__.py:777
None async_set_repeat(self, RepeatMode repeat)
Definition: __init__.py:919
None async_play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
Definition: __init__.py:877
BrowseMedia async_browse_media(self, MediaType|str|None media_content_type=None, str|None media_content_id=None)
Definition: __init__.py:1137
tuple[bytes|None, str|None] _async_fetch_image_from_cache(self, str url)
Definition: __init__.py:1163
None join_players(self, list[str] group_members)
Definition: __init__.py:1145
None async_join_players(self, list[str] group_members)
Definition: __init__.py:1149
str get_browse_image_url(self, str media_content_type, str media_content_id, str|None media_image_id=None)
Definition: __init__.py:1199
None play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
Definition: __init__.py:871
tuple[bytes|None, str|None] _async_fetch_image(self, str url)
Definition: __init__.py:1190
tuple[bytes|None, str|None] async_get_browse_image(self, str media_content_type, str media_content_id, str|None media_image_id=None)
Definition: __init__.py:669
MediaPlayerDeviceClass|None device_class(self)
Definition: __init__.py:566
tuple[bytes|None, str|None] async_get_media_image(self)
Definition: __init__.py:657
MediaPlayerEntityFeature supported_features(self)
Definition: __init__.py:772
None __init__(self, EntityComponent[MediaPlayerEntity] component)
Definition: __init__.py:1227
web.Response get(self, web.Request request, str entity_id, MediaType|str|None media_content_type=None, str|None media_content_id=None)
Definition: __init__.py:1237
None _report_deprecated_supported_features_values(self, IntFlag replacement)
Definition: entity.py:1645
int|None supported_features(self)
Definition: entity.py:861
CalendarEntity get_entity(HomeAssistant hass, str entity_id)
Definition: trigger.py:96
bool is_on(HomeAssistant hass, str|None entity_id=None)
Definition: __init__.py:251
Callable[[dict[str, Any]], dict[str, Any]] _rename_keys(**Any keys)
Definition: __init__.py:263
None websocket_browse_media(HomeAssistant hass, websocket_api.connection.ActiveConnection connection, dict[str, Any] msg)
Definition: __init__.py:1292
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:454
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:280
tuple[bytes|None, str|None] async_fetch_image(logging.Logger logger, HomeAssistant hass, str url)
Definition: __init__.py:1353
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:459
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
list[str] all_with_deprecated_constants(dict[str, Any] module_globals)
Definition: deprecation.py:356