mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
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
This commit is contained in:
parent
2c535c92bd
commit
28cef89e03
@ -3,8 +3,8 @@
|
|||||||
"name": "Androidtv",
|
"name": "Androidtv",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"adb-shell==0.0.4",
|
"adb-shell==0.0.7",
|
||||||
"androidtv==0.0.30"
|
"androidtv==0.0.32"
|
||||||
],
|
],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@JeffLIrion"]
|
"codeowners": ["@JeffLIrion"]
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
"""Support for functionality to interact with Android TV / Fire TV devices."""
|
"""Support for functionality to interact with Android TV / Fire TV devices."""
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from adb_shell.auth.keygen import keygen
|
||||||
from adb_shell.exceptions import (
|
from adb_shell.exceptions import (
|
||||||
InvalidChecksumError,
|
InvalidChecksumError,
|
||||||
InvalidCommandError,
|
InvalidCommandError,
|
||||||
@ -40,6 +42,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.exceptions import PlatformNotReady
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.storage import STORAGE_DIR
|
||||||
|
|
||||||
ANDROIDTV_DOMAIN = "androidtv"
|
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:
|
if CONF_ADB_SERVER_IP not in config:
|
||||||
# Use "adb_shell" (Python ADB implementation)
|
# Use "adb_shell" (Python ADB implementation)
|
||||||
adb_log = "using Python ADB implementation " + (
|
if CONF_ADBKEY not in config:
|
||||||
f"with adbkey='{config[CONF_ADBKEY]}'"
|
# Generate ADB key files (if they don't exist)
|
||||||
if CONF_ADBKEY in config
|
adbkey = hass.config.path(STORAGE_DIR, "androidtv_adbkey")
|
||||||
else "without adbkey authentication"
|
if not os.path.isfile(adbkey):
|
||||||
)
|
keygen(adbkey)
|
||||||
if CONF_ADBKEY in config:
|
|
||||||
|
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(
|
aftv = setup(
|
||||||
host,
|
host,
|
||||||
config[CONF_ADBKEY],
|
config[CONF_ADBKEY],
|
||||||
device_class=config[CONF_DEVICE_CLASS],
|
device_class=config[CONF_DEVICE_CLASS],
|
||||||
state_detection_rules=config[CONF_STATE_DETECTION_RULES],
|
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:
|
else:
|
||||||
# Use "pure-python-adb" (communicate with ADB server)
|
# 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(
|
aftv = setup(
|
||||||
host,
|
host,
|
||||||
adb_server_ip=config[CONF_ADB_SERVER_IP],
|
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],
|
device_class=config[CONF_DEVICE_CLASS],
|
||||||
state_detection_rules=config[CONF_STATE_DETECTION_RULES],
|
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:
|
if not aftv.available:
|
||||||
# Determine the name that will be used for the device in the log
|
# 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",
|
"establishing attempt in the next update. Error: %s",
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
self.aftv.adb.close()
|
self.aftv.adb_close()
|
||||||
self._available = False # pylint: disable=protected-access
|
self._available = False # pylint: disable=protected-access
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -429,7 +443,7 @@ class AndroidTVDevice(ADBDevice):
|
|||||||
# Check if device is disconnected.
|
# Check if device is disconnected.
|
||||||
if not self._available:
|
if not self._available:
|
||||||
# Try to connect
|
# 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
|
# To be safe, wait until the next update to run ADB commands if
|
||||||
# using the Python ADB implementation.
|
# using the Python ADB implementation.
|
||||||
@ -508,7 +522,7 @@ class FireTVDevice(ADBDevice):
|
|||||||
# Check if device is disconnected.
|
# Check if device is disconnected.
|
||||||
if not self._available:
|
if not self._available:
|
||||||
# Try to connect
|
# 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
|
# To be safe, wait until the next update to run ADB commands if
|
||||||
# using the Python ADB implementation.
|
# using the Python ADB implementation.
|
||||||
|
@ -112,7 +112,7 @@ adafruit-blinka==1.2.1
|
|||||||
adafruit-circuitpython-mcp230xx==1.1.2
|
adafruit-circuitpython-mcp230xx==1.1.2
|
||||||
|
|
||||||
# homeassistant.components.androidtv
|
# homeassistant.components.androidtv
|
||||||
adb-shell==0.0.4
|
adb-shell==0.0.7
|
||||||
|
|
||||||
# homeassistant.components.adguard
|
# homeassistant.components.adguard
|
||||||
adguardhome==0.2.1
|
adguardhome==0.2.1
|
||||||
@ -203,7 +203,7 @@ ambiclimate==0.2.1
|
|||||||
amcrest==1.5.3
|
amcrest==1.5.3
|
||||||
|
|
||||||
# homeassistant.components.androidtv
|
# homeassistant.components.androidtv
|
||||||
androidtv==0.0.30
|
androidtv==0.0.32
|
||||||
|
|
||||||
# homeassistant.components.anel_pwrctrl
|
# homeassistant.components.anel_pwrctrl
|
||||||
anel_pwrctrl-homeassistant==0.0.1.dev2
|
anel_pwrctrl-homeassistant==0.0.1.dev2
|
||||||
|
@ -49,7 +49,7 @@ YesssSMS==0.4.1
|
|||||||
abodepy==0.16.5
|
abodepy==0.16.5
|
||||||
|
|
||||||
# homeassistant.components.androidtv
|
# homeassistant.components.androidtv
|
||||||
adb-shell==0.0.4
|
adb-shell==0.0.7
|
||||||
|
|
||||||
# homeassistant.components.adguard
|
# homeassistant.components.adguard
|
||||||
adguardhome==0.2.1
|
adguardhome==0.2.1
|
||||||
@ -98,7 +98,7 @@ airly==0.0.2
|
|||||||
ambiclimate==0.2.1
|
ambiclimate==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.androidtv
|
# homeassistant.components.androidtv
|
||||||
androidtv==0.0.30
|
androidtv==0.0.32
|
||||||
|
|
||||||
# homeassistant.components.apns
|
# homeassistant.components.apns
|
||||||
apns2==0.3.0
|
apns2==0.3.0
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Define patches used for androidtv tests."""
|
"""Define patches used for androidtv tests."""
|
||||||
|
|
||||||
from socket import error as socket_error
|
from socket import error as socket_error
|
||||||
from unittest.mock import patch
|
from unittest.mock import mock_open, patch
|
||||||
|
|
||||||
|
|
||||||
class AdbDeviceFake:
|
class AdbDeviceFake:
|
||||||
@ -128,3 +128,15 @@ def patch_shell(response=None, error=False):
|
|||||||
|
|
||||||
|
|
||||||
PATCH_ADB_DEVICE = patch("androidtv.adb_manager.AdbDevice", AdbDeviceFake)
|
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)
|
||||||
|
@ -5,6 +5,7 @@ from homeassistant.setup import async_setup_component
|
|||||||
from homeassistant.components.androidtv.media_player import (
|
from homeassistant.components.androidtv.media_player import (
|
||||||
ANDROIDTV_DOMAIN,
|
ANDROIDTV_DOMAIN,
|
||||||
CONF_ADB_SERVER_IP,
|
CONF_ADB_SERVER_IP,
|
||||||
|
CONF_ADBKEY,
|
||||||
)
|
)
|
||||||
from homeassistant.components.media_player.const import DOMAIN
|
from homeassistant.components.media_player.const import DOMAIN
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -61,14 +62,8 @@ CONFIG_FIRETV_ADB_SERVER = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def _test_reconnect(hass, caplog, config):
|
def _setup(hass, config):
|
||||||
"""Test that the error and reconnection attempts are logged correctly.
|
"""Perform common setup tasks for the tests."""
|
||||||
|
|
||||||
"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
|
|
||||||
"""
|
|
||||||
if CONF_ADB_SERVER_IP not in config[DOMAIN]:
|
if CONF_ADB_SERVER_IP not in config[DOMAIN]:
|
||||||
patch_key = "python"
|
patch_key = "python"
|
||||||
else:
|
else:
|
||||||
@ -79,10 +74,26 @@ async def _test_reconnect(hass, caplog, config):
|
|||||||
else:
|
else:
|
||||||
entity_id = "media_player.fire_tv"
|
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)[
|
with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[
|
||||||
patch_key
|
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)
|
assert await async_setup_component(hass, DOMAIN, config)
|
||||||
|
|
||||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
assert state is not None
|
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)[
|
with patchers.patch_connect(False)[patch_key], patchers.patch_shell(error=True)[
|
||||||
patch_key
|
patch_key
|
||||||
]:
|
], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
|
||||||
for _ in range(5):
|
for _ in range(5):
|
||||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||||
state = hass.states.get(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
|
assert caplog.record_tuples[1][1] == logging.WARNING
|
||||||
|
|
||||||
caplog.set_level(logging.DEBUG)
|
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
|
# Update 1 will reconnect
|
||||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
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.
|
The state should be `None` and the device should be unavailable.
|
||||||
"""
|
"""
|
||||||
if CONF_ADB_SERVER_IP not in config[DOMAIN]:
|
patch_key, entity_id = _setup(hass, config)
|
||||||
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"
|
|
||||||
|
|
||||||
with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[
|
with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[
|
||||||
patch_key
|
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)
|
assert await async_setup_component(hass, DOMAIN, config)
|
||||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||||
state = hass.states.get(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)[
|
with patchers.patch_shell(None)[patch_key], patchers.patch_shell(error=True)[
|
||||||
patch_key
|
patch_key
|
||||||
]:
|
], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
|
||||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
assert state is not None
|
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)
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user