mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
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:
parent
7f3871028d
commit
7db28d3d91
@ -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
|
||||||
|
@ -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'),
|
||||||
|
@ -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):
|
||||||
|
72
homeassistant/components/remote/roku.py
Normal file
72
homeassistant/components/remote/roku.py
Normal 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)()
|
115
homeassistant/components/roku.py
Normal file
115
homeassistant/components/roku.py
Normal 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)
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user