From 88d13f0ac96af181a5e2c1bf12de2e56f23f03cc Mon Sep 17 00:00:00 2001 From: Brent <bah2830@users.noreply.github.com> Date: Sun, 15 May 2016 15:00:31 -0500 Subject: [PATCH] Added support for the roku media player (#2046) --- .coveragerc | 1 + homeassistant/components/discovery.py | 4 +- .../components/media_player/__init__.py | 1 + homeassistant/components/media_player/roku.py | 187 ++++++++++++++++++ requirements_all.txt | 5 +- 5 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/media_player/roku.py diff --git a/.coveragerc b/.coveragerc index c6af4c298b9..7a3978d04cc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -122,6 +122,7 @@ omit = homeassistant/components/media_player/panasonic_viera.py homeassistant/components/media_player/pioneer.py homeassistant/components/media_player/plex.py + homeassistant/components/media_player/roku.py homeassistant/components/media_player/samsungtv.py homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/sonos.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 01211398f72..fac94aacf69 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -15,7 +15,7 @@ from homeassistant.const import ( EVENT_PLATFORM_DISCOVERED) DOMAIN = "discovery" -REQUIREMENTS = ['netdisco==0.6.6'] +REQUIREMENTS = ['netdisco==0.6.7'] SCAN_INTERVAL = 300 # seconds @@ -29,6 +29,7 @@ SERVICE_SONOS = 'sonos' SERVICE_PLEX = 'plex_mediaserver' SERVICE_SQUEEZEBOX = 'logitech_mediaserver' SERVICE_PANASONIC_VIERA = 'panasonic_viera' +SERVICE_ROKU = 'roku' SERVICE_HANDLERS = { SERVICE_WEMO: "wemo", @@ -39,6 +40,7 @@ SERVICE_HANDLERS = { SERVICE_PLEX: 'media_player', SERVICE_SQUEEZEBOX: 'media_player', SERVICE_PANASONIC_VIERA: 'media_player', + SERVICE_ROKU: 'media_player', } diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index ff8eb8113b9..072a145129b 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -36,6 +36,7 @@ DISCOVERY_PLATFORMS = { discovery.SERVICE_PLEX: 'plex', discovery.SERVICE_SQUEEZEBOX: 'squeezebox', discovery.SERVICE_PANASONIC_VIERA: 'panasonic_viera', + discovery.SERVICE_ROKU: 'roku', } SERVICE_PLAY_MEDIA = 'play_media' diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py new file mode 100644 index 00000000000..3a196fe38d4 --- /dev/null +++ b/homeassistant/components/media_player/roku.py @@ -0,0 +1,187 @@ +""" +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 + +from homeassistant.components.media_player import ( + MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_SELECT_SOURCE, MediaPlayerDevice) + +from homeassistant.const import ( + CONF_HOST, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, STATE_HOME) + +REQUIREMENTS = [ + 'https://github.com/bah2830/python-roku/archive/3.1.1.zip' + '#python-roku==3.1.1'] + +KNOWN_HOSTS = [] +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 + + +# pylint: disable=abstract-method +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Roku platform.""" + hosts = [] + + if discovery_info and discovery_info in KNOWN_HOSTS: + return + + if discovery_info is not None: + _LOGGER.debug('Discovered Roku: %s', discovery_info[0]) + hosts.append(discovery_info[0]) + + elif CONF_HOST in config: + hosts.append(config[CONF_HOST]) + + rokus = [] + for host in hosts: + rokus.append(RokuDevice(host)) + KNOWN_HOSTS.append(host) + + add_devices(rokus) + + +class RokuDevice(MediaPlayerDevice): + """Representation of a Roku device on the network.""" + + # pylint: disable=abstract-method + # pylint: disable=too-many-public-methods + def __init__(self, host): + """Initialize the Roku device.""" + from roku import Roku + + self.roku = Roku(host) + self.update() + + def update(self): + """Retrieve latest state.""" + self.roku_name = "roku_" + self.roku.device_info.sernum + self.ip_address = self.roku.host + self.channels = self.get_source_list() + + if self.roku.current_app is not None: + self.current_app = self.roku.current_app + else: + self.current_app = None + + def get_source_list(self): + """Get the list of applications to be used as sources.""" + return ["Home"] + sorted(channel.name for channel in self.roku.apps) + + @property + def should_poll(self): + """Device should be polled.""" + return True + + @property + def name(self): + """Return the name of the device.""" + return self.roku_name + + @property + def state(self): + """Return the state of the device.""" + if self.current_app.name in ["Power Saver", "Default screensaver"]: + return STATE_IDLE + elif self.current_app.name == "Roku": + return STATE_HOME + elif self.current_app.name is not None: + return STATE_PLAYING + + return STATE_UNKNOWN + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_ROKU + + @property + def media_content_type(self): + """Content type of current playing media.""" + if self.current_app is None: + return None + elif self.current_app.name == "Power Saver": + return None + elif self.current_app.name == "Roku": + return None + else: + return MEDIA_TYPE_VIDEO + + @property + def media_image_url(self): + """Image url of current playing media.""" + if self.current_app is None: + return None + elif self.current_app.name == "Roku": + return None + elif self.current_app.name == "Power Saver": + return None + elif self.current_app.id is None: + return None + + return 'http://{0}:{1}/query/icon/{2}'.format(self.ip_address, + DEFAULT_PORT, + self.current_app.id) + + @property + def app_name(self): + """Name of the current running app.""" + return self.current_app.name + + @property + def app_id(self): + """Return the ID of the current running app.""" + return self.current_app.id + + @property + def source(self): + """Return the current input source.""" + return self.current_app.name + + @property + def source_list(self): + """List of available input sources.""" + return self.channels + + def media_play_pause(self): + """Send play/pause command.""" + self.roku.play() + + def media_previous_track(self): + """Send previous track command.""" + self.roku.reverse() + + def media_next_track(self): + """Send next track command.""" + self.roku.forward() + + def mute_volume(self, mute): + """Mute the volume.""" + self.roku.volume_mute() + + def volume_up(self): + """Volume up media player.""" + self.roku.volume_up() + + def volume_down(self): + """Volume down media player.""" + self.roku.volume_down() + + def select_source(self, source): + """Select input source.""" + if source == "Home": + self.roku.home() + else: + channel = self.roku[source] + channel.launch() diff --git a/requirements_all.txt b/requirements_all.txt index 60461a5eddf..90d88e0bcae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -97,6 +97,9 @@ https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcl # homeassistant.components.alarm_control_panel.alarmdotcom https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1 +# homeassistant.components.media_player.roku +https://github.com/bah2830/python-roku/archive/3.1.1.zip#python-roku==3.1.1 + # homeassistant.components.modbus https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0 @@ -156,7 +159,7 @@ messagebird==1.2.0 mficlient==0.3.0 # homeassistant.components.discovery -netdisco==0.6.6 +netdisco==0.6.7 # homeassistant.components.sensor.neurio_energy neurio==0.2.10 -- GitLab