Add Roku hub and remote (#17548)

* add roku remote component

* remove name config (for now)

* update coveragerc and requirements_all

* fix linting errors

* remove extra requirements entry

* fix flake8 errors

* remove some references to apple tv

* remove redundant REQUIREMENTS

* Update requirements_all.txt

* Pass hass_config to load_platform

* don't expose registry constant

* remove unnecessary registry list

* use await instead of add_job

* use ensure_list

* fix code style

* some review fixes

* code style fixes

* stop using async

* use add with update

* fix whitespace

* remove I/O from init loop

* move import
This commit is contained in:
Spencer Oberstadt 2019-01-14 02:44:30 -05:00 committed by Martin Hjelmare
parent 7f3871028d
commit 7db28d3d91
6 changed files with 203 additions and 59 deletions

View File

@ -308,6 +308,9 @@ omit =
homeassistant/components/rfxtrx.py homeassistant/components/rfxtrx.py
homeassistant/components/*/rfxtrx.py homeassistant/components/*/rfxtrx.py
homeassistant/components/roku.py
homeassistant/components/*/roku.py
homeassistant/components/rpi_gpio.py homeassistant/components/rpi_gpio.py
homeassistant/components/*/rpi_gpio.py homeassistant/components/*/rpi_gpio.py
@ -642,7 +645,6 @@ omit =
homeassistant/components/media_player/pioneer.py homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/pjlink.py homeassistant/components/media_player/pjlink.py
homeassistant/components/media_player/plex.py homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
homeassistant/components/media_player/russound_rio.py homeassistant/components/media_player/russound_rio.py
homeassistant/components/media_player/russound_rnet.py homeassistant/components/media_player/russound_rnet.py
homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/snapcast.py

View File

@ -47,6 +47,7 @@ SERVICE_OCTOPRINT = 'octoprint'
SERVICE_FREEBOX = 'freebox' SERVICE_FREEBOX = 'freebox'
SERVICE_IGD = 'igd' SERVICE_IGD = 'igd'
SERVICE_DLNA_DMR = 'dlna_dmr' SERVICE_DLNA_DMR = 'dlna_dmr'
SERVICE_ROKU = 'roku'
CONFIG_ENTRY_HANDLERS = { CONFIG_ENTRY_HANDLERS = {
SERVICE_DAIKIN: 'daikin', SERVICE_DAIKIN: 'daikin',
@ -67,6 +68,7 @@ SERVICE_HANDLERS = {
SERVICE_HASSIO: ('hassio', None), SERVICE_HASSIO: ('hassio', None),
SERVICE_AXIS: ('axis', None), SERVICE_AXIS: ('axis', None),
SERVICE_APPLE_TV: ('apple_tv', None), SERVICE_APPLE_TV: ('apple_tv', None),
SERVICE_ROKU: ('roku', None),
SERVICE_WINK: ('wink', None), SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_SABNZBD: ('sabnzbd', None), SERVICE_SABNZBD: ('sabnzbd', None),
@ -76,7 +78,6 @@ SERVICE_HANDLERS = {
SERVICE_FREEBOX: ('freebox', None), SERVICE_FREEBOX: ('freebox', None),
'panasonic_viera': ('media_player', 'panasonic_viera'), 'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'), 'plex_mediaserver': ('media_player', 'plex'),
'roku': ('media_player', 'roku'),
'yamaha': ('media_player', 'yamaha'), 'yamaha': ('media_player', 'yamaha'),
'logitech_mediaserver': ('media_player', 'squeezebox'), 'logitech_mediaserver': ('media_player', 'squeezebox'),
'directv': ('media_player', 'directv'), 'directv': ('media_player', 'directv'),

View File

@ -1,79 +1,38 @@
""" """
Support for the roku media player. Support for the Roku media player.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.roku/ https://home-assistant.io/components/media_player.roku/
""" """
import logging import logging
import requests.exceptions
import voluptuous as vol
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MEDIA_TYPE_MOVIE, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PLAY, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PLAY,
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, STATE_HOME, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN) CONF_HOST, STATE_HOME, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-roku==3.1.5'] DEPENDENCIES = ['roku']
KNOWN_HOSTS = []
DEFAULT_PORT = 8060 DEFAULT_PORT = 8060
NOTIFICATION_ID = 'roku_notification'
NOTIFICATION_TITLE = 'Roku Media Player Setup'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_ROKU = SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\ SUPPORT_ROKU = SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\
SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY SUPPORT_SELECT_SOURCE | SUPPORT_PLAY
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): cv.string,
})
async def async_setup_platform(
def setup_platform(hass, config, add_entities, discovery_info=None): hass, config, async_add_entities, discovery_info=None):
"""Set up the Roku platform.""" """Set up the Roku platform."""
hosts = [] if not discovery_info:
return
if discovery_info: host = discovery_info[CONF_HOST]
host = discovery_info.get('host') async_add_entities([RokuDevice(host)], True)
if host in KNOWN_HOSTS:
return
_LOGGER.debug("Discovered Roku: %s", host)
hosts.append(discovery_info.get('host'))
elif CONF_HOST in config:
hosts.append(config.get(CONF_HOST))
rokus = []
for host in hosts:
new_roku = RokuDevice(host)
try:
if new_roku.name is not None:
rokus.append(RokuDevice(host))
KNOWN_HOSTS.append(host)
else:
_LOGGER.error("Unable to initialize roku at %s", host)
except AttributeError:
_LOGGER.error("Unable to initialize roku at %s", host)
hass.components.persistent_notification.create(
'Error: Unable to initialize roku at {}<br />'
'Check its network connection or consider '
'using auto discovery.<br />'
'You will need to restart hass after fixing.'
''.format(config.get(CONF_HOST)),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
add_entities(rokus)
class RokuDevice(MediaPlayerDevice): class RokuDevice(MediaPlayerDevice):
@ -89,12 +48,8 @@ class RokuDevice(MediaPlayerDevice):
self.current_app = None self.current_app = None
self._device_info = {} self._device_info = {}
self.update()
def update(self): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
import requests.exceptions
try: try:
self._device_info = self.roku.device_info self._device_info = self.roku.device_info
self.ip_address = self.roku.host self.ip_address = self.roku.host
@ -106,7 +61,6 @@ class RokuDevice(MediaPlayerDevice):
self.current_app = None self.current_app = None
except (requests.exceptions.ConnectionError, except (requests.exceptions.ConnectionError,
requests.exceptions.ReadTimeout): requests.exceptions.ReadTimeout):
pass pass
def get_source_list(self): def get_source_list(self):

View File

@ -0,0 +1,72 @@
"""
Support for the Roku remote.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/remote.roku/
"""
import requests.exceptions
from homeassistant.components import remote
from homeassistant.const import (CONF_HOST)
DEPENDENCIES = ['roku']
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Roku remote platform."""
if not discovery_info:
return
host = discovery_info[CONF_HOST]
async_add_entities([RokuRemote(host)], True)
class RokuRemote(remote.RemoteDevice):
"""Device that sends commands to an Roku."""
def __init__(self, host):
"""Initialize the Roku device."""
from roku import Roku
self.roku = Roku(host)
self._device_info = {}
def update(self):
"""Retrieve latest state."""
try:
self._device_info = self.roku.device_info
except (requests.exceptions.ConnectionError,
requests.exceptions.ReadTimeout):
pass
@property
def name(self):
"""Return the name of the device."""
if self._device_info.userdevicename:
return self._device_info.userdevicename
return "Roku {}".format(self._device_info.sernum)
@property
def unique_id(self):
"""Return a unique ID."""
return self._device_info.sernum
@property
def is_on(self):
"""Return true if device is on."""
return True
@property
def should_poll(self):
"""No polling needed for Roku."""
return False
def send_command(self, command, **kwargs):
"""Send a command to one device."""
for single_command in command:
if not hasattr(self.roku, single_command):
continue
getattr(self.roku, single_command)()

View File

@ -0,0 +1,115 @@
"""
Support for Roku platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/roku/
"""
import logging
import voluptuous as vol
from homeassistant.components.discovery import SERVICE_ROKU
from homeassistant.const import CONF_HOST
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-roku==3.1.5']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'roku'
SERVICE_SCAN = 'roku_scan'
ATTR_ROKU = 'roku'
DATA_ROKU = 'data_roku'
NOTIFICATION_ID = 'roku_notification'
NOTIFICATION_TITLE = 'Roku Setup'
NOTIFICATION_SCAN_ID = 'roku_scan_notification'
NOTIFICATION_SCAN_TITLE = 'Roku Scan'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_HOST): cv.string
})])
}, extra=vol.ALLOW_EXTRA)
# Currently no attributes but it might change later
ROKU_SCAN_SCHEMA = vol.Schema({})
def setup(hass, config):
"""Set up the Roku component."""
hass.data[DATA_ROKU] = {}
def service_handler(service):
"""Handle service calls."""
if service.service == SERVICE_SCAN:
scan_for_rokus(hass)
def roku_discovered(service, info):
"""Set up an Roku that was auto discovered."""
_setup_roku(hass, config, {
CONF_HOST: info['host']
})
discovery.listen(hass, SERVICE_ROKU, roku_discovered)
for conf in config.get(DOMAIN, []):
_setup_roku(hass, config, conf)
hass.services.register(
DOMAIN, SERVICE_SCAN, service_handler,
schema=ROKU_SCAN_SCHEMA)
return True
def scan_for_rokus(hass):
"""Scan for devices and present a notification of the ones found."""
from roku import Roku, RokuException
rokus = Roku.discover()
devices = []
for roku in rokus:
try:
r_info = roku.device_info
except RokuException: # skip non-roku device
continue
devices.append('Name: {0}<br />Host: {1}<br />'.format(
r_info.userdevicename if r_info.userdevicename
else "{} {}".format(r_info.modelname, r_info.sernum),
roku.host))
if not devices:
devices = ['No device(s) found']
hass.components.persistent_notification.create(
'The following devices were found:<br /><br />' +
'<br /><br />'.join(devices),
title=NOTIFICATION_SCAN_TITLE,
notification_id=NOTIFICATION_SCAN_ID)
def _setup_roku(hass, hass_config, roku_config):
"""Set up a Roku."""
from roku import Roku
host = roku_config[CONF_HOST]
if host in hass.data[DATA_ROKU]:
return
roku = Roku(host)
r_info = roku.device_info
hass.data[DATA_ROKU][host] = {
ATTR_ROKU: r_info.sernum
}
discovery.load_platform(
hass, 'media_player', DOMAIN, roku_config, hass_config)
discovery.load_platform(
hass, 'remote', DOMAIN, roku_config, hass_config)

View File

@ -1304,7 +1304,7 @@ python-qbittorrent==0.3.1
# homeassistant.components.sensor.ripple # homeassistant.components.sensor.ripple
python-ripple-api==0.0.3 python-ripple-api==0.0.3
# homeassistant.components.media_player.roku # homeassistant.components.roku
python-roku==3.1.5 python-roku==3.1.5
# homeassistant.components.sensor.sochain # homeassistant.components.sensor.sochain