"""Media Player component to integrate TVs exposing the Joint Space API.""" from datetime import timedelta import logging import voluptuous as vol from homeassistant.components.media_player import ( MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MEDIA_TYPE_CHANNEL, SUPPORT_PLAY_MEDIA) from homeassistant.const import ( CONF_API_VERSION, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import call_later, track_time_interval from homeassistant.helpers.script import Script _LOGGER = logging.getLogger(__name__) SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_SELECT_SOURCE | SUPPORT_NEXT_TRACK | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY_MEDIA CONF_ON_ACTION = 'turn_on_action' DEFAULT_NAME = "Philips TV" DEFAULT_API_VERSION = '1' DEFAULT_SCAN_INTERVAL = 30 DELAY_ACTION_DEFAULT = 2.0 DELAY_ACTION_ON = 10.0 PREFIX_SEPARATOR = ': ' PREFIX_SOURCE = 'Input' PREFIX_CHANNEL = 'Channel' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): cv.string, vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, }) def _inverted(data): return {v: k for k, v in data.items()} def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Philips TV platform.""" import haphilipsjs name = config.get(CONF_NAME) host = config.get(CONF_HOST) api_version = config.get(CONF_API_VERSION) turn_on_action = config.get(CONF_ON_ACTION) tvapi = haphilipsjs.PhilipsTV(host, api_version) on_script = Script(hass, turn_on_action) if turn_on_action else None add_entities([PhilipsTV(tvapi, name, on_script)]) class PhilipsTV(MediaPlayerDevice): """Representation of a Philips TV exposing the JointSpace API.""" def __init__(self, tv, name, on_script): """Initialize the Philips TV.""" self._tv = tv self._name = name self._sources = {} self._channels = {} self._on_script = on_script self._supports = SUPPORT_PHILIPS_JS if self._on_script: self._supports |= SUPPORT_TURN_ON self._update_task = None def _update_soon(self, delay): """Reschedule update task.""" if self._update_task: self._update_task() self._update_task = None self.schedule_update_ha_state( force_refresh=False) def update_forced(event_time): self.schedule_update_ha_state(force_refresh=True) def update_and_restart(event_time): update_forced(event_time) self._update_task = track_time_interval( self.hass, update_forced, timedelta(seconds=DEFAULT_SCAN_INTERVAL)) call_later(self.hass, delay, update_and_restart) async def async_added_to_hass(self): """Start running updates once we are added to hass.""" await self.hass.async_add_executor_job( self._update_soon, 0) @property def name(self): """Return the device name.""" return self._name @property def should_poll(self): """Device should be polled.""" return False @property def supported_features(self): """Flag media player features that are supported.""" return self._supports @property def state(self): """Get the device state. An exception means OFF state.""" if self._tv.on: return STATE_ON return STATE_OFF @property def source(self): """Return the current input source.""" if self.media_content_type == MEDIA_TYPE_CHANNEL: name = self._channels.get(self._tv.channel_id) prefix = PREFIX_CHANNEL else: name = self._sources.get(self._tv.source_id) prefix = PREFIX_SOURCE if name is None: return None return prefix + PREFIX_SEPARATOR + name @property def source_list(self): """List of available input sources.""" complete = [] for source in self._sources.values(): complete.append(PREFIX_SOURCE + PREFIX_SEPARATOR + source) for channel in self._channels.values(): complete.append(PREFIX_CHANNEL + PREFIX_SEPARATOR + channel) return complete def select_source(self, source): """Set the input source.""" data = source.split(PREFIX_SEPARATOR, 1) if data[0] == PREFIX_SOURCE: source_id = _inverted(self._sources).get(data[1]) if source_id: self._tv.setSource(source_id) elif data[0] == PREFIX_CHANNEL: channel_id = _inverted(self._channels).get(data[1]) if channel_id: self._tv.setChannel(channel_id) self._update_soon(DELAY_ACTION_DEFAULT) @property def volume_level(self): """Volume level of the media player (0..1).""" return self._tv.volume @property def is_volume_muted(self): """Boolean if volume is currently muted.""" return self._tv.muted def turn_on(self): """Turn on the device.""" if self._on_script: self._on_script.run() self._update_soon(DELAY_ACTION_ON) def turn_off(self): """Turn off the device.""" self._tv.sendKey('Standby') self._tv.on = False self._update_soon(DELAY_ACTION_DEFAULT) def volume_up(self): """Send volume up command.""" self._tv.sendKey('VolumeUp') self._update_soon(DELAY_ACTION_DEFAULT) def volume_down(self): """Send volume down command.""" self._tv.sendKey('VolumeDown') self._update_soon(DELAY_ACTION_DEFAULT) def mute_volume(self, mute): """Send mute command.""" self._tv.setVolume(self._tv.volume, mute) self._update_soon(DELAY_ACTION_DEFAULT) def set_volume_level(self, volume): """Set volume level, range 0..1.""" self._tv.setVolume(volume, self._tv.muted) self._update_soon(DELAY_ACTION_DEFAULT) def media_previous_track(self): """Send rewind command.""" self._tv.sendKey('Previous') self._update_soon(DELAY_ACTION_DEFAULT) def media_next_track(self): """Send fast forward command.""" self._tv.sendKey('Next') self._update_soon(DELAY_ACTION_DEFAULT) @property def media_channel(self): """Get current channel if it's a channel.""" if self.media_content_type == MEDIA_TYPE_CHANNEL: return self._channels.get(self._tv.channel_id) return None @property def media_title(self): """Title of current playing media.""" if self.media_content_type == MEDIA_TYPE_CHANNEL: return self._channels.get(self._tv.channel_id) return self._sources.get(self._tv.source_id) @property def media_content_type(self): """Return content type of playing media.""" if (self._tv.source_id == 'tv' or self._tv.source_id == '11'): return MEDIA_TYPE_CHANNEL if (self._tv.source_id is None and self._tv.channels): return MEDIA_TYPE_CHANNEL return None @property def media_content_id(self): """Content type of current playing media.""" if self.media_content_type == MEDIA_TYPE_CHANNEL: return self._channels.get(self._tv.channel_id) return None @property def device_state_attributes(self): """Return the state attributes.""" return { 'channel_list': list(self._channels.values()) } def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" _LOGGER.debug( "Call play media type <%s>, Id <%s>", media_type, media_id) if media_type == MEDIA_TYPE_CHANNEL: channel_id = _inverted(self._channels).get(media_id) if channel_id: self._tv.setChannel(channel_id) self._update_soon(DELAY_ACTION_DEFAULT) else: _LOGGER.error("Unable to find channel <%s>", media_id) else: _LOGGER.error("Unsupported media type <%s>", media_type) def update(self): """Get the latest data and update device state.""" self._tv.update() self._tv.getChannelId() self._tv.getChannels() self._sources = { srcid: source['name'] or "Source {}".format(srcid) for srcid, source in (self._tv.sources or {}).items() } self._channels = { chid: channel['name'] for chid, channel in (self._tv.channels or {}).items() }