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