1 """Support for ESPHome media players."""
3 from __future__
import annotations
5 from functools
import partial
7 from typing
import Any, cast
8 from urllib.parse
import urlparse
10 from aioesphomeapi
import (
13 MediaPlayerEntityState,
14 MediaPlayerFormatPurpose,
16 MediaPlayerState
as EspMediaPlayerState,
17 MediaPlayerSupportedFormat,
25 MediaPlayerDeviceClass,
27 MediaPlayerEntityFeature,
30 async_process_play_media_url,
36 convert_api_error_ha_error,
37 esphome_float_state_property,
38 esphome_state_property,
39 platform_async_setup_entry,
41 from .enum_mapper
import EsphomeEnumMapper
42 from .ffmpeg_proxy
import async_create_proxy_url
44 _LOGGER = logging.getLogger(__name__)
46 _STATES: EsphomeEnumMapper[EspMediaPlayerState, MediaPlayerState] =
EsphomeEnumMapper(
48 EspMediaPlayerState.IDLE: MediaPlayerState.IDLE,
49 EspMediaPlayerState.PLAYING: MediaPlayerState.PLAYING,
50 EspMediaPlayerState.PAUSED: MediaPlayerState.PAUSED,
54 ATTR_BYPASS_PROXY =
"bypass_proxy"
58 EsphomeEntity[MediaPlayerInfo, MediaPlayerEntityState], MediaPlayerEntity
60 """A media player implementation for esphome."""
62 _attr_device_class = MediaPlayerDeviceClass.SPEAKER
66 """Set attrs from static info."""
69 MediaPlayerEntityFeature.PLAY_MEDIA
70 | MediaPlayerEntityFeature.BROWSE_MEDIA
71 | MediaPlayerEntityFeature.STOP
72 | MediaPlayerEntityFeature.VOLUME_SET
73 | MediaPlayerEntityFeature.VOLUME_MUTE
74 | MediaPlayerEntityFeature.MEDIA_ANNOUNCE
77 flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY
79 self.
_entry_data_entry_data.media_player_formats[static_info.unique_id] = cast(
80 MediaPlayerInfo, static_info
84 @esphome_state_property
85 def state(self) -> MediaPlayerState | None:
86 """Return current state."""
87 return _STATES.from_esphome(self.
_state_state.state)
90 @esphome_state_property
92 """Return true if volume is muted."""
93 return self.
_state_state.muted
96 @esphome_float_state_property
98 """Volume level of the media player (0..1)."""
99 return self.
_state_state.volume
101 @convert_api_error_ha_error
103 self, media_type: MediaType | str, media_id: str, **kwargs: Any
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(
110 media_id = sourced_media.url
113 announcement = kwargs.get(ATTR_MEDIA_ANNOUNCE)
114 bypass_proxy = kwargs.get(ATTR_MEDIA_EXTRA, {}).
get(ATTR_BYPASS_PROXY)
116 supported_formats: list[MediaPlayerSupportedFormat] |
None = (
122 and supported_formats
126 supported_formats, media_id, announcement
is True
133 self.
_client_client.media_player_command(
134 self.
_key_key, media_url=media_id, announcement=announcement
138 """Handle entity being removed."""
144 supported_formats: list[MediaPlayerSupportedFormat],
148 """Get URL for ffmpeg proxy."""
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
160 format_to_use = supported_format
161 elif announcement
and (
162 supported_format.purpose == MediaPlayerFormatPurpose.ANNOUNCEMENT
165 format_to_use = supported_format
168 if format_to_use
is None:
175 _LOGGER.debug(
"Proxying media url %s with format %s", url, format_to_use)
177 media_format = format_to_use.format
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
186 if format_to_use.num_channels > 0:
187 channels = format_to_use.num_channels
189 if format_to_use.sample_bytes > 0:
190 width = format_to_use.sample_bytes
196 media_format=media_format,
207 media_content_type: MediaType | str |
None =
None,
208 media_content_id: str |
None =
None,
210 """Implement the websocket media browsing helper."""
211 return await media_source.async_browse_media(
214 content_filter=
lambda item: item.media_content_type.startswith(
"audio/"),
217 @convert_api_error_ha_error
219 """Set volume level, range 0..1."""
220 self.
_client_client.media_player_command(self.
_key_key, volume=volume)
222 @convert_api_error_ha_error
224 """Send pause command."""
225 self.
_client_client.media_player_command(self.
_key_key, command=MediaPlayerCommand.PAUSE)
227 @convert_api_error_ha_error
229 """Send play command."""
230 self.
_client_client.media_player_command(self.
_key_key, command=MediaPlayerCommand.PLAY)
232 @convert_api_error_ha_error
234 """Send stop command."""
235 self.
_client_client.media_player_command(self.
_key_key, command=MediaPlayerCommand.STOP)
237 @convert_api_error_ha_error
239 """Mute the volume."""
240 self.
_client_client.media_player_command(
242 command=MediaPlayerCommand.MUTE
if mute
else MediaPlayerCommand.UNMUTE,
247 """Validate the URL can be parsed and at least has scheme + netloc."""
248 result = urlparse(url)
249 return all([result.scheme, result.netloc])
252 async_setup_entry = partial(
253 platform_async_setup_entry,
254 info_type=MediaPlayerInfo,
255 entity_type=EsphomeMediaPlayer,
256 state_type=MediaPlayerEntityState,
web.Response get(self, web.Request request, str config_key)
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)