Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Roku hub and remote #17548

Merged
merged 24 commits into from Jan 14, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion .coveragerc
Expand Up @@ -275,6 +275,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

Expand Down Expand Up @@ -583,7 +586,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
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/discovery.py
Expand Up @@ -44,6 +44,7 @@
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
SERVICE_HOMEKIT = 'homekit'
SERVICE_OCTOPRINT = 'octoprint'
SERVICE_ROKU = 'roku'

CONFIG_ENTRY_HANDLERS = {
SERVICE_DECONZ: 'deconz',
Expand All @@ -61,6 +62,7 @@
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_TELLDUSLIVE: ('tellduslive', None),
Expand All @@ -71,7 +73,6 @@
SERVICE_OCTOPRINT: ('octoprint', 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'),
Expand Down
65 changes: 18 additions & 47 deletions homeassistant/components/media_player/roku.py
@@ -1,79 +1,49 @@
"""
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

from homeassistant.components.roku import (DATA_ENTITIES)
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']
soberstadt marked this conversation as resolved.
Show resolved Hide resolved

KNOWN_HOSTS = []
DEFAULT_PORT = 8060
DEPENDENCIES = ['roku']

NOTIFICATION_ID = 'roku_notification'
NOTIFICATION_TITLE = 'Roku Media Player Setup'
DEFAULT_PORT = 8060

_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 discovery_info:
host = discovery_info.get('host')
if not discovery_info:
return

if host in KNOWN_HOSTS:
return
# Manage entity cache for service handler
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do we use the cache?

if DATA_ENTITIES not in hass.data:
hass.data[DATA_ENTITIES] = []

_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)
host = discovery_info[CONF_HOST]
entity = RokuDevice(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)
if entity not in hass.data[DATA_ENTITIES]:
hass.data[DATA_ENTITIES].append(entity)

add_entities(rokus)
async_add_entities([entity])


class RokuDevice(MediaPlayerDevice):
Expand All @@ -89,6 +59,7 @@ def __init__(self, host):
self.current_app = None
self._device_info = {}

"""fetch device info right away"""
soberstadt marked this conversation as resolved.
Show resolved Hide resolved
self.update()
soberstadt marked this conversation as resolved.
Show resolved Hide resolved

def update(self):
Expand Down
64 changes: 64 additions & 0 deletions homeassistant/components/remote/roku.py
@@ -0,0 +1,64 @@
"""
Support for the Roku remote.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/remote.roku/
"""

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)])


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._unique_id = self._roku.device_info.sernum
soberstadt marked this conversation as resolved.
Show resolved Hide resolved

@property
def name(self):
"""Return the name of the device."""
info = self._roku.device_info
soberstadt marked this conversation as resolved.
Show resolved Hide resolved
if info.userdevicename:
return info.userdevicename
return "Roku {}".format(info.sernum)

@property
def unique_id(self):
"""Return a unique ID."""
return self._unique_id

@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)()
119 changes: 119 additions & 0 deletions homeassistant/components/roku.py
@@ -0,0 +1,119 @@
"""
Support for Roku platform.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/roku/
"""
import asyncio
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'
DATA_ENTITIES = 'data_roku_entities'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define this in the platform. Stored entities should not be accessed outside of the platform. Not sharing the key will make this clearer.


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.Schema({
vol.Optional(CONF_HOST): cv.string,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config schema doesn't match the iteration we do in async_setup. There should be a list somewhere here. Use cv.ensure_list.

})
}, extra=vol.ALLOW_EXTRA)

# Currently no attributes but it might change later
ROKU_SCAN_SCHEMA = vol.Schema({})


async def scan_for_rokus(hass):
soberstadt marked this conversation as resolved.
Show resolved Hide resolved
"""Scan for devices and present a notification of the ones found."""
from roku import Roku, RokuException
rokus = await Roku.discover()

devices = []
for roku in rokus:
try:
r_info = roku.device_info
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
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)


async def async_setup(hass, config):
"""Set up the Roku component."""
hass.data[DATA_ROKU] = {}

async def async_service_handler(service):
"""Handle service calls."""
if service.service == SERVICE_SCAN:
hass.async_add_job(scan_for_rokus, hass)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just await? If we need to schedule a task, use hass.async_create_task.

return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed return.


async def roku_discovered(service, info):
"""Set up an Roku that was auto discovered."""
await _setup_roku(hass, {
CONF_HOST: info['host']
})

discovery.async_listen(hass, SERVICE_ROKU, roku_discovered)

tasks = [_setup_roku(hass, conf) for conf in config.get(DOMAIN, [])]
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
soberstadt marked this conversation as resolved.
Show resolved Hide resolved

hass.services.async_register(
DOMAIN, SERVICE_SCAN, async_service_handler,
schema=ROKU_SCAN_SCHEMA)

return True


async def _setup_roku(hass, roku_config):
"""Set up a Roku."""
from roku import Roku
host = roku_config.get(CONF_HOST)
soberstadt marked this conversation as resolved.
Show resolved Hide resolved

if host in hass.data[DATA_ROKU]:
return

roku = Roku(host)
r_info = roku.device_info
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved

hass.data[DATA_ROKU][host] = {
ATTR_ROKU: r_info.sernum
}

hass.async_create_task(discovery.async_load_platform(
hass, 'media_player', DOMAIN, roku_config))

hass.async_create_task(discovery.async_load_platform(
hass, 'remote', DOMAIN, roku_config))
1 change: 1 addition & 0 deletions requirements_all.txt
Expand Up @@ -1176,6 +1176,7 @@ python-pushover==0.3
# homeassistant.components.sensor.ripple
python-ripple-api==0.0.3

# homeassistant.components.roku
# homeassistant.components.media_player.roku
python-roku==3.1.5

Expand Down