""" Combination of multiple media players into one for a universal controller. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.universal/ """ import logging # pylint: disable=import-error from copy import copy from homeassistant.components.media_player import ( ATTR_APP_ID, ATTR_APP_NAME, ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ARTIST, ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, ATTR_MEDIA_EPISODE, ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_SUPPORTED_MEDIA_COMMANDS, DOMAIN, SERVICE_PLAY_MEDIA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, ATTR_INPUT_SOURCE, SERVICE_SELECT_SOURCE, SERVICE_CLEAR_PLAYLIST, MediaPlayerDevice) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, CONF_NAME, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_IDLE, STATE_OFF, STATE_ON, SERVICE_MEDIA_STOP) from homeassistant.helpers.event import track_state_change from homeassistant.helpers.service import call_from_config ATTR_ACTIVE_CHILD = 'active_child' CONF_ATTRS = 'attributes' CONF_CHILDREN = 'children' CONF_COMMANDS = 'commands' CONF_PLATFORM = 'platform' CONF_SERVICE = 'service' CONF_SERVICE_DATA = 'service_data' CONF_STATE = 'state' OFF_STATES = [STATE_IDLE, STATE_OFF] REQUIREMENTS = [] _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the universal media players.""" if not validate_config(config): return player = UniversalMediaPlayer(hass, config[CONF_NAME], config[CONF_CHILDREN], config[CONF_COMMANDS], config[CONF_ATTRS]) add_devices([player]) def validate_config(config): """Validate universal media player configuration.""" del config[CONF_PLATFORM] # Validate name if CONF_NAME not in config: _LOGGER.error('Universal Media Player configuration requires name') return False validate_children(config) validate_commands(config) validate_attributes(config) del_keys = [] for key in config: if key not in [CONF_NAME, CONF_CHILDREN, CONF_COMMANDS, CONF_ATTRS]: _LOGGER.warning( 'Universal Media Player (%s) unrecognized parameter %s', config[CONF_NAME], key) del_keys.append(key) for key in del_keys: del config[key] return True def validate_children(config): """Validate children.""" if CONF_CHILDREN not in config: _LOGGER.info( 'No children under Universal Media Player (%s)', config[CONF_NAME]) config[CONF_CHILDREN] = [] elif not isinstance(config[CONF_CHILDREN], list): _LOGGER.warning( 'Universal Media Player (%s) children not list in config. ' 'They will be ignored.', config[CONF_NAME]) config[CONF_CHILDREN] = [] def validate_commands(config): """Validate commands.""" if CONF_COMMANDS not in config: config[CONF_COMMANDS] = {} elif not isinstance(config[CONF_COMMANDS], dict): _LOGGER.warning( 'Universal Media Player (%s) specified commands not dict in ' 'config. They will be ignored.', config[CONF_NAME]) config[CONF_COMMANDS] = {} def validate_attributes(config): """Validate attributes.""" if CONF_ATTRS not in config: config[CONF_ATTRS] = {} elif not isinstance(config[CONF_ATTRS], dict): _LOGGER.warning( 'Universal Media Player (%s) specified attributes ' 'not dict in config. They will be ignored.', config[CONF_NAME]) config[CONF_ATTRS] = {} for key, val in config[CONF_ATTRS].items(): attr = val.split('|', 1) if len(attr) == 1: attr.append(None) config[CONF_ATTRS][key] = attr class UniversalMediaPlayer(MediaPlayerDevice): """Representation of an universal media player.""" def __init__(self, hass, name, children, commands, attributes): """Initialize the Universal media device.""" self.hass = hass self._name = name self._children = children self._cmds = commands self._attrs = attributes self._child_state = None def on_dependency_update(*_): """Update ha state when dependencies update.""" self.update_ha_state(True) depend = copy(children) for entity in attributes.values(): depend.append(entity[0]) track_state_change(hass, depend, on_dependency_update) def _entity_lkp(self, entity_id, state_attr=None): """Look up an entity state.""" state_obj = self.hass.states.get(entity_id) if state_obj is None: return if state_attr: return state_obj.attributes.get(state_attr) return state_obj.state def _override_or_child_attr(self, attr_name): """Return either the override or the active child for attr_name.""" if attr_name in self._attrs: return self._entity_lkp(self._attrs[attr_name][0], self._attrs[attr_name][1]) return self._child_attr(attr_name) def _child_attr(self, attr_name): """Return the active child's attributes.""" active_child = self._child_state return active_child.attributes.get(attr_name) if active_child else None def _call_service(self, service_name, service_data=None, allow_override=False): """Call either a specified or active child's service.""" if allow_override and service_name in self._cmds: call_from_config( self.hass, self._cmds[service_name], blocking=True) return if service_data is None: service_data = {} active_child = self._child_state service_data[ATTR_ENTITY_ID] = active_child.entity_id self.hass.services.call(DOMAIN, service_name, service_data, blocking=True) @property def should_poll(self): """No polling needed.""" return False @property def master_state(self): """Return the master state for entity or None.""" if CONF_STATE in self._attrs: master_state = self._entity_lkp(self._attrs[CONF_STATE][0], self._attrs[CONF_STATE][1]) return master_state if master_state else STATE_OFF else: return None @property def name(self): """Return the name of universal player.""" return self._name @property def state(self): """Current state of media player. Off if master state is off else Status of first active child else master state or off """ master_state = self.master_state # avoid multiple lookups if master_state == STATE_OFF: return STATE_OFF active_child = self._child_state if active_child: return active_child.state return master_state if master_state else STATE_OFF @property def volume_level(self): """Volume level of entity specified in attributes or active child.""" return self._child_attr(ATTR_MEDIA_VOLUME_LEVEL) @property def is_volume_muted(self): """Boolean if volume is muted.""" return self._override_or_child_attr(ATTR_MEDIA_VOLUME_MUTED) \ in [True, STATE_ON] @property def media_content_id(self): """Content ID of current playing media.""" return self._child_attr(ATTR_MEDIA_CONTENT_ID) @property def media_content_type(self): """Content type of current playing media.""" return self._child_attr(ATTR_MEDIA_CONTENT_TYPE) @property def media_duration(self): """Duration of current playing media in seconds.""" return self._child_attr(ATTR_MEDIA_DURATION) @property def media_image_url(self): """Image url of current playing media.""" return self._child_attr(ATTR_ENTITY_PICTURE) @property def media_title(self): """Title of current playing media.""" return self._child_attr(ATTR_MEDIA_TITLE) @property def media_artist(self): """Artist of current playing media (Music track only).""" return self._child_attr(ATTR_MEDIA_ARTIST) @property def media_album_name(self): """Album name of current playing media (Music track only).""" return self._child_attr(ATTR_MEDIA_ALBUM_NAME) @property def media_album_artist(self): """Album artist of current playing media (Music track only).""" return self._child_attr(ATTR_MEDIA_ALBUM_ARTIST) @property def media_track(self): """Track number of current playing media (Music track only).""" return self._child_attr(ATTR_MEDIA_TRACK) @property def media_series_title(self): """The title of the series of current playing media (TV Show only).""" return self._child_attr(ATTR_MEDIA_SERIES_TITLE) @property def media_season(self): """Season of current playing media (TV Show only).""" return self._child_attr(ATTR_MEDIA_SEASON) @property def media_episode(self): """Episode of current playing media (TV Show only).""" return self._child_attr(ATTR_MEDIA_EPISODE) @property def media_channel(self): """Channel currently playing.""" return self._child_attr(ATTR_MEDIA_CHANNEL) @property def media_playlist(self): """Title of Playlist currently playing.""" return self._child_attr(ATTR_MEDIA_PLAYLIST) @property def app_id(self): """ID of the current running app.""" return self._child_attr(ATTR_APP_ID) @property def app_name(self): """Name of the current running app.""" return self._child_attr(ATTR_APP_NAME) @property def current_source(self): """"Return the current input source of the device.""" return self._child_attr(ATTR_INPUT_SOURCE) @property def supported_media_commands(self): """Flag media commands that are supported.""" flags = self._child_attr(ATTR_SUPPORTED_MEDIA_COMMANDS) or 0 if SERVICE_TURN_ON in self._cmds: flags |= SUPPORT_TURN_ON if SERVICE_TURN_OFF in self._cmds: flags |= SUPPORT_TURN_OFF if any([cmd in self._cmds for cmd in [SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN]]): flags |= SUPPORT_VOLUME_STEP flags &= ~SUPPORT_VOLUME_SET if SERVICE_VOLUME_MUTE in self._cmds and \ ATTR_MEDIA_VOLUME_MUTED in self._attrs: flags |= SUPPORT_VOLUME_MUTE if SERVICE_SELECT_SOURCE in self._cmds: flags |= SUPPORT_SELECT_SOURCE if SERVICE_CLEAR_PLAYLIST in self._cmds: flags |= SUPPORT_CLEAR_PLAYLIST return flags @property def device_state_attributes(self): """Return device specific state attributes.""" active_child = self._child_state return {ATTR_ACTIVE_CHILD: active_child.entity_id} \ if active_child else {} def turn_on(self): """Turn the media player on.""" self._call_service(SERVICE_TURN_ON, allow_override=True) def turn_off(self): """Turn the media player off.""" self._call_service(SERVICE_TURN_OFF, allow_override=True) def mute_volume(self, is_volume_muted): """Mute the volume.""" data = {ATTR_MEDIA_VOLUME_MUTED: is_volume_muted} self._call_service(SERVICE_VOLUME_MUTE, data, allow_override=True) def set_volume_level(self, volume_level): """Set volume level, range 0..1.""" data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level} self._call_service(SERVICE_VOLUME_SET, data) def media_play(self): """Send play commmand.""" self._call_service(SERVICE_MEDIA_PLAY) def media_pause(self): """Send pause command.""" self._call_service(SERVICE_MEDIA_PAUSE) def media_stop(self): """Send stop command.""" self._call_service(SERVICE_MEDIA_STOP) def media_previous_track(self): """Send previous track command.""" self._call_service(SERVICE_MEDIA_PREVIOUS_TRACK) def media_next_track(self): """Send next track command.""" self._call_service(SERVICE_MEDIA_NEXT_TRACK) def media_seek(self, position): """Send seek command.""" data = {ATTR_MEDIA_SEEK_POSITION: position} self._call_service(SERVICE_MEDIA_SEEK, data) def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" data = {ATTR_MEDIA_CONTENT_TYPE: media_type, ATTR_MEDIA_CONTENT_ID: media_id} self._call_service(SERVICE_PLAY_MEDIA, data) def volume_up(self): """Turn volume up for media player.""" self._call_service(SERVICE_VOLUME_UP, allow_override=True) def volume_down(self): """Turn volume down for media player.""" self._call_service(SERVICE_VOLUME_DOWN, allow_override=True) def media_play_pause(self): """Play or pause the media player.""" self._call_service(SERVICE_MEDIA_PLAY_PAUSE) def select_source(self, source): """Set the input source.""" data = {ATTR_INPUT_SOURCE: source} self._call_service(SERVICE_SELECT_SOURCE, data) def clear_playlist(self): """Clear players playlist.""" self._call_service(SERVICE_CLEAR_PLAYLIST) def update(self): """Update state in HA.""" for child_name in self._children: child_state = self.hass.states.get(child_name) if child_state and child_state.state not in OFF_STATES: self._child_state = child_state return self._child_state = None