mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Add a Media Player Component for Channels (#12937)
* add Channels media player * add Channels' services * style 💄 * more 💄 * make up your mind robot * 💄 💄 💄 * dump client and pull it in via a package * ChannelsApp -> ChannelsPlayer * load the lib * add pychannels in requirements * not using requests anymore * extra line 💄 * move this here * move this up * 🔥 * 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 * 💄💄💄 * 💄 * docs * like this? ¯\(°_o)/¯
This commit is contained in:
parent
4aed41cbe8
commit
d119610cf1
303
homeassistant/components/media_player/channels.py
Normal file
303
homeassistant/components/media_player/channels.py
Normal file
@ -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)
|
@ -242,6 +242,30 @@ sonos_set_option:
|
|||||||
description: Enable Speech Enhancement mode
|
description: Enable Speech Enhancement mode
|
||||||
example: 'true'
|
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:
|
soundtouch_play_everywhere:
|
||||||
description: Play on all Bose Soundtouch devices.
|
description: Play on all Bose Soundtouch devices.
|
||||||
fields:
|
fields:
|
||||||
@ -367,7 +391,7 @@ bluesound_clear_sleep_timer:
|
|||||||
|
|
||||||
songpal_set_sound_setting:
|
songpal_set_sound_setting:
|
||||||
description: Change sound setting.
|
description: Change sound setting.
|
||||||
|
|
||||||
fields:
|
fields:
|
||||||
entity_id:
|
entity_id:
|
||||||
description: Target device.
|
description: Target device.
|
||||||
|
@ -675,6 +675,9 @@ pybbox==0.0.5-alpha
|
|||||||
# homeassistant.components.device_tracker.bluetooth_tracker
|
# homeassistant.components.device_tracker.bluetooth_tracker
|
||||||
# pybluez==0.22
|
# pybluez==0.22
|
||||||
|
|
||||||
|
# homeassistant.components.media_player.channels
|
||||||
|
pychannels==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.media_player.cast
|
# homeassistant.components.media_player.cast
|
||||||
pychromecast==2.0.0
|
pychromecast==2.0.0
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user