2019-04-03 15:40:03 +00:00
|
|
|
"""Support for interfacing with an instance of getchannels.com."""
|
2018-03-07 08:33:13 +00:00
|
|
|
import logging
|
|
|
|
|
2019-10-18 23:04:10 +00:00
|
|
|
from pychannels import Channels
|
2018-03-07 08:33:13 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2019-10-18 23:04:10 +00:00
|
|
|
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
|
2019-02-08 22:18:18 +00:00
|
|
|
from homeassistant.components.media_player.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN,
|
|
|
|
MEDIA_TYPE_CHANNEL,
|
|
|
|
MEDIA_TYPE_EPISODE,
|
|
|
|
MEDIA_TYPE_MOVIE,
|
|
|
|
MEDIA_TYPE_TVSHOW,
|
|
|
|
SUPPORT_NEXT_TRACK,
|
|
|
|
SUPPORT_PAUSE,
|
|
|
|
SUPPORT_PLAY,
|
|
|
|
SUPPORT_PLAY_MEDIA,
|
|
|
|
SUPPORT_PREVIOUS_TRACK,
|
|
|
|
SUPPORT_SELECT_SOURCE,
|
|
|
|
SUPPORT_STOP,
|
|
|
|
SUPPORT_VOLUME_MUTE,
|
|
|
|
)
|
2018-03-07 08:33:13 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_ENTITY_ID,
|
|
|
|
CONF_HOST,
|
|
|
|
CONF_NAME,
|
|
|
|
CONF_PORT,
|
|
|
|
STATE_IDLE,
|
|
|
|
STATE_PAUSED,
|
|
|
|
STATE_PLAYING,
|
|
|
|
)
|
2018-03-07 08:33:13 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DATA_CHANNELS = "channels"
|
|
|
|
DEFAULT_NAME = "Channels"
|
2018-03-07 08:33:13 +00:00
|
|
|
DEFAULT_PORT = 57000
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
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"
|
2018-03-07 08:33:13 +00:00
|
|
|
|
|
|
|
# Service call validation schemas
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_SECONDS = "seconds"
|
2018-03-07 08:33:13 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
CHANNELS_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_id})
|
2018-03-07 08:33:13 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
CHANNELS_SEEK_BY_SCHEMA = CHANNELS_SCHEMA.extend(
|
|
|
|
{vol.Required(ATTR_SECONDS): vol.Coerce(int)}
|
|
|
|
)
|
2018-03-07 08:33:13 +00:00
|
|
|
|
|
|
|
|
2018-08-24 14:37:30 +00:00
|
|
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
2018-08-19 20:29:08 +00:00
|
|
|
"""Set up the Channels platform."""
|
2018-03-07 08:33:13 +00:00
|
|
|
device = ChannelsPlayer(
|
2019-07-31 19:25:30 +00:00
|
|
|
config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT)
|
|
|
|
)
|
2018-03-07 08:33:13 +00:00
|
|
|
|
|
|
|
if DATA_CHANNELS not in hass.data:
|
|
|
|
hass.data[DATA_CHANNELS] = []
|
|
|
|
|
2018-08-24 14:37:30 +00:00
|
|
|
add_entities([device], True)
|
2018-03-07 08:33:13 +00:00
|
|
|
hass.data[DATA_CHANNELS].append(device)
|
|
|
|
|
|
|
|
def service_handler(service):
|
2018-08-24 08:28:43 +00:00
|
|
|
"""Handle service."""
|
2018-03-13 21:14:02 +00:00
|
|
|
entity_id = service.data.get(ATTR_ENTITY_ID)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
device = next(
|
|
|
|
(
|
|
|
|
device
|
|
|
|
for device in hass.data[DATA_CHANNELS]
|
|
|
|
if device.entity_id == entity_id
|
|
|
|
),
|
|
|
|
None,
|
|
|
|
)
|
2018-03-13 21:14:02 +00:00
|
|
|
|
|
|
|
if device is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.warning("Unable to find Channels with entity_id: %s", entity_id)
|
2018-03-13 21:14:02 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
if service.service == SERVICE_SEEK_FORWARD:
|
|
|
|
device.seek_forward()
|
|
|
|
elif service.service == SERVICE_SEEK_BACKWARD:
|
|
|
|
device.seek_backward()
|
|
|
|
elif service.service == SERVICE_SEEK_BY:
|
2019-07-31 19:25:30 +00:00
|
|
|
seconds = service.data.get("seconds")
|
2018-03-13 21:14:02 +00:00
|
|
|
device.seek_by(seconds)
|
2018-03-07 08:33:13 +00:00
|
|
|
|
|
|
|
hass.services.register(
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN, SERVICE_SEEK_FORWARD, service_handler, schema=CHANNELS_SCHEMA
|
|
|
|
)
|
2018-03-07 08:33:13 +00:00
|
|
|
|
|
|
|
hass.services.register(
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN, SERVICE_SEEK_BACKWARD, service_handler, schema=CHANNELS_SCHEMA
|
|
|
|
)
|
2018-03-07 08:33:13 +00:00
|
|
|
|
|
|
|
hass.services.register(
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN, SERVICE_SEEK_BY, service_handler, schema=CHANNELS_SEEK_BY_SCHEMA
|
|
|
|
)
|
2018-03-07 08:33:13 +00:00
|
|
|
|
|
|
|
|
|
|
|
class ChannelsPlayer(MediaPlayerDevice):
|
|
|
|
"""Representation of a Channels instance."""
|
|
|
|
|
|
|
|
def __init__(self, name, host, port):
|
|
|
|
"""Initialize the Channels app."""
|
|
|
|
|
|
|
|
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."""
|
2019-07-31 19:25:30 +00:00
|
|
|
self.status = state_hash.get("status", "stopped")
|
|
|
|
self.muted = state_hash.get("muted", False)
|
2018-03-07 08:33:13 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
channel_hash = state_hash.get("channel")
|
|
|
|
np_hash = state_hash.get("now_playing")
|
2018-03-07 08:33:13 +00:00
|
|
|
|
|
|
|
if channel_hash:
|
2019-07-31 19:25:30 +00:00
|
|
|
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")
|
2018-03-07 08:33:13 +00:00
|
|
|
else:
|
|
|
|
self.channel_number = None
|
|
|
|
self.channel_name = None
|
|
|
|
self.channel_image_url = None
|
|
|
|
|
|
|
|
if np_hash:
|
2019-07-31 19:25:30 +00:00
|
|
|
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")
|
2018-03-07 08:33:13 +00:00
|
|
|
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."""
|
2019-07-31 19:25:30 +00:00
|
|
|
if self.status == "stopped":
|
2018-03-07 08:33:13 +00:00
|
|
|
return STATE_IDLE
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if self.status == "paused":
|
2018-03-07 08:33:13 +00:00
|
|
|
return STATE_PAUSED
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if self.status == "playing":
|
2018-03-07 08:33:13 +00:00
|
|
|
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."""
|
2019-07-31 19:25:30 +00:00
|
|
|
sources = [channel["name"] for channel in self.favorite_channels]
|
2018-03-07 08:33:13 +00:00
|
|
|
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
|
2018-07-23 08:16:05 +00:00
|
|
|
if self.channel_image_url:
|
2018-03-07 08:33:13 +00:00
|
|
|
return self.channel_image_url
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return "https://getchannels.com/assets/img/icon-1024.png"
|
2018-03-07 08:33:13 +00:00
|
|
|
|
|
|
|
@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)
|
2019-07-31 19:25:30 +00:00
|
|
|
elif media_type in [MEDIA_TYPE_MOVIE, MEDIA_TYPE_EPISODE, MEDIA_TYPE_TVSHOW]:
|
2018-03-07 08:33:13 +00:00
|
|
|
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)
|