From d119610cf1b4289094e2aaf0df15b492c4013f0f Mon Sep 17 00:00:00 2001 From: Jon Maddox <jon@jonmaddox.com> Date: Wed, 7 Mar 2018 03:33:13 -0500 Subject: [PATCH] Add a Media Player Component for Channels (#12937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add Channels media player * add Channels' services * style :lipstick: * more :lipstick: * make up your mind robot * :lipstick: :lipstick: :lipstick: * dump client and pull it in via a package * ChannelsApp -> ChannelsPlayer * load the lib * add pychannels in requirements * not using requests anymore * extra line :lipstick: * move this here * move this up * :fire: * use constants for these * add a platform schema * force update here * get defaults to None * break out after finding it * use None for state if offline or errored * pull in CONF_NAME * fix syntax * update requirements_all.txt * :lipstick::lipstick::lipstick: * :lipstick: * docs * like this? ¯\(°_o)/¯ --- .../components/media_player/channels.py | 303 ++++++++++++++++++ .../components/media_player/services.yaml | 26 +- requirements_all.txt | 3 + 3 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/media_player/channels.py diff --git a/homeassistant/components/media_player/channels.py b/homeassistant/components/media_player/channels.py new file mode 100644 index 00000000000..eda47237b44 --- /dev/null +++ b/homeassistant/components/media_player/channels.py @@ -0,0 +1,303 @@ +""" +Support for interfacing with an instance of Channels (https://getchannels.com). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.channels/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.media_player import ( + MEDIA_TYPE_CHANNEL, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_EPISODE, + MEDIA_TYPE_VIDEO, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, + SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, DOMAIN, PLATFORM_SCHEMA, + MediaPlayerDevice) + +from homeassistant.const import ( + CONF_HOST, CONF_PORT, CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, + ATTR_ENTITY_ID) + +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DATA_CHANNELS = 'channels' +DEFAULT_NAME = 'Channels' +DEFAULT_PORT = 57000 + +FEATURE_SUPPORT = SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | \ + SUPPORT_VOLUME_MUTE | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_PLAY_MEDIA | SUPPORT_SELECT_SOURCE + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + +SERVICE_SEEK_FORWARD = 'channels_seek_forward' +SERVICE_SEEK_BACKWARD = 'channels_seek_backward' +SERVICE_SEEK_BY = 'channels_seek_by' + +# Service call validation schemas +ATTR_SECONDS = 'seconds' + +CHANNELS_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, +}) + +CHANNELS_SEEK_BY_SCHEMA = CHANNELS_SCHEMA.extend({ + vol.Required(ATTR_SECONDS): vol.Coerce(int), +}) + +REQUIREMENTS = ['pychannels==1.0.0'] + + +# pylint: disable=unused-argument, abstract-method +# pylint: disable=too-many-instance-attributes +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Channels platform.""" + device = ChannelsPlayer( + config.get('name', DEFAULT_NAME), + config.get(CONF_HOST), + config.get(CONF_PORT, DEFAULT_PORT) + ) + + if DATA_CHANNELS not in hass.data: + hass.data[DATA_CHANNELS] = [] + + add_devices([device], True) + hass.data[DATA_CHANNELS].append(device) + + def service_handler(service): + """Handler for services.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + + if entity_ids: + devices = [device for device in hass.data[DATA_CHANNELS] + if device.entity_id in entity_ids] + else: + devices = hass.data[DATA_CHANNELS] + + for device in devices: + if service.service == SERVICE_SEEK_FORWARD: + device.seek_forward() + elif service.service == SERVICE_SEEK_BACKWARD: + device.seek_backward() + elif service.service == SERVICE_SEEK_BY: + seconds = service.data.get('seconds') + device.seek_by(seconds) + + hass.services.register( + DOMAIN, SERVICE_SEEK_FORWARD, service_handler, + schema=CHANNELS_SCHEMA) + + hass.services.register( + DOMAIN, SERVICE_SEEK_BACKWARD, service_handler, + schema=CHANNELS_SCHEMA) + + hass.services.register( + DOMAIN, SERVICE_SEEK_BY, service_handler, + schema=CHANNELS_SEEK_BY_SCHEMA) + + +class ChannelsPlayer(MediaPlayerDevice): + """Representation of a Channels instance.""" + + # pylint: disable=too-many-public-methods + def __init__(self, name, host, port): + """Initialize the Channels app.""" + from pychannels import Channels + + self._name = name + self._host = host + self._port = port + + self.client = Channels(self._host, self._port) + + self.status = None + self.muted = None + + self.channel_number = None + self.channel_name = None + self.channel_image_url = None + + self.now_playing_title = None + self.now_playing_episode_title = None + self.now_playing_season_number = None + self.now_playing_episode_number = None + self.now_playing_summary = None + self.now_playing_image_url = None + + self.favorite_channels = [] + + def update_favorite_channels(self): + """Update the favorite channels from the client.""" + self.favorite_channels = self.client.favorite_channels() + + def update_state(self, state_hash): + """Update all the state properties with the passed in dictionary.""" + self.status = state_hash.get('status', "stopped") + self.muted = state_hash.get('muted', False) + + channel_hash = state_hash.get('channel') + np_hash = state_hash.get('now_playing') + + if channel_hash: + self.channel_number = channel_hash.get('channel_number') + self.channel_name = channel_hash.get('channel_name') + self.channel_image_url = channel_hash.get('channel_image_url') + else: + self.channel_number = None + self.channel_name = None + self.channel_image_url = None + + if np_hash: + self.now_playing_title = np_hash.get('title') + self.now_playing_episode_title = np_hash.get('episode_title') + self.now_playing_season_number = np_hash.get('season_number') + self.now_playing_episode_number = np_hash.get('episode_number') + self.now_playing_summary = np_hash.get('summary') + self.now_playing_image_url = np_hash.get('image_url') + else: + self.now_playing_title = None + self.now_playing_episode_title = None + self.now_playing_season_number = None + self.now_playing_episode_number = None + self.now_playing_summary = None + self.now_playing_image_url = None + + @property + def name(self): + """Return the name of the player.""" + return self._name + + @property + def state(self): + """Return the state of the player.""" + if self.status == 'stopped': + return STATE_IDLE + + if self.status == 'paused': + return STATE_PAUSED + + if self.status == 'playing': + return STATE_PLAYING + + return None + + def update(self): + """Retrieve latest state.""" + self.update_favorite_channels() + self.update_state(self.client.status()) + + @property + def source_list(self): + """List of favorite channels.""" + sources = [channel['name'] for channel in self.favorite_channels] + return sources + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self.muted + + @property + def media_content_id(self): + """Content ID of current playing channel.""" + return self.channel_number + + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_CHANNEL + + @property + def media_image_url(self): + """Image url of current playing media.""" + if self.now_playing_image_url: + return self.now_playing_image_url + elif self.channel_image_url: + return self.channel_image_url + + return 'https://getchannels.com/assets/img/icon-1024.png' + + @property + def media_title(self): + """Title of current playing media.""" + if self.state: + return self.now_playing_title + + return None + + @property + def supported_features(self): + """Flag of media commands that are supported.""" + return FEATURE_SUPPORT + + def mute_volume(self, mute): + """Mute (true) or unmute (false) player.""" + if mute != self.muted: + response = self.client.toggle_muted() + self.update_state(response) + + def media_stop(self): + """Send media_stop command to player.""" + self.status = "stopped" + response = self.client.stop() + self.update_state(response) + + def media_play(self): + """Send media_play command to player.""" + response = self.client.resume() + self.update_state(response) + + def media_pause(self): + """Send media_pause command to player.""" + response = self.client.pause() + self.update_state(response) + + def media_next_track(self): + """Seek ahead.""" + response = self.client.skip_forward() + self.update_state(response) + + def media_previous_track(self): + """Seek back.""" + response = self.client.skip_backward() + self.update_state(response) + + def select_source(self, source): + """Select a channel to tune to.""" + for channel in self.favorite_channels: + if channel["name"] == source: + response = self.client.play_channel(channel["number"]) + self.update_state(response) + break + + def play_media(self, media_type, media_id, **kwargs): + """Send the play_media command to the player.""" + if media_type == MEDIA_TYPE_CHANNEL: + response = self.client.play_channel(media_id) + self.update_state(response) + elif media_type in [MEDIA_TYPE_VIDEO, MEDIA_TYPE_EPISODE, + MEDIA_TYPE_TVSHOW]: + response = self.client.play_recording(media_id) + self.update_state(response) + + def seek_forward(self): + """Seek forward in the timeline.""" + response = self.client.seek_forward() + self.update_state(response) + + def seek_backward(self): + """Seek backward in the timeline.""" + response = self.client.seek_backward() + self.update_state(response) + + def seek_by(self, seconds): + """Seek backward in the timeline.""" + response = self.client.seek(seconds) + self.update_state(response) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 4d488a92300..beaea8a8ad0 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -242,6 +242,30 @@ sonos_set_option: description: Enable Speech Enhancement mode example: 'true' +channels_seek_forward: + description: Seek forward by a set number of seconds. + fields: + entity_id: + description: Name of entity for the instance of Channels to seek in. + example: 'media_player.family_room_channels' + +channels_seek_backward: + description: Seek backward by a set number of seconds. + fields: + entity_id: + description: Name of entity for the instance of Channels to seek in. + example: 'media_player.family_room_channels' + +channels_seek_by: + description: Seek by an inputted number of seconds. + fields: + entity_id: + description: Name of entity for the instance of Channels to seek in. + example: 'media_player.family_room_channels' + seconds: + description: Number of seconds to seek by. Negative numbers seek backwards. + example: 120 + soundtouch_play_everywhere: description: Play on all Bose Soundtouch devices. fields: @@ -367,7 +391,7 @@ bluesound_clear_sleep_timer: songpal_set_sound_setting: description: Change sound setting. - + fields: entity_id: description: Target device. diff --git a/requirements_all.txt b/requirements_all.txt index 8b7e6c8c16d..4eae1e2688f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -675,6 +675,9 @@ pybbox==0.0.5-alpha # homeassistant.components.device_tracker.bluetooth_tracker # pybluez==0.22 +# homeassistant.components.media_player.channels +pychannels==1.0.0 + # homeassistant.components.media_player.cast pychromecast==2.0.0 -- GitLab