diff --git a/.coveragerc b/.coveragerc index aedf311d6cd..bab0eb8703e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -308,6 +308,9 @@ omit = 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 @@ -642,7 +645,6 @@ omit = homeassistant/components/media_player/pioneer.py homeassistant/components/media_player/pjlink.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_rnet.py homeassistant/components/media_player/snapcast.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index f87395520bb..d8198ba3033 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -47,6 +47,7 @@ SERVICE_OCTOPRINT = 'octoprint' SERVICE_FREEBOX = 'freebox' SERVICE_IGD = 'igd' SERVICE_DLNA_DMR = 'dlna_dmr' +SERVICE_ROKU = 'roku' CONFIG_ENTRY_HANDLERS = { SERVICE_DAIKIN: 'daikin', @@ -67,6 +68,7 @@ SERVICE_HANDLERS = { SERVICE_HASSIO: ('hassio', None), SERVICE_AXIS: ('axis', None), SERVICE_APPLE_TV: ('apple_tv', None), + SERVICE_ROKU: ('roku', None), SERVICE_WINK: ('wink', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_SABNZBD: ('sabnzbd', None), @@ -76,7 +78,6 @@ SERVICE_HANDLERS = { SERVICE_FREEBOX: ('freebox', None), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), - 'roku': ('media_player', 'roku'), 'yamaha': ('media_player', 'yamaha'), 'logitech_mediaserver': ('media_player', 'squeezebox'), 'directv': ('media_player', 'directv'), diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index fccca235193..20a6f42d729 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -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 https://home-assistant.io/components/media_player.roku/ """ import logging - -import voluptuous as vol +import requests.exceptions 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_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( 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 -NOTIFICATION_ID = 'roku_notification' -NOTIFICATION_TITLE = 'Roku Media Player Setup' - _LOGGER = logging.getLogger(__name__) SUPPORT_ROKU = SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\ SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_SELECT_SOURCE | SUPPORT_PLAY -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST): cv.string, -}) - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Roku platform.""" - hosts = [] + if not discovery_info: + return - if discovery_info: - host = discovery_info.get('host') - - 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 {}
' - 'Check its network connection or consider ' - 'using auto discovery.
' - 'You will need to restart hass after fixing.' - ''.format(config.get(CONF_HOST)), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - - add_entities(rokus) + host = discovery_info[CONF_HOST] + async_add_entities([RokuDevice(host)], True) class RokuDevice(MediaPlayerDevice): @@ -89,12 +48,8 @@ class RokuDevice(MediaPlayerDevice): self.current_app = None self._device_info = {} - self.update() - def update(self): """Retrieve latest state.""" - import requests.exceptions - try: self._device_info = self.roku.device_info self.ip_address = self.roku.host @@ -106,7 +61,6 @@ class RokuDevice(MediaPlayerDevice): self.current_app = None except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout): - pass def get_source_list(self): diff --git a/homeassistant/components/remote/roku.py b/homeassistant/components/remote/roku.py new file mode 100644 index 00000000000..86a7105dafe --- /dev/null +++ b/homeassistant/components/remote/roku.py @@ -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)() diff --git a/homeassistant/components/roku.py b/homeassistant/components/roku.py new file mode 100644 index 00000000000..5ceebb3dee5 --- /dev/null +++ b/homeassistant/components/roku.py @@ -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}
Host: {1}
'.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:

' + + '

'.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) diff --git a/requirements_all.txt b/requirements_all.txt index f7f56db88b7..8dd6709898f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1304,7 +1304,7 @@ python-qbittorrent==0.3.1 # homeassistant.components.sensor.ripple python-ripple-api==0.0.3 -# homeassistant.components.media_player.roku +# homeassistant.components.roku python-roku==3.1.5 # homeassistant.components.sensor.sochain