"""Support for media players through the SmartThings cloud API.""" from __future__ import annotations from typing import Any from pysmartthings import Attribute, Capability, Category, Command, SmartThings from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, RepeatMode, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import FullDevice, SmartThingsConfigEntry from .const import MAIN from .entity import SmartThingsEntity MEDIA_PLAYER_CAPABILITIES = ( Capability.AUDIO_MUTE, Capability.AUDIO_VOLUME, ) CONTROLLABLE_SOURCES = ["bluetooth", "wifi"] DEVICE_CLASS_MAP: dict[Category | str, MediaPlayerDeviceClass] = { Category.NETWORK_AUDIO: MediaPlayerDeviceClass.SPEAKER, Category.SPEAKER: MediaPlayerDeviceClass.SPEAKER, Category.TELEVISION: MediaPlayerDeviceClass.TV, Category.RECEIVER: MediaPlayerDeviceClass.RECEIVER, } VALUE_TO_STATE = { "buffering": MediaPlayerState.BUFFERING, "paused": MediaPlayerState.PAUSED, "playing": MediaPlayerState.PLAYING, "stopped": MediaPlayerState.IDLE, "fast forwarding": MediaPlayerState.BUFFERING, "rewinding": MediaPlayerState.BUFFERING, } REPEAT_MODE_TO_HA = { "all": RepeatMode.ALL, "one": RepeatMode.ONE, "off": RepeatMode.OFF, } HA_REPEAT_MODE_TO_SMARTTHINGS = {v: k for k, v in REPEAT_MODE_TO_HA.items()} async def async_setup_entry( hass: HomeAssistant, entry: SmartThingsConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Add media players for a config entry.""" entry_data = entry.runtime_data async_add_entities( SmartThingsMediaPlayer(entry_data.client, device) for device in entry_data.devices.values() if all( capability in device.status[MAIN] for capability in MEDIA_PLAYER_CAPABILITIES ) ) class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity): """Define a SmartThings media player.""" _attr_name = None def __init__(self, client: SmartThings, device: FullDevice) -> None: """Initialize the media_player class.""" super().__init__( client, device, { Capability.AUDIO_MUTE, Capability.AUDIO_TRACK_DATA, Capability.AUDIO_VOLUME, Capability.MEDIA_INPUT_SOURCE, Capability.MEDIA_PLAYBACK, Capability.MEDIA_PLAYBACK_REPEAT, Capability.MEDIA_PLAYBACK_SHUFFLE, Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE, Capability.SWITCH, }, ) self._attr_supported_features = self._determine_features() self._attr_device_class = DEVICE_CLASS_MAP.get( device.device.components[MAIN].user_category or device.device.components[MAIN].manufacturer_category, ) def _determine_features(self) -> MediaPlayerEntityFeature: flags = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE ) if self.supports_capability(Capability.MEDIA_PLAYBACK): playback_commands = self.get_attribute_value( Capability.MEDIA_PLAYBACK, Attribute.SUPPORTED_PLAYBACK_COMMANDS ) if "play" in playback_commands: flags |= MediaPlayerEntityFeature.PLAY if "pause" in playback_commands: flags |= MediaPlayerEntityFeature.PAUSE if "stop" in playback_commands: flags |= MediaPlayerEntityFeature.STOP if "rewind" in playback_commands: flags |= MediaPlayerEntityFeature.PREVIOUS_TRACK if "fastForward" in playback_commands: flags |= MediaPlayerEntityFeature.NEXT_TRACK if self.supports_capability(Capability.SWITCH): flags |= ( MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF ) if self.supports_capability(Capability.MEDIA_INPUT_SOURCE): flags |= MediaPlayerEntityFeature.SELECT_SOURCE if self.supports_capability(Capability.MEDIA_PLAYBACK_SHUFFLE): flags |= MediaPlayerEntityFeature.SHUFFLE_SET if self.supports_capability(Capability.MEDIA_PLAYBACK_REPEAT): flags |= MediaPlayerEntityFeature.REPEAT_SET return flags async def async_turn_off(self, **kwargs: Any) -> None: """Turn the media player off.""" await self.execute_device_command( Capability.SWITCH, Command.OFF, ) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the media player on.""" await self.execute_device_command( Capability.SWITCH, Command.ON, ) async def async_mute_volume(self, mute: bool) -> None: """Mute volume.""" await self.execute_device_command( Capability.AUDIO_MUTE, Command.SET_MUTE, argument="muted" if mute else "unmuted", ) async def async_set_volume_level(self, volume: float) -> None: """Set volume level.""" await self.execute_device_command( Capability.AUDIO_VOLUME, Command.SET_VOLUME, argument=int(volume * 100), ) async def async_volume_up(self) -> None: """Increase volume.""" await self.execute_device_command( Capability.AUDIO_VOLUME, Command.VOLUME_UP, ) async def async_volume_down(self) -> None: """Decrease volume.""" await self.execute_device_command( Capability.AUDIO_VOLUME, Command.VOLUME_DOWN, ) async def async_media_play(self) -> None: """Play media.""" await self.execute_device_command( Capability.MEDIA_PLAYBACK, Command.PLAY, ) async def async_media_pause(self) -> None: """Pause media.""" await self.execute_device_command( Capability.MEDIA_PLAYBACK, Command.PAUSE, ) async def async_media_stop(self) -> None: """Stop media.""" await self.execute_device_command( Capability.MEDIA_PLAYBACK, Command.STOP, ) async def async_media_previous_track(self) -> None: """Previous track.""" await self.execute_device_command( Capability.MEDIA_PLAYBACK, Command.REWIND, ) async def async_media_next_track(self) -> None: """Next track.""" await self.execute_device_command( Capability.MEDIA_PLAYBACK, Command.FAST_FORWARD, ) async def async_select_source(self, source: str) -> None: """Select source.""" await self.execute_device_command( Capability.MEDIA_INPUT_SOURCE, Command.SET_INPUT_SOURCE, argument=source, ) async def async_set_shuffle(self, shuffle: bool) -> None: """Set shuffle mode.""" await self.execute_device_command( Capability.MEDIA_PLAYBACK_SHUFFLE, Command.SET_PLAYBACK_SHUFFLE, argument="enabled" if shuffle else "disabled", ) async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" await self.execute_device_command( Capability.MEDIA_PLAYBACK_REPEAT, Command.SET_PLAYBACK_REPEAT_MODE, argument=HA_REPEAT_MODE_TO_SMARTTHINGS[repeat], ) @property def media_title(self) -> str | None: """Title of current playing media.""" if ( not self.supports_capability(Capability.AUDIO_TRACK_DATA) or ( track_data := self.get_attribute_value( Capability.AUDIO_TRACK_DATA, Attribute.AUDIO_TRACK_DATA ) ) is None ): return None return track_data.get("title", None) @property def media_artist(self) -> str | None: """Artist of current playing media.""" if ( not self.supports_capability(Capability.AUDIO_TRACK_DATA) or ( track_data := self.get_attribute_value( Capability.AUDIO_TRACK_DATA, Attribute.AUDIO_TRACK_DATA ) ) is None ): return None return track_data.get("artist") @property def state(self) -> MediaPlayerState | None: """State of the media player.""" if self.supports_capability(Capability.SWITCH): if not self.supports_capability(Capability.MEDIA_PLAYBACK): if ( self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on" ): return MediaPlayerState.ON return MediaPlayerState.OFF if self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on": if ( self.source is not None and self.source in CONTROLLABLE_SOURCES and self.get_attribute_value( Capability.MEDIA_PLAYBACK, Attribute.PLAYBACK_STATUS ) in VALUE_TO_STATE ): return VALUE_TO_STATE[ self.get_attribute_value( Capability.MEDIA_PLAYBACK, Attribute.PLAYBACK_STATUS ) ] return MediaPlayerState.ON return MediaPlayerState.OFF return VALUE_TO_STATE[ self.get_attribute_value( Capability.MEDIA_PLAYBACK, Attribute.PLAYBACK_STATUS ) ] @property def is_volume_muted(self) -> bool: """Returns if the volume is muted.""" return ( self.get_attribute_value(Capability.AUDIO_MUTE, Attribute.MUTE) == "muted" ) @property def volume_level(self) -> float: """Volume level.""" return self.get_attribute_value(Capability.AUDIO_VOLUME, Attribute.VOLUME) / 100 @property def source(self) -> str | None: """Input source.""" if self.supports_capability(Capability.MEDIA_INPUT_SOURCE): return self.get_attribute_value( Capability.MEDIA_INPUT_SOURCE, Attribute.INPUT_SOURCE ) if self.supports_capability(Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE): return self.get_attribute_value( Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE, Attribute.INPUT_SOURCE ) return None @property def source_list(self) -> list[str] | None: """List of input sources.""" if self.supports_capability(Capability.MEDIA_INPUT_SOURCE): return self.get_attribute_value( Capability.MEDIA_INPUT_SOURCE, Attribute.SUPPORTED_INPUT_SOURCES ) if self.supports_capability(Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE): return self.get_attribute_value( Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE, Attribute.SUPPORTED_INPUT_SOURCES, ) return None @property def shuffle(self) -> bool | None: """Returns if shuffle mode is set.""" if self.supports_capability(Capability.MEDIA_PLAYBACK_SHUFFLE): return ( self.get_attribute_value( Capability.MEDIA_PLAYBACK_SHUFFLE, Attribute.PLAYBACK_SHUFFLE ) == "enabled" ) return None @property def repeat(self) -> RepeatMode | None: """Returns if repeat mode is set.""" if self.supports_capability(Capability.MEDIA_PLAYBACK_REPEAT): return REPEAT_MODE_TO_HA[ self.get_attribute_value( Capability.MEDIA_PLAYBACK_REPEAT, Attribute.PLAYBACK_REPEAT_MODE ) ] return None