Add ADB server functionality to Fire TV (#21221)

* Bump firetv to 1.0.8

* Update the 'update' function for Fire TV

* Return None for properties when unavailable

* Remove 'self.adb_lock' attribute

* Remove threading import

* Update configuration for Fire TV component

* Clarify 'python-adb' vs. 'pure-python-adb'

* Rename '__adb_decorator' to '_adb_exception_catcher'

* Don't check 'self._available' in properties

* Bump firetv to 1.0.9
pull/21425/head
Jeff Irion 2019-02-24 15:16:49 -08:00 committed by Martin Hjelmare
parent 814e610b1d
commit ff93cdb0bc
2 changed files with 61 additions and 104 deletions

View File

@ -6,7 +6,6 @@ https://home-assistant.io/components/media_player.firetv/
""" """
import functools import functools
import logging import logging
import threading
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
@ -20,22 +19,22 @@ from homeassistant.const import (
STATE_PLAYING, STATE_STANDBY) STATE_PLAYING, STATE_STANDBY)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['firetv==1.0.7'] REQUIREMENTS = ['firetv==1.0.9']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_FIRETV = SUPPORT_PAUSE | \ SUPPORT_FIRETV = SUPPORT_PAUSE | SUPPORT_PLAY | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_STOP | \ SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_STOP
SUPPORT_PLAY
CONF_ADBKEY = 'adbkey' CONF_ADBKEY = 'adbkey'
CONF_GET_SOURCE = 'get_source' CONF_ADB_SERVER_IP = 'adb_server_ip'
CONF_ADB_SERVER_PORT = 'adb_server_port'
CONF_GET_SOURCES = 'get_sources' CONF_GET_SOURCES = 'get_sources'
DEFAULT_NAME = 'Amazon Fire TV' DEFAULT_NAME = 'Amazon Fire TV'
DEFAULT_PORT = 5555 DEFAULT_PORT = 5555
DEFAULT_GET_SOURCE = True DEFAULT_ADB_SERVER_PORT = 5037
DEFAULT_GET_SOURCES = True DEFAULT_GET_SOURCES = True
@ -52,12 +51,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_ADBKEY): has_adb_files, vol.Optional(CONF_ADBKEY): has_adb_files,
vol.Optional(CONF_GET_SOURCE, default=DEFAULT_GET_SOURCE): cv.boolean, vol.Optional(CONF_ADB_SERVER_IP): cv.string,
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_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean
}) })
PACKAGE_LAUNCHER = "com.amazon.tv.launcher" # Translate from `FireTV` reported state to HA state.
PACKAGE_SETTINGS = "com.amazon.tv.settings" FIRETV_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): def setup_platform(hass, config, add_entities, discovery_info=None):
@ -66,82 +71,82 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
host = '{0}:{1}'.format(config[CONF_HOST], config[CONF_PORT]) host = '{0}:{1}'.format(config[CONF_HOST], config[CONF_PORT])
if CONF_ADBKEY in config: if CONF_ADB_SERVER_IP not in config:
ftv = FireTV(host, config[CONF_ADBKEY]) # Use "python-adb" (Python ADB implementation)
adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY]) if CONF_ADBKEY in config:
ftv = FireTV(host, config[CONF_ADBKEY])
adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY])
else:
ftv = FireTV(host)
adb_log = ""
else: else:
ftv = FireTV(host) # Use "pure-python-adb" (communicate with ADB server)
adb_log = "" ftv = FireTV(host, adb_server_ip=config[CONF_ADB_SERVER_IP],
adb_server_port=config[CONF_ADB_SERVER_PORT])
adb_log = " using ADB server at {0}:{1}".format(
config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT])
if not ftv.available: if not ftv.available:
_LOGGER.warning("Could not connect to Fire TV at %s%s", host, adb_log) _LOGGER.warning("Could not connect to Fire TV at %s%s", host, adb_log)
return return
name = config[CONF_NAME] name = config[CONF_NAME]
get_source = config[CONF_GET_SOURCE]
get_sources = config[CONF_GET_SOURCES] get_sources = config[CONF_GET_SOURCES]
device = FireTVDevice(ftv, name, get_source, get_sources) device = FireTVDevice(ftv, name, get_sources)
add_entities([device]) add_entities([device])
_LOGGER.debug("Setup Fire TV at %s%s", host, adb_log) _LOGGER.debug("Setup Fire TV at %s%s", host, adb_log)
def adb_decorator(override_available=False): def adb_decorator(override_available=False):
"""Send an ADB command if the device is available and not locked.""" """Send an ADB command if the device is available and catch exceptions."""
def adb_wrapper(func): def _adb_decorator(func):
"""Wait if previous ADB commands haven't finished.""" """Wait if previous ADB commands haven't finished."""
@functools.wraps(func) @functools.wraps(func)
def _adb_wrapper(self, *args, **kwargs): def _adb_exception_catcher(self, *args, **kwargs):
# If the device is unavailable, don't do anything # If the device is unavailable, don't do anything
if not self.available and not override_available: if not self.available and not override_available:
return None return None
# If an ADB command is already running, skip this command
if not self.adb_lock.acquire(blocking=False):
_LOGGER.info("Skipping an ADB command because a previous "
"command is still running")
return None
# Additional ADB commands will be prevented while trying this one
try: try:
returns = func(self, *args, **kwargs) return func(self, *args, **kwargs)
except self.exceptions as err: except self.exceptions as err:
_LOGGER.error( _LOGGER.error(
"Failed to execute an ADB command. ADB connection re-" "Failed to execute an ADB command. ADB connection re-"
"establishing attempt in the next update. Error: %s", err) "establishing attempt in the next update. Error: %s", err)
returns = None
self._available = False # pylint: disable=protected-access self._available = False # pylint: disable=protected-access
finally: return None
self.adb_lock.release()
return returns return _adb_exception_catcher
return _adb_wrapper return _adb_decorator
return adb_wrapper
class FireTVDevice(MediaPlayerDevice): class FireTVDevice(MediaPlayerDevice):
"""Representation of an Amazon Fire TV device on the network.""" """Representation of an Amazon Fire TV device on the network."""
def __init__(self, ftv, name, get_source, get_sources): def __init__(self, ftv, name, get_sources):
"""Initialize the FireTV device.""" """Initialize the FireTV device."""
from adb.adb_protocol import (
InvalidChecksumError, InvalidCommandError, InvalidResponseError)
self.firetv = ftv self.firetv = ftv
self._name = name self._name = name
self._get_source = get_source
self._get_sources = get_sources self._get_sources = get_sources
# whether or not the ADB connection is currently in use
self.adb_lock = threading.Lock()
# ADB exceptions to catch # ADB exceptions to catch
self.exceptions = ( if not self.firetv.adb_server_ip:
AttributeError, BrokenPipeError, TypeError, ValueError, # Using "python-adb" (Python ADB implementation)
InvalidChecksumError, InvalidCommandError, InvalidResponseError) from adb.adb_protocol import (InvalidChecksumError,
InvalidCommandError,
InvalidResponseError)
from adb.usb_exceptions import TcpTimeoutException
self.exceptions = (AttributeError, BrokenPipeError, TypeError,
ValueError, InvalidChecksumError,
InvalidCommandError, InvalidResponseError,
TcpTimeoutException)
else:
# Using "pure-python-adb" (communicate with ADB server)
self.exceptions = (ConnectionResetError,)
self._state = None self._state = None
self._available = self.firetv.available self._available = self.firetv.available
@ -190,72 +195,24 @@ class FireTVDevice(MediaPlayerDevice):
@adb_decorator(override_available=True) @adb_decorator(override_available=True)
def update(self): def update(self):
"""Get the latest date and update device state.""" """Update the device state and, if necessary, re-connect."""
# Check if device is disconnected. # Check if device is disconnected.
if not self._available: if not self._available:
self._running_apps = None
self._current_app = None
# Try to connect # Try to connect
self.firetv.connect() self._available = self.firetv.connect()
self._available = self.firetv.available
# To be safe, wait until the next update to run ADB commands.
return
# If the ADB connection is not intact, don't update. # If the ADB connection is not intact, don't update.
if not self._available: if not self._available:
return return
# Check if device is off. # Get the `state`, `current_app`, and `running_apps`.
if not self.firetv.screen_on: ftv_state, self._current_app, self._running_apps = \
self._state = STATE_OFF self.firetv.update(self._get_sources)
self._running_apps = None
self._current_app = None
# Check if screen saver is on. self._state = FIRETV_STATES[ftv_state]
elif not self.firetv.awake:
self._state = STATE_IDLE
self._running_apps = None
self._current_app = None
else:
# Get the running apps.
if self._get_sources:
self._running_apps = self.firetv.running_apps
# Get the current app.
if self._get_source:
current_app = self.firetv.current_app
if isinstance(current_app, dict)\
and 'package' in current_app:
self._current_app = current_app['package']
else:
self._current_app = current_app
# Show the current app as the only running app.
if not self._get_sources:
if self._current_app:
self._running_apps = [self._current_app]
else:
self._running_apps = None
# Check if the launcher is active.
if self._current_app in [PACKAGE_LAUNCHER, PACKAGE_SETTINGS]:
self._state = STATE_STANDBY
# Check for a wake lock (device is playing).
elif self.firetv.wake_lock:
self._state = STATE_PLAYING
# Otherwise, device is paused.
else:
self._state = STATE_PAUSED
# Don't get the current app.
elif self.firetv.wake_lock:
# Check for a wake lock (device is playing).
self._state = STATE_PLAYING
else:
# Assume the devices is on standby.
self._state = STATE_STANDBY
@adb_decorator() @adb_decorator()
def turn_on(self): def turn_on(self):

View File

@ -420,7 +420,7 @@ fiblary3==0.1.7
fints==1.0.1 fints==1.0.1
# homeassistant.components.media_player.firetv # homeassistant.components.media_player.firetv
firetv==1.0.7 firetv==1.0.9
# homeassistant.components.sensor.fitbit # homeassistant.components.sensor.fitbit
fitbit==0.3.0 fitbit==0.3.0