Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Support for ESPHome media players."""
2 
3 from __future__ import annotations
4 
5 from functools import partial
6 import logging
7 from typing import Any, cast
8 from urllib.parse import urlparse
9 
10 from aioesphomeapi import (
11  EntityInfo,
12  MediaPlayerCommand,
13  MediaPlayerEntityState,
14  MediaPlayerFormatPurpose,
15  MediaPlayerInfo,
16  MediaPlayerState as EspMediaPlayerState,
17  MediaPlayerSupportedFormat,
18 )
19 
20 from homeassistant.components import media_source
22  ATTR_MEDIA_ANNOUNCE,
23  ATTR_MEDIA_EXTRA,
24  BrowseMedia,
25  MediaPlayerDeviceClass,
26  MediaPlayerEntity,
27  MediaPlayerEntityFeature,
28  MediaPlayerState,
29  MediaType,
30  async_process_play_media_url,
31 )
32 from homeassistant.core import callback
33 
34 from .entity import (
35  EsphomeEntity,
36  convert_api_error_ha_error,
37  esphome_float_state_property,
38  esphome_state_property,
39  platform_async_setup_entry,
40 )
41 from .enum_mapper import EsphomeEnumMapper
42 from .ffmpeg_proxy import async_create_proxy_url
43 
44 _LOGGER = logging.getLogger(__name__)
45 
46 _STATES: EsphomeEnumMapper[EspMediaPlayerState, MediaPlayerState] = EsphomeEnumMapper(
47  {
48  EspMediaPlayerState.IDLE: MediaPlayerState.IDLE,
49  EspMediaPlayerState.PLAYING: MediaPlayerState.PLAYING,
50  EspMediaPlayerState.PAUSED: MediaPlayerState.PAUSED,
51  }
52 )
53 
54 ATTR_BYPASS_PROXY = "bypass_proxy"
55 
56 
58  EsphomeEntity[MediaPlayerInfo, MediaPlayerEntityState], MediaPlayerEntity
59 ):
60  """A media player implementation for esphome."""
61 
62  _attr_device_class = MediaPlayerDeviceClass.SPEAKER
63 
64  @callback
65  def _on_static_info_update(self, static_info: EntityInfo) -> None:
66  """Set attrs from static info."""
67  super()._on_static_info_update(static_info)
68  flags = (
69  MediaPlayerEntityFeature.PLAY_MEDIA
70  | MediaPlayerEntityFeature.BROWSE_MEDIA
71  | MediaPlayerEntityFeature.STOP
72  | MediaPlayerEntityFeature.VOLUME_SET
73  | MediaPlayerEntityFeature.VOLUME_MUTE
74  | MediaPlayerEntityFeature.MEDIA_ANNOUNCE
75  )
76  if self._static_info_static_info.supports_pause:
77  flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY
78  self._attr_supported_features_attr_supported_features = flags
79  self._entry_data_entry_data.media_player_formats[static_info.unique_id] = cast(
80  MediaPlayerInfo, static_info
81  ).supported_formats
82 
83  @property
84  @esphome_state_property
85  def state(self) -> MediaPlayerState | None:
86  """Return current state."""
87  return _STATES.from_esphome(self._state_state.state)
88 
89  @property
90  @esphome_state_property
91  def is_volume_muted(self) -> bool:
92  """Return true if volume is muted."""
93  return self._state_state.muted
94 
95  @property
96  @esphome_float_state_property
97  def volume_level(self) -> float | None:
98  """Volume level of the media player (0..1)."""
99  return self._state_state.volume
100 
101  @convert_api_error_ha_error
102  async def async_play_media(
103  self, media_type: MediaType | str, media_id: str, **kwargs: Any
104  ) -> None:
105  """Send the play command with media url to the media player."""
106  if media_source.is_media_source_id(media_id):
107  sourced_media = await media_source.async_resolve_media(
108  self.hasshass, media_id, self.entity_identity_identity_id
109  )
110  media_id = sourced_media.url
111 
112  media_id = async_process_play_media_url(self.hasshass, media_id)
113  announcement = kwargs.get(ATTR_MEDIA_ANNOUNCE)
114  bypass_proxy = kwargs.get(ATTR_MEDIA_EXTRA, {}).get(ATTR_BYPASS_PROXY)
115 
116  supported_formats: list[MediaPlayerSupportedFormat] | None = (
117  self._entry_data_entry_data.media_player_formats.get(self._static_info_static_info.unique_id)
118  )
119 
120  if (
121  not bypass_proxy
122  and supported_formats
123  and _is_url(media_id)
124  and (
125  proxy_url := self._get_proxy_url_get_proxy_url(
126  supported_formats, media_id, announcement is True
127  )
128  )
129  ):
130  # Substitute proxy URL
131  media_id = proxy_url
132 
133  self._client_client.media_player_command(
134  self._key_key, media_url=media_id, announcement=announcement
135  )
136 
137  async def async_will_remove_from_hass(self) -> None:
138  """Handle entity being removed."""
139  await super().async_will_remove_from_hass()
140  self._entry_data_entry_data.media_player_formats.pop(self.entity_identity_identity_id, None)
141 
143  self,
144  supported_formats: list[MediaPlayerSupportedFormat],
145  url: str,
146  announcement: bool,
147  ) -> str | None:
148  """Get URL for ffmpeg proxy."""
149  if self.device_entrydevice_entry is None:
150  # Device id is required
151  return None
152 
153  # Choose the first default or announcement supported format
154  format_to_use: MediaPlayerSupportedFormat | None = None
155  for supported_format in supported_formats:
156  if (format_to_use is None) and (
157  supported_format.purpose == MediaPlayerFormatPurpose.DEFAULT
158  ):
159  # First default format
160  format_to_use = supported_format
161  elif announcement and (
162  supported_format.purpose == MediaPlayerFormatPurpose.ANNOUNCEMENT
163  ):
164  # First announcement format
165  format_to_use = supported_format
166  break
167 
168  if format_to_use is None:
169  # No format for conversion
170  return None
171 
172  # Replace the media URL with a proxy URL pointing to Home
173  # Assistant. When requested, Home Assistant will use ffmpeg to
174  # convert the source URL to the supported format.
175  _LOGGER.debug("Proxying media url %s with format %s", url, format_to_use)
176  device_id = self.device_entrydevice_entry.id
177  media_format = format_to_use.format
178 
179  # 0 = None
180  rate: int | None = None
181  channels: int | None = None
182  width: int | None = None
183  if format_to_use.sample_rate > 0:
184  rate = format_to_use.sample_rate
185 
186  if format_to_use.num_channels > 0:
187  channels = format_to_use.num_channels
188 
189  if format_to_use.sample_bytes > 0:
190  width = format_to_use.sample_bytes
191 
192  proxy_url = async_create_proxy_url(
193  self.hasshass,
194  device_id,
195  url,
196  media_format=media_format,
197  rate=rate,
198  channels=channels,
199  width=width,
200  )
201 
202  # Resolve URL
203  return async_process_play_media_url(self.hasshass, proxy_url)
204 
206  self,
207  media_content_type: MediaType | str | None = None,
208  media_content_id: str | None = None,
209  ) -> BrowseMedia:
210  """Implement the websocket media browsing helper."""
211  return await media_source.async_browse_media(
212  self.hasshass,
213  media_content_id,
214  content_filter=lambda item: item.media_content_type.startswith("audio/"),
215  )
216 
217  @convert_api_error_ha_error
218  async def async_set_volume_level(self, volume: float) -> None:
219  """Set volume level, range 0..1."""
220  self._client_client.media_player_command(self._key_key, volume=volume)
221 
222  @convert_api_error_ha_error
223  async def async_media_pause(self) -> None:
224  """Send pause command."""
225  self._client_client.media_player_command(self._key_key, command=MediaPlayerCommand.PAUSE)
226 
227  @convert_api_error_ha_error
228  async def async_media_play(self) -> None:
229  """Send play command."""
230  self._client_client.media_player_command(self._key_key, command=MediaPlayerCommand.PLAY)
231 
232  @convert_api_error_ha_error
233  async def async_media_stop(self) -> None:
234  """Send stop command."""
235  self._client_client.media_player_command(self._key_key, command=MediaPlayerCommand.STOP)
236 
237  @convert_api_error_ha_error
238  async def async_mute_volume(self, mute: bool) -> None:
239  """Mute the volume."""
240  self._client_client.media_player_command(
241  self._key_key,
242  command=MediaPlayerCommand.MUTE if mute else MediaPlayerCommand.UNMUTE,
243  )
244 
245 
246 def _is_url(url: str) -> bool:
247  """Validate the URL can be parsed and at least has scheme + netloc."""
248  result = urlparse(url)
249  return all([result.scheme, result.netloc])
250 
251 
252 async_setup_entry = partial(
253  platform_async_setup_entry,
254  info_type=MediaPlayerInfo,
255  entity_type=EsphomeMediaPlayer,
256  state_type=MediaPlayerEntityState,
257 )
str|None _get_proxy_url(self, list[MediaPlayerSupportedFormat] supported_formats, str url, bool announcement)
None async_play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
BrowseMedia async_browse_media(self, MediaType|str|None media_content_type=None, str|None media_content_id=None)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
str async_create_proxy_url(HomeAssistant hass, str device_id, str media_url, str media_format, int|None rate=None, int|None channels=None, int|None width=None)
Definition: ffmpeg_proxy.py:33
str async_process_play_media_url(HomeAssistant hass, str media_content_id, *bool allow_relative_url=False, bool for_supervisor_network=False)
Definition: browse_media.py:36