From 28cef89e038381570c75e5d5fd66821fc16b76af Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 17 Oct 2019 06:33:20 -0700 Subject: [PATCH] Generate ADB key for Android TV integration (#27344) * Generate ADB key for Android TV integration * Remove 'do_nothing' function * Remove 'return True' * Re-add 2 'return True' lines --- .../components/androidtv/manifest.json | 4 +- .../components/androidtv/media_player.py | 46 ++++++++----- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/androidtv/patchers.py | 14 +++- .../components/androidtv/test_media_player.py | 69 +++++++++++++------ 6 files changed, 96 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index e84ed35c763..9ec993b9f91 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.0.4", - "androidtv==0.0.30" + "adb-shell==0.0.7", + "androidtv==0.0.32" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index fcf4950f5e2..62ae93f96e4 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -1,8 +1,10 @@ """Support for functionality to interact with Android TV / Fire TV devices.""" import functools import logging +import os import voluptuous as vol +from adb_shell.auth.keygen import keygen from adb_shell.exceptions import ( InvalidChecksumError, InvalidCommandError, @@ -40,6 +42,7 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.storage import STORAGE_DIR ANDROIDTV_DOMAIN = "androidtv" @@ -133,27 +136,39 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if CONF_ADB_SERVER_IP not in config: # Use "adb_shell" (Python ADB implementation) - adb_log = "using Python ADB implementation " + ( - f"with adbkey='{config[CONF_ADBKEY]}'" - if CONF_ADBKEY in config - else "without adbkey authentication" - ) - if CONF_ADBKEY in config: + if CONF_ADBKEY not in config: + # Generate ADB key files (if they don't exist) + adbkey = hass.config.path(STORAGE_DIR, "androidtv_adbkey") + if not os.path.isfile(adbkey): + keygen(adbkey) + + adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" + + aftv = setup( + host, + adbkey, + device_class=config[CONF_DEVICE_CLASS], + state_detection_rules=config[CONF_STATE_DETECTION_RULES], + auth_timeout_s=10.0, + ) + + else: + adb_log = ( + f"using Python ADB implementation with adbkey='{config[CONF_ADBKEY]}'" + ) + aftv = setup( host, config[CONF_ADBKEY], device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], + auth_timeout_s=10.0, ) - else: - aftv = setup( - host, - device_class=config[CONF_DEVICE_CLASS], - state_detection_rules=config[CONF_STATE_DETECTION_RULES], - ) else: # Use "pure-python-adb" (communicate with ADB server) + adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" + aftv = setup( host, adb_server_ip=config[CONF_ADB_SERVER_IP], @@ -161,7 +176,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) - adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" if not aftv.available: # Determine the name that will be used for the device in the log @@ -257,7 +271,7 @@ def adb_decorator(override_available=False): "establishing attempt in the next update. Error: %s", err, ) - self.aftv.adb.close() + self.aftv.adb_close() self._available = False # pylint: disable=protected-access return None @@ -429,7 +443,7 @@ class AndroidTVDevice(ADBDevice): # Check if device is disconnected. if not self._available: # Try to connect - self._available = self.aftv.connect(always_log_errors=False) + self._available = self.aftv.adb_connect(always_log_errors=False) # To be safe, wait until the next update to run ADB commands if # using the Python ADB implementation. @@ -508,7 +522,7 @@ class FireTVDevice(ADBDevice): # Check if device is disconnected. if not self._available: # Try to connect - self._available = self.aftv.connect(always_log_errors=False) + self._available = self.aftv.adb_connect(always_log_errors=False) # To be safe, wait until the next update to run ADB commands if # using the Python ADB implementation. diff --git a/requirements_all.txt b/requirements_all.txt index c8d66f91468..6a70451cc12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.4 +adb-shell==0.0.7 # homeassistant.components.adguard adguardhome==0.2.1 @@ -203,7 +203,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.30 +androidtv==0.0.32 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 967943894fb..ece529ef6e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -49,7 +49,7 @@ YesssSMS==0.4.1 abodepy==0.16.5 # homeassistant.components.androidtv -adb-shell==0.0.4 +adb-shell==0.0.7 # homeassistant.components.adguard adguardhome==0.2.1 @@ -98,7 +98,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.30 +androidtv==0.0.32 # homeassistant.components.apns apns2==0.3.0 diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 73aa5225989..5fc6bc754fa 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,7 +1,7 @@ """Define patches used for androidtv tests.""" from socket import error as socket_error -from unittest.mock import patch +from unittest.mock import mock_open, patch class AdbDeviceFake: @@ -128,3 +128,15 @@ def patch_shell(response=None, error=False): PATCH_ADB_DEVICE = patch("androidtv.adb_manager.AdbDevice", AdbDeviceFake) +PATCH_ANDROIDTV_OPEN = patch("androidtv.adb_manager.open", mock_open()) +PATCH_KEYGEN = patch("homeassistant.components.androidtv.media_player.keygen") +PATCH_SIGNER = patch("androidtv.adb_manager.PythonRSASigner") + + +def isfile(filepath): + """Mock `os.path.isfile`.""" + return filepath.endswith("adbkey") + + +PATCH_ISFILE = patch("os.path.isfile", isfile) +PATCH_ACCESS = patch("os.access", return_value=True) diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index feffc70d841..85f562a3500 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -5,6 +5,7 @@ from homeassistant.setup import async_setup_component from homeassistant.components.androidtv.media_player import ( ANDROIDTV_DOMAIN, CONF_ADB_SERVER_IP, + CONF_ADBKEY, ) from homeassistant.components.media_player.const import DOMAIN from homeassistant.const import ( @@ -61,14 +62,8 @@ CONFIG_FIRETV_ADB_SERVER = { } -async def _test_reconnect(hass, caplog, config): - """Test that the error and reconnection attempts are logged correctly. - - "Handles device/service unavailable. Log a warning once when - unavailable, log once when reconnected." - - https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html - """ +def _setup(hass, config): + """Perform common setup tasks for the tests.""" if CONF_ADB_SERVER_IP not in config[DOMAIN]: patch_key = "python" else: @@ -79,10 +74,26 @@ async def _test_reconnect(hass, caplog, config): else: entity_id = "media_player.fire_tv" + return patch_key, entity_id + + +async def _test_reconnect(hass, caplog, config): + """Test that the error and reconnection attempts are logged correctly. + + "Handles device/service unavailable. Log a warning once when + unavailable, log once when reconnected." + + https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html + """ + patch_key, entity_id = _setup(hass, config) + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ patch_key - ], patchers.patch_shell("")[patch_key]: + ], patchers.patch_shell("")[ + patch_key + ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -93,7 +104,7 @@ async def _test_reconnect(hass, caplog, config): with patchers.patch_connect(False)[patch_key], patchers.patch_shell(error=True)[ patch_key - ]: + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: for _ in range(5): await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -105,7 +116,9 @@ async def _test_reconnect(hass, caplog, config): assert caplog.record_tuples[1][1] == logging.WARNING caplog.set_level(logging.DEBUG) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell("1")[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell("1")[ + patch_key + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: # Update 1 will reconnect await hass.helpers.entity_component.async_update_entity(entity_id) @@ -143,19 +156,13 @@ async def _test_adb_shell_returns_none(hass, config): The state should be `None` and the device should be unavailable. """ - if CONF_ADB_SERVER_IP not in config[DOMAIN]: - patch_key = "python" - else: - patch_key = "server" - - if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": - entity_id = "media_player.android_tv" - else: - entity_id = "media_player.fire_tv" + patch_key, entity_id = _setup(hass, config) with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ patch_key - ], patchers.patch_shell("")[patch_key]: + ], patchers.patch_shell("")[ + patch_key + ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: assert await async_setup_component(hass, DOMAIN, config) await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -164,7 +171,7 @@ async def _test_adb_shell_returns_none(hass, config): with patchers.patch_shell(None)[patch_key], patchers.patch_shell(error=True)[ patch_key - ]: + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -251,3 +258,21 @@ async def test_adb_shell_returns_none_firetv_adb_server(hass): """ assert await _test_adb_shell_returns_none(hass, CONFIG_FIRETV_ADB_SERVER) + + +async def test_setup_with_adbkey(hass): + """Test that setup succeeds when using an ADB key.""" + config = CONFIG_ANDROIDTV_PYTHON_ADB.copy() + config[DOMAIN][CONF_ADBKEY] = hass.config.path("user_provided_adbkey") + patch_key, entity_id = _setup(hass, config) + + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[ + patch_key + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, patchers.PATCH_ISFILE, patchers.PATCH_ACCESS: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF