From 8ff0ced846e505a0c33a848e21b19820861e6884 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 May 2022 04:46:13 +1200 Subject: [PATCH] Initial implementation of ESPHome media players (#72047) Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof --- .coveragerc | 1 + .../components/esphome/entry_data.py | 2 + .../components/esphome/media_player.py | 151 ++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 homeassistant/components/esphome/media_player.py diff --git a/.coveragerc b/.coveragerc index da12b41d222..bd7aa405b94 100644 --- a/.coveragerc +++ b/.coveragerc @@ -314,6 +314,7 @@ omit = homeassistant/components/esphome/fan.py homeassistant/components/esphome/light.py homeassistant/components/esphome/lock.py + homeassistant/components/esphome/media_player.py homeassistant/components/esphome/number.py homeassistant/components/esphome/select.py homeassistant/components/esphome/sensor.py diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index c00073b4432..4c5a94afe0f 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -20,6 +20,7 @@ from aioesphomeapi import ( FanInfo, LightInfo, LockInfo, + MediaPlayerInfo, NumberInfo, SelectInfo, SensorInfo, @@ -46,6 +47,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { FanInfo: "fan", LightInfo: "light", LockInfo: "lock", + MediaPlayerInfo: "media_player", NumberInfo: "number", SelectInfo: "select", SensorInfo: "sensor", diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py new file mode 100644 index 00000000000..6e83d12a427 --- /dev/null +++ b/homeassistant/components/esphome/media_player.py @@ -0,0 +1,151 @@ +"""Support for ESPHome media players.""" +from __future__ import annotations + +from typing import Any + +from aioesphomeapi import ( + MediaPlayerCommand, + MediaPlayerEntityState, + MediaPlayerInfo, + MediaPlayerState, +) + +from homeassistant.components import media_source +from homeassistant.components.media_player import ( + MediaPlayerDeviceClass, + MediaPlayerEntity, +) +from homeassistant.components.media_player.browse_media import ( + BrowseMedia, + async_process_play_media_url, +) +from homeassistant.components.media_player.const import MediaPlayerEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import EsphomeEntity, EsphomeEnumMapper, platform_async_setup_entry + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up esphome media players based on a config entry.""" + await platform_async_setup_entry( + hass, + entry, + async_add_entities, + component_key="media_player", + info_type=MediaPlayerInfo, + entity_type=EsphomeMediaPlayer, + state_type=MediaPlayerEntityState, + ) + + +_STATES: EsphomeEnumMapper[MediaPlayerState, str] = EsphomeEnumMapper( + { + MediaPlayerState.IDLE: STATE_IDLE, + MediaPlayerState.PLAYING: STATE_PLAYING, + MediaPlayerState.PAUSED: STATE_PAUSED, + } +) + + +class EsphomeMediaPlayer( + EsphomeEntity[MediaPlayerInfo, MediaPlayerEntityState], MediaPlayerEntity +): + """A media player implementation for esphome.""" + + _attr_device_class = MediaPlayerDeviceClass.SPEAKER + + @property + def state(self) -> str | None: + """Return current state.""" + return _STATES.from_esphome(self._state.state) + + @property + def is_volume_muted(self) -> bool: + """Return true if volume is muted.""" + return self._state.muted + + @property + def volume_level(self) -> float | None: + """Volume level of the media player (0..1).""" + return self._state.volume + + @property + def supported_features(self) -> int: + """Flag supported features.""" + flags = ( + MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_MUTE + ) + if self._static_info.supports_pause: + flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY + return flags + + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: + """Send the play command with media url to the media player.""" + if media_source.is_media_source_id(media_id): + sourced_media = await media_source.async_resolve_media(self.hass, media_id) + media_id = sourced_media.url + + media_id = async_process_play_media_url(self.hass, media_id) + + await self._client.media_player_command( + self._static_info.key, + media_url=media_id, + ) + + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) + + async def async_set_volume_level(self, volume: float) -> None: + """Set volume level, range 0..1.""" + await self._client.media_player_command( + self._static_info.key, + volume=volume, + ) + + async def async_media_pause(self) -> None: + """Send pause command.""" + await self._client.media_player_command( + self._static_info.key, + command=MediaPlayerCommand.PAUSE, + ) + + async def async_media_play(self) -> None: + """Send play command.""" + await self._client.media_player_command( + self._static_info.key, + command=MediaPlayerCommand.PLAY, + ) + + async def async_media_stop(self) -> None: + """Send stop command.""" + await self._client.media_player_command( + self._static_info.key, + command=MediaPlayerCommand.STOP, + ) + + async def async_mute_volume(self, mute: bool) -> None: + """Mute the volume.""" + await self._client.media_player_command( + self._static_info.key, + command=MediaPlayerCommand.MUTE if mute else MediaPlayerCommand.UNMUTE, + )