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.9pull/21425/head
parent
814e610b1d
commit
ff93cdb0bc
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue