From 007bf2bcb52112c8cb6e280dddf7883cf4b5a704 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 13 Mar 2019 03:18:59 -0700 Subject: [PATCH] Rename 'firetv' to 'androidtv' and add Android TV functionality (#21944) * Working on adding androidtv functionality to firetv component * 'should_poll' must return True * Change 'properties' to 'device_properties' * Also mention 'Android TV' in services.yaml * Use GitHub for 'androidtv' requirement * Add 'androidtv==0.0.10' to requirements, remove 'firetv==1.0.9' * Add 'GET_PROPERTIES' adb command option; use pypi for REQUIREMENTS * Rename integration from 'firetv' to 'androidtv' * Change default name to 'Android TV' * Rename integration from 'firetv' to 'androidtv' * Change firetv to androidtv in .coveragerc * Change firetv to androidtv in requirements_all.txt * Remove 'DEFAULT_APPS' --- .coveragerc | 2 +- .../components/androidtv/__init__.py | 6 + .../{firetv => androidtv}/media_player.py | 410 +++++++++++------- .../components/androidtv/services.yaml | 11 + homeassistant/components/firetv/__init__.py | 6 - homeassistant/components/firetv/services.yaml | 11 - requirements_all.txt | 6 +- 7 files changed, 284 insertions(+), 168 deletions(-) create mode 100644 homeassistant/components/androidtv/__init__.py rename homeassistant/components/{firetv => androidtv}/media_player.py (52%) create mode 100644 homeassistant/components/androidtv/services.yaml delete mode 100644 homeassistant/components/firetv/__init__.py delete mode 100644 homeassistant/components/firetv/services.yaml diff --git a/.coveragerc b/.coveragerc index b25d8e1f54b..b7f2961f14d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -27,6 +27,7 @@ omit = homeassistant/components/ambient_station/* homeassistant/components/amcrest/* homeassistant/components/android_ip_webcam/* + homeassistant/components/androidtv/* homeassistant/components/apcupsd/* homeassistant/components/apiai/* homeassistant/components/apple_tv/* @@ -172,7 +173,6 @@ omit = homeassistant/components/fan/wemo.py homeassistant/components/fastdotcom/* homeassistant/components/fibaro/* - homeassistant/components/firetv/* homeassistant/components/folder_watcher/* homeassistant/components/foursquare/* homeassistant/components/freebox/* diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py new file mode 100644 index 00000000000..fd108e05973 --- /dev/null +++ b/homeassistant/components/androidtv/__init__.py @@ -0,0 +1,6 @@ +""" +Support for functionality to interact with Android TV and Fire TV devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.androidtv/ +""" diff --git a/homeassistant/components/firetv/media_player.py b/homeassistant/components/androidtv/media_player.py similarity index 52% rename from homeassistant/components/firetv/media_player.py rename to homeassistant/components/androidtv/media_player.py index ca7f1de4246..ab43dc8c6ea 100644 --- a/homeassistant/components/firetv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -1,8 +1,8 @@ """ -Support for functionality to interact with FireTV devices. +Support for functionality to interact with Android TV and Fire TV devices. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/media_player.firetv/ +https://home-assistant.io/components/media_player.androidtv/ """ import functools import logging @@ -12,18 +12,25 @@ from homeassistant.components.media_player import ( MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) + SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP) from homeassistant.const import ( - ATTR_COMMAND, ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, STATE_IDLE, - STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY) + ATTR_COMMAND, ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, + CONF_PORT, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, + STATE_STANDBY) import homeassistant.helpers.config_validation as cv -FIRETV_DOMAIN = 'firetv' +ANDROIDTV_DOMAIN = 'androidtv' -REQUIREMENTS = ['firetv==1.0.9'] +REQUIREMENTS = ['androidtv==0.0.10'] _LOGGER = logging.getLogger(__name__) +SUPPORT_ANDROIDTV = SUPPORT_PAUSE | SUPPORT_PLAY | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_NEXT_TRACK | SUPPORT_STOP | SUPPORT_VOLUME_MUTE | \ + SUPPORT_VOLUME_STEP + SUPPORT_FIRETV = SUPPORT_PAUSE | SUPPORT_PLAY | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_STOP @@ -34,11 +41,15 @@ CONF_ADB_SERVER_PORT = 'adb_server_port' CONF_APPS = 'apps' CONF_GET_SOURCES = 'get_sources' -DEFAULT_NAME = 'Amazon Fire TV' +DEFAULT_NAME = 'Android TV' DEFAULT_PORT = 5555 DEFAULT_ADB_SERVER_PORT = 5037 DEFAULT_GET_SOURCES = True -DEFAULT_APPS = {} +DEFAULT_DEVICE_CLASS = 'auto' + +DEVICE_ANDROIDTV = 'androidtv' +DEVICE_FIRETV = 'firetv' +DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV] SERVICE_ADB_COMMAND = 'adb_command' @@ -58,72 +69,94 @@ def has_adb_files(value): PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): + vol.In(DEVICE_CLASSES), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_ADBKEY): has_adb_files, vol.Optional(CONF_ADB_SERVER_IP): cv.string, - vol.Optional( - CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): cv.port, + vol.Optional(CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): + cv.port, vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean, - vol.Optional( - CONF_APPS, default=DEFAULT_APPS): vol.Schema({cv.string: cv.string}) + vol.Optional(CONF_APPS, default=dict()): + vol.Schema({cv.string: cv.string}) }) -# Translate from `FireTV` reported state to HA state. -FIRETV_STATES = {'off': STATE_OFF, - 'idle': STATE_IDLE, - 'standby': STATE_STANDBY, - 'playing': STATE_PLAYING, - 'paused': STATE_PAUSED} +# Translate from `AndroidTV` / `FireTV` reported state to HA state. +ANDROIDTV_STATES = {'off': STATE_OFF, + 'idle': STATE_IDLE, + 'standby': STATE_STANDBY, + 'playing': STATE_PLAYING, + 'paused': STATE_PAUSED} def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the FireTV platform.""" - from firetv import FireTV + """Set up the Android TV / Fire TV platform.""" + from androidtv import setup - hass.data.setdefault(FIRETV_DOMAIN, {}) + hass.data.setdefault(ANDROIDTV_DOMAIN, {}) host = '{0}:{1}'.format(config[CONF_HOST], config[CONF_PORT]) if CONF_ADB_SERVER_IP not in config: # Use "python-adb" (Python ADB implementation) if CONF_ADBKEY in config: - ftv = FireTV(host, config[CONF_ADBKEY]) + aftv = setup(host, config[CONF_ADBKEY], + device_class=config[CONF_DEVICE_CLASS]) adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY]) + else: - ftv = FireTV(host) + aftv = setup(host, device_class=config[CONF_DEVICE_CLASS]) adb_log = "" else: # Use "pure-python-adb" (communicate with ADB server) - ftv = FireTV(host, adb_server_ip=config[CONF_ADB_SERVER_IP], - adb_server_port=config[CONF_ADB_SERVER_PORT]) + aftv = setup(host, adb_server_ip=config[CONF_ADB_SERVER_IP], + adb_server_port=config[CONF_ADB_SERVER_PORT], + device_class=config[CONF_DEVICE_CLASS]) adb_log = " using ADB server at {0}:{1}".format( config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT]) - if not ftv.available: - _LOGGER.warning("Could not connect to Fire TV at %s%s", host, adb_log) + if not aftv.available: + # Determine the name that will be used for the device in the log + if CONF_NAME in config: + device_name = config[CONF_NAME] + elif config[CONF_DEVICE_CLASS] == DEVICE_ANDROIDTV: + device_name = 'Android TV device' + elif config[CONF_DEVICE_CLASS] == DEVICE_FIRETV: + device_name = 'Fire TV device' + else: + device_name = 'Android TV / Fire TV device' + + _LOGGER.warning("Could not connect to %s at %s%s", + device_name, host, adb_log) return - name = config[CONF_NAME] - get_sources = config[CONF_GET_SOURCES] - apps = config[CONF_APPS] - - if host in hass.data[FIRETV_DOMAIN]: + if host in hass.data[ANDROIDTV_DOMAIN]: _LOGGER.warning("Platform already setup on %s, skipping", host) else: - device = FireTVDevice(ftv, name, get_sources, apps) - add_entities([device]) - _LOGGER.debug("Setup Fire TV at %s%s", host, adb_log) - hass.data[FIRETV_DOMAIN][host] = device + if aftv.DEVICE_CLASS == DEVICE_ANDROIDTV: + device = AndroidTVDevice(aftv, config[CONF_NAME], + config[CONF_APPS]) + device_name = config[CONF_NAME] if CONF_NAME in config \ + else 'Android TV' + else: + device = FireTVDevice(aftv, config[CONF_NAME], config[CONF_APPS], + config[CONF_GET_SOURCES]) + device_name = config[CONF_NAME] if CONF_NAME in config \ + else 'Fire TV' - if hass.services.has_service(FIRETV_DOMAIN, SERVICE_ADB_COMMAND): + add_entities([device]) + _LOGGER.debug("Setup %s at %s%s", device_name, host, adb_log) + hass.data[ANDROIDTV_DOMAIN][host] = device + + if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND): return def service_adb_command(service): """Dispatch service calls to target entities.""" cmd = service.data.get(ATTR_COMMAND) entity_id = service.data.get(ATTR_ENTITY_ID) - target_devices = [dev for dev in hass.data[FIRETV_DOMAIN].values() + target_devices = [dev for dev in hass.data[ANDROIDTV_DOMAIN].values() if dev.entity_id in entity_id] for target_device in target_devices: @@ -134,7 +167,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.info("Output of command '%s' from '%s': %s", cmd, target_device.entity_id, repr(output)) - hass.services.register(FIRETV_DOMAIN, SERVICE_ADB_COMMAND, + hass.services.register(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND, service_adb_command, schema=SERVICE_ADB_COMMAND_SCHEMA) @@ -163,24 +196,21 @@ def adb_decorator(override_available=False): return _adb_decorator -class FireTVDevice(MediaPlayerDevice): - """Representation of an Amazon Fire TV device on the network.""" +class ADBDevice(MediaPlayerDevice): + """Representation of an Android TV or Fire TV device.""" - def __init__(self, ftv, name, get_sources, apps): - """Initialize the FireTV device.""" - from firetv import APPS, KEYS - self.apps = APPS - self.keys = KEYS - - self.apps.update(apps) - - self.firetv = ftv + def __init__(self, aftv, name, apps): + """Initialize the Android TV / Fire TV device.""" + from androidtv.constants import APPS, KEYS + self.aftv = aftv self._name = name - self._get_sources = get_sources + self._apps = APPS + self._apps.update(apps) + self._keys = KEYS # ADB exceptions to catch - if not self.firetv.adb_server_ip: + if not self.aftv.adb_server_ip: # Using "python-adb" (Python ADB implementation) from adb.adb_protocol import (InvalidChecksumError, InvalidCommandError, @@ -195,10 +225,25 @@ class FireTVDevice(MediaPlayerDevice): # Using "pure-python-adb" (communicate with ADB server) self.exceptions = (ConnectionResetError,) - self._state = None - self._available = self.firetv.available + # Property attributes + self._available = self.aftv.available self._current_app = None - self._running_apps = None + self._state = None + + @property + def app_id(self): + """Return the current app.""" + return self._current_app + + @property + def app_name(self): + """Return the friendly name of the current app.""" + return self._apps.get(self._current_app, self._current_app) + + @property + def available(self): + """Return whether or not the ADB connection is valid.""" + return self._available @property def name(self): @@ -210,30 +255,170 @@ class FireTVDevice(MediaPlayerDevice): """Device should be polled.""" return True - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_FIRETV - @property def state(self): """Return the state of the player.""" return self._state - @property - def available(self): - """Return whether or not the ADB connection is valid.""" - return self._available + @adb_decorator() + def media_play(self): + """Send play command.""" + self.aftv.media_play() + + @adb_decorator() + def media_pause(self): + """Send pause command.""" + self.aftv.media_pause() + + @adb_decorator() + def media_play_pause(self): + """Send play/pause command.""" + self.aftv.media_play_pause() + + @adb_decorator() + def turn_on(self): + """Turn on the device.""" + self.aftv.turn_on() + + @adb_decorator() + def turn_off(self): + """Turn off the device.""" + self.aftv.turn_off() + + @adb_decorator() + def media_previous_track(self): + """Send previous track command (results in rewind).""" + self.aftv.media_previous() + + @adb_decorator() + def media_next_track(self): + """Send next track command (results in fast-forward).""" + self.aftv.media_next() + + @adb_decorator() + def adb_command(self, cmd): + """Send an ADB command to an Android TV / Fire TV device.""" + key = self._keys.get(cmd) + if key: + return self.aftv.adb_shell('input keyevent {}'.format(key)) + + if cmd == 'GET_PROPERTIES': + return self.aftv.get_properties_dict() + + return self.aftv.adb_shell(cmd) + + +class AndroidTVDevice(ADBDevice): + """Representation of an Android TV device.""" + + def __init__(self, aftv, name, apps): + """Initialize the Android TV device.""" + super().__init__(aftv, name, apps) + + self._device = None + self._muted = None + self._device_properties = self.aftv.device_properties + self._unique_id = 'androidtv-{}-{}'.format( + name, self._device_properties['serialno']) + self._volume = None + + @adb_decorator(override_available=True) + def update(self): + """Update the device state and, if necessary, re-connect.""" + # Check if device is disconnected. + if not self._available: + # Try to connect + self._available = self.aftv.connect(always_log_errors=False) + + # To be safe, wait until the next update to run ADB commands. + return + + # If the ADB connection is not intact, don't update. + if not self._available: + return + + # Get the `state`, `current_app`, and `running_apps`. + state, self._current_app, self._device, self._muted, self._volume = \ + self.aftv.update() + + self._state = ANDROIDTV_STATES[state] @property - def app_id(self): - """Return the current app.""" - return self._current_app + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted @property - def app_name(self): - """Return the friendly name of the current app.""" - return self.apps.get(self._current_app, self._current_app) + def source(self): + """Return the current playback device.""" + return self._device + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_ANDROIDTV + + @property + def unique_id(self): + """Return the device unique id.""" + return self._unique_id + + @property + def volume_level(self): + """Return the volume level.""" + return self._volume + + @adb_decorator() + def media_stop(self): + """Send stop command.""" + self.aftv.media_stop() + + @adb_decorator() + def mute_volume(self, mute): + """Mute the volume.""" + self.aftv.mute_volume() + + @adb_decorator() + def volume_down(self): + """Send volume down command.""" + self.aftv.volume_down() + + @adb_decorator() + def volume_up(self): + """Send volume up command.""" + self.aftv.volume_up() + + +class FireTVDevice(ADBDevice): + """Representation of a Fire TV device.""" + + def __init__(self, aftv, name, apps, get_sources): + """Initialize the Fire TV device.""" + super().__init__(aftv, name, apps) + + self._get_sources = get_sources + self._running_apps = None + + @adb_decorator(override_available=True) + def update(self): + """Update the device state and, if necessary, re-connect.""" + # Check if device is disconnected. + if not self._available: + # Try to connect + self._available = self.aftv.connect(always_log_errors=False) + + # To be safe, wait until the next update to run ADB commands. + return + + # If the ADB connection is not intact, don't update. + if not self._available: + return + + # Get the `state`, `current_app`, and `running_apps`. + state, self._current_app, self._running_apps = \ + self.aftv.update(self._get_sources) + + self._state = ANDROIDTV_STATES[state] @property def source(self): @@ -245,76 +430,15 @@ class FireTVDevice(MediaPlayerDevice): """Return a list of running apps.""" return self._running_apps - @adb_decorator(override_available=True) - def update(self): - """Update the device state and, if necessary, re-connect.""" - # Check if device is disconnected. - if not self._available: - # Try to connect - self._available = self.firetv.connect() - - # To be safe, wait until the next update to run ADB commands. - return - - # If the ADB connection is not intact, don't update. - if not self._available: - return - - # Get the `state`, `current_app`, and `running_apps`. - ftv_state, self._current_app, self._running_apps = \ - self.firetv.update(self._get_sources) - - self._state = FIRETV_STATES[ftv_state] - - @adb_decorator() - def turn_on(self): - """Turn on the device.""" - self.firetv.turn_on() - - @adb_decorator() - def turn_off(self): - """Turn off the device.""" - self.firetv.turn_off() - - @adb_decorator() - def media_play(self): - """Send play command.""" - self.firetv.media_play() - - @adb_decorator() - def media_pause(self): - """Send pause command.""" - self.firetv.media_pause() - - @adb_decorator() - def media_play_pause(self): - """Send play/pause command.""" - self.firetv.media_play_pause() + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_FIRETV @adb_decorator() def media_stop(self): """Send stop (back) command.""" - self.firetv.back() - - @adb_decorator() - def volume_up(self): - """Send volume up command.""" - self.firetv.volume_up() - - @adb_decorator() - def volume_down(self): - """Send volume down command.""" - self.firetv.volume_down() - - @adb_decorator() - def media_previous_track(self): - """Send previous track command (results in rewind).""" - self.firetv.media_previous() - - @adb_decorator() - def media_next_track(self): - """Send next track command (results in fast-forward).""" - self.firetv.media_next() + self.aftv.back() @adb_decorator() def select_source(self, source): @@ -325,14 +449,6 @@ class FireTVDevice(MediaPlayerDevice): """ if isinstance(source, str): if not source.startswith('!'): - self.firetv.launch_app(source) + self.aftv.launch_app(source) else: - self.firetv.stop_app(source[1:].lstrip()) - - @adb_decorator() - def adb_command(self, cmd): - """Send an ADB command to a Fire TV device.""" - key = self.keys.get(cmd) - if key: - return self.firetv.adb_shell('input keyevent {}'.format(key)) - return self.firetv.adb_shell(cmd) + self.aftv.stop_app(source[1:].lstrip()) diff --git a/homeassistant/components/androidtv/services.yaml b/homeassistant/components/androidtv/services.yaml new file mode 100644 index 00000000000..78ff0a828f6 --- /dev/null +++ b/homeassistant/components/androidtv/services.yaml @@ -0,0 +1,11 @@ +# Describes the format for available Android TV and Fire TV services + +adb_command: + description: Send an ADB command to an Android TV / Fire TV device. + fields: + entity_id: + description: Name(s) of Android TV / Fire TV entities. + example: 'media_player.android_tv_living_room' + command: + description: Either a key command or an ADB shell command. + example: 'HOME' diff --git a/homeassistant/components/firetv/__init__.py b/homeassistant/components/firetv/__init__.py deleted file mode 100644 index 68f55631332..00000000000 --- a/homeassistant/components/firetv/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -Support for functionality to interact with FireTV devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/media_player.firetv/ -""" diff --git a/homeassistant/components/firetv/services.yaml b/homeassistant/components/firetv/services.yaml deleted file mode 100644 index 78019547641..00000000000 --- a/homeassistant/components/firetv/services.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Describes the format for available Fire TV services - -adb_command: - description: Send an ADB command to a Fire TV device. - fields: - entity_id: - description: Name(s) of Fire TV entities. - example: 'media_player.fire_tv_living_room' - command: - description: Either a key command or an ADB shell command. - example: 'HOME' diff --git a/requirements_all.txt b/requirements_all.txt index 1af8f373c6f..dc6b2e17d94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -157,6 +157,9 @@ alpha_vantage==2.1.0 # homeassistant.components.amcrest amcrest==1.2.5 +# homeassistant.components.androidtv.media_player +androidtv==0.0.10 + # homeassistant.components.switch.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 @@ -435,9 +438,6 @@ fiblary3==0.1.7 # homeassistant.components.sensor.fints fints==1.0.1 -# homeassistant.components.firetv.media_player -firetv==1.0.9 - # homeassistant.components.sensor.fitbit fitbit==0.3.0