mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Register 'androidtv.download' and 'androidtv.upload' services (#30086)
* Add tests * Add FileSync test * Fill in services.yaml for 'androidtv.adb_filesync' service * Update example paths in services.yaml * Bump androidtv to 0.0.37 * Bump androidtv to 0.0.37 * Bump androidtv to 0.0.37 * Import LockNotAcquiredException * Import LockNotAcquiredException from androidtv.exceptions * Rename 'host' to 'address' * Add a logging statement when an ADB command is skipped * Check hass.config.is_allowed_path(local_path) * Add return * Fix pylint * Reduce duplicated code (AndroidTVDevice vs. FireTVDevice) * Split 'adb_filesync' service into 'download' and 'upload' services * Don't use '.get()' for required data; return if the services are already registered * Replace "command" with ATTR_COMMAND * Don't try to connect to a device if it is a duplicate
This commit is contained in:
parent
e88bfda2a8
commit
5ec5df77cc
@ -4,7 +4,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"adb-shell==0.1.0",
|
"adb-shell==0.1.0",
|
||||||
"androidtv==0.0.36",
|
"androidtv==0.0.37",
|
||||||
"pure-python-adb==0.2.2.dev0"
|
"pure-python-adb==0.2.2.dev0"
|
||||||
],
|
],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
|
@ -12,6 +12,7 @@ from adb_shell.exceptions import (
|
|||||||
)
|
)
|
||||||
from androidtv import ha_state_detection_rules_validator, setup
|
from androidtv import ha_state_detection_rules_validator, setup
|
||||||
from androidtv.constants import APPS, KEYS
|
from androidtv.constants import APPS, KEYS
|
||||||
|
from androidtv.exceptions import LockNotAcquiredException
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
|
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
|
||||||
@ -72,6 +73,9 @@ SUPPORT_FIRETV = (
|
|||||||
| SUPPORT_STOP
|
| SUPPORT_STOP
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ATTR_DEVICE_PATH = "device_path"
|
||||||
|
ATTR_LOCAL_PATH = "local_path"
|
||||||
|
|
||||||
CONF_ADBKEY = "adbkey"
|
CONF_ADBKEY = "adbkey"
|
||||||
CONF_ADB_SERVER_IP = "adb_server_ip"
|
CONF_ADB_SERVER_IP = "adb_server_ip"
|
||||||
CONF_ADB_SERVER_PORT = "adb_server_port"
|
CONF_ADB_SERVER_PORT = "adb_server_port"
|
||||||
@ -92,11 +96,29 @@ DEVICE_FIRETV = "firetv"
|
|||||||
DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV]
|
DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV]
|
||||||
|
|
||||||
SERVICE_ADB_COMMAND = "adb_command"
|
SERVICE_ADB_COMMAND = "adb_command"
|
||||||
|
SERVICE_DOWNLOAD = "download"
|
||||||
|
SERVICE_UPLOAD = "upload"
|
||||||
|
|
||||||
SERVICE_ADB_COMMAND_SCHEMA = vol.Schema(
|
SERVICE_ADB_COMMAND_SCHEMA = vol.Schema(
|
||||||
{vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_COMMAND): cv.string}
|
{vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_COMMAND): cv.string}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SERVICE_DOWNLOAD_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||||
|
vol.Required(ATTR_DEVICE_PATH): cv.string,
|
||||||
|
vol.Required(ATTR_LOCAL_PATH): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SERVICE_UPLOAD_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
|
vol.Required(ATTR_DEVICE_PATH): cv.string,
|
||||||
|
vol.Required(ATTR_LOCAL_PATH): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
@ -133,7 +155,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
"""Set up the Android TV / Fire TV platform."""
|
"""Set up the Android TV / Fire TV platform."""
|
||||||
hass.data.setdefault(ANDROIDTV_DOMAIN, {})
|
hass.data.setdefault(ANDROIDTV_DOMAIN, {})
|
||||||
|
|
||||||
host = f"{config[CONF_HOST]}:{config[CONF_PORT]}"
|
address = f"{config[CONF_HOST]}:{config[CONF_PORT]}"
|
||||||
|
|
||||||
|
if address in hass.data[ANDROIDTV_DOMAIN]:
|
||||||
|
_LOGGER.warning("Platform already setup on %s, skipping", address)
|
||||||
|
return
|
||||||
|
|
||||||
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)
|
||||||
@ -192,44 +218,38 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
else:
|
else:
|
||||||
device_name = "Android TV / Fire TV device"
|
device_name = "Android TV / Fire TV device"
|
||||||
|
|
||||||
_LOGGER.warning("Could not connect to %s at %s %s", device_name, host, adb_log)
|
_LOGGER.warning(
|
||||||
|
"Could not connect to %s at %s %s", device_name, address, adb_log
|
||||||
|
)
|
||||||
raise PlatformNotReady
|
raise PlatformNotReady
|
||||||
|
|
||||||
if host in hass.data[ANDROIDTV_DOMAIN]:
|
device_args = [
|
||||||
_LOGGER.warning("Platform already setup on %s, skipping", host)
|
aftv,
|
||||||
else:
|
config[CONF_NAME],
|
||||||
if aftv.DEVICE_CLASS == DEVICE_ANDROIDTV:
|
config[CONF_APPS],
|
||||||
device = AndroidTVDevice(
|
config[CONF_GET_SOURCES],
|
||||||
aftv,
|
config.get(CONF_TURN_ON_COMMAND),
|
||||||
config[CONF_NAME],
|
config.get(CONF_TURN_OFF_COMMAND),
|
||||||
config[CONF_APPS],
|
]
|
||||||
config[CONF_GET_SOURCES],
|
|
||||||
config.get(CONF_TURN_ON_COMMAND),
|
|
||||||
config.get(CONF_TURN_OFF_COMMAND),
|
|
||||||
)
|
|
||||||
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],
|
|
||||||
config.get(CONF_TURN_ON_COMMAND),
|
|
||||||
config.get(CONF_TURN_OFF_COMMAND),
|
|
||||||
)
|
|
||||||
device_name = config[CONF_NAME] if CONF_NAME in config else "Fire TV"
|
|
||||||
|
|
||||||
add_entities([device])
|
if aftv.DEVICE_CLASS == DEVICE_ANDROIDTV:
|
||||||
_LOGGER.debug("Setup %s at %s %s", device_name, host, adb_log)
|
device = AndroidTVDevice(*device_args)
|
||||||
hass.data[ANDROIDTV_DOMAIN][host] = device
|
device_name = config.get(CONF_NAME, "Android TV")
|
||||||
|
else:
|
||||||
|
device = FireTVDevice(*device_args)
|
||||||
|
device_name = config.get(CONF_NAME, "Fire TV")
|
||||||
|
|
||||||
|
add_entities([device])
|
||||||
|
_LOGGER.debug("Setup %s at %s %s", device_name, address, adb_log)
|
||||||
|
hass.data[ANDROIDTV_DOMAIN][address] = device
|
||||||
|
|
||||||
if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND):
|
if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND):
|
||||||
return
|
return
|
||||||
|
|
||||||
def service_adb_command(service):
|
def service_adb_command(service):
|
||||||
"""Dispatch service calls to target entities."""
|
"""Dispatch service calls to target entities."""
|
||||||
cmd = service.data.get(ATTR_COMMAND)
|
cmd = service.data[ATTR_COMMAND]
|
||||||
entity_id = service.data.get(ATTR_ENTITY_ID)
|
entity_id = service.data[ATTR_ENTITY_ID]
|
||||||
target_devices = [
|
target_devices = [
|
||||||
dev
|
dev
|
||||||
for dev in hass.data[ANDROIDTV_DOMAIN].values()
|
for dev in hass.data[ANDROIDTV_DOMAIN].values()
|
||||||
@ -255,6 +275,52 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
schema=SERVICE_ADB_COMMAND_SCHEMA,
|
schema=SERVICE_ADB_COMMAND_SCHEMA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def service_download(service):
|
||||||
|
"""Download a file from your Android TV / Fire TV device to your Home Assistant instance."""
|
||||||
|
local_path = service.data[ATTR_LOCAL_PATH]
|
||||||
|
if not hass.config.is_allowed_path(local_path):
|
||||||
|
_LOGGER.warning("'%s' is not secure to load data from!", local_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
device_path = service.data[ATTR_DEVICE_PATH]
|
||||||
|
entity_id = service.data[ATTR_ENTITY_ID]
|
||||||
|
target_device = [
|
||||||
|
dev
|
||||||
|
for dev in hass.data[ANDROIDTV_DOMAIN].values()
|
||||||
|
if dev.entity_id in entity_id
|
||||||
|
][0]
|
||||||
|
|
||||||
|
target_device.adb_pull(local_path, device_path)
|
||||||
|
|
||||||
|
hass.services.register(
|
||||||
|
ANDROIDTV_DOMAIN,
|
||||||
|
SERVICE_DOWNLOAD,
|
||||||
|
service_download,
|
||||||
|
schema=SERVICE_DOWNLOAD_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
def service_upload(service):
|
||||||
|
"""Upload a file from your Home Assistant instance to an Android TV / Fire TV device."""
|
||||||
|
local_path = service.data[ATTR_LOCAL_PATH]
|
||||||
|
if not hass.config.is_allowed_path(local_path):
|
||||||
|
_LOGGER.warning("'%s' is not secure to load data from!", local_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
device_path = service.data[ATTR_DEVICE_PATH]
|
||||||
|
entity_id = service.data[ATTR_ENTITY_ID]
|
||||||
|
target_devices = [
|
||||||
|
dev
|
||||||
|
for dev in hass.data[ANDROIDTV_DOMAIN].values()
|
||||||
|
if dev.entity_id in entity_id
|
||||||
|
]
|
||||||
|
|
||||||
|
for target_device in target_devices:
|
||||||
|
target_device.adb_push(local_path, device_path)
|
||||||
|
|
||||||
|
hass.services.register(
|
||||||
|
ANDROIDTV_DOMAIN, SERVICE_UPLOAD, service_upload, schema=SERVICE_UPLOAD_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def adb_decorator(override_available=False):
|
def adb_decorator(override_available=False):
|
||||||
"""Wrap ADB methods and catch exceptions.
|
"""Wrap ADB methods and catch exceptions.
|
||||||
@ -274,6 +340,12 @@ def adb_decorator(override_available=False):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
|
except LockNotAcquiredException:
|
||||||
|
# If the ADB lock could not be acquired, skip this command
|
||||||
|
_LOGGER.info(
|
||||||
|
"ADB command not executed because the connection is currently in use"
|
||||||
|
)
|
||||||
|
return
|
||||||
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-"
|
||||||
@ -465,6 +537,16 @@ class ADBDevice(MediaPlayerDevice):
|
|||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
return self._adb_response
|
return self._adb_response
|
||||||
|
|
||||||
|
@adb_decorator()
|
||||||
|
def adb_pull(self, local_path, device_path):
|
||||||
|
"""Download a file from your Android TV / Fire TV device to your Home Assistant instance."""
|
||||||
|
self.aftv.adb_pull(local_path, device_path)
|
||||||
|
|
||||||
|
@adb_decorator()
|
||||||
|
def adb_push(self, local_path, device_path):
|
||||||
|
"""Upload a file from your Home Assistant instance to an Android TV / Fire TV device."""
|
||||||
|
self.aftv.adb_push(local_path, device_path)
|
||||||
|
|
||||||
|
|
||||||
class AndroidTVDevice(ADBDevice):
|
class AndroidTVDevice(ADBDevice):
|
||||||
"""Representation of an Android TV device."""
|
"""Representation of an Android TV device."""
|
||||||
|
@ -9,3 +9,27 @@ adb_command:
|
|||||||
command:
|
command:
|
||||||
description: Either a key command or an ADB shell command.
|
description: Either a key command or an ADB shell command.
|
||||||
example: 'HOME'
|
example: 'HOME'
|
||||||
|
download:
|
||||||
|
description: Download a file from your Android TV / Fire TV device to your Home Assistant instance.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of Android TV / Fire TV entity.
|
||||||
|
example: 'media_player.android_tv_living_room'
|
||||||
|
device_path:
|
||||||
|
description: The filepath on the Android TV / Fire TV device.
|
||||||
|
example: '/storage/emulated/0/Download/example.txt'
|
||||||
|
local_path:
|
||||||
|
description: The filepath on your Home Assistant instance.
|
||||||
|
example: '/config/example.txt'
|
||||||
|
upload:
|
||||||
|
description: Upload a file from your Home Assistant instance 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'
|
||||||
|
device_path:
|
||||||
|
description: The filepath on the Android TV / Fire TV device.
|
||||||
|
example: '/storage/emulated/0/Download/example.txt'
|
||||||
|
local_path:
|
||||||
|
description: The filepath on your Home Assistant instance.
|
||||||
|
example: '/config/example.txt'
|
||||||
|
@ -220,7 +220,7 @@ ambiclimate==0.2.1
|
|||||||
amcrest==1.5.3
|
amcrest==1.5.3
|
||||||
|
|
||||||
# homeassistant.components.androidtv
|
# homeassistant.components.androidtv
|
||||||
androidtv==0.0.36
|
androidtv==0.0.37
|
||||||
|
|
||||||
# homeassistant.components.anel_pwrctrl
|
# homeassistant.components.anel_pwrctrl
|
||||||
anel_pwrctrl-homeassistant==0.0.1.dev2
|
anel_pwrctrl-homeassistant==0.0.1.dev2
|
||||||
|
@ -87,7 +87,7 @@ airly==0.0.2
|
|||||||
ambiclimate==0.2.1
|
ambiclimate==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.androidtv
|
# homeassistant.components.androidtv
|
||||||
androidtv==0.0.36
|
androidtv==0.0.37
|
||||||
|
|
||||||
# homeassistant.components.apns
|
# homeassistant.components.apns
|
||||||
apns2==0.3.0
|
apns2==0.3.0
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
"""The tests for the androidtv platform."""
|
"""The tests for the androidtv platform."""
|
||||||
import logging
|
import logging
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from androidtv.exceptions import LockNotAcquiredException
|
||||||
|
|
||||||
from homeassistant.components.androidtv.media_player import (
|
from homeassistant.components.androidtv.media_player import (
|
||||||
ANDROIDTV_DOMAIN,
|
ANDROIDTV_DOMAIN,
|
||||||
|
ATTR_COMMAND,
|
||||||
|
ATTR_DEVICE_PATH,
|
||||||
|
ATTR_LOCAL_PATH,
|
||||||
CONF_ADB_SERVER_IP,
|
CONF_ADB_SERVER_IP,
|
||||||
CONF_ADBKEY,
|
CONF_ADBKEY,
|
||||||
CONF_APPS,
|
CONF_APPS,
|
||||||
|
KEYS,
|
||||||
|
SERVICE_ADB_COMMAND,
|
||||||
|
SERVICE_DOWNLOAD,
|
||||||
|
SERVICE_UPLOAD,
|
||||||
)
|
)
|
||||||
from homeassistant.components.media_player.const import (
|
from homeassistant.components.media_player.const import (
|
||||||
ATTR_INPUT_SOURCE,
|
ATTR_INPUT_SOURCE,
|
||||||
@ -70,7 +80,7 @@ CONFIG_FIRETV_ADB_SERVER = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _setup(hass, config):
|
def _setup(config):
|
||||||
"""Perform common setup tasks for the tests."""
|
"""Perform common setup tasks for the tests."""
|
||||||
if CONF_ADB_SERVER_IP not in config[DOMAIN]:
|
if CONF_ADB_SERVER_IP not in config[DOMAIN]:
|
||||||
patch_key = "python"
|
patch_key = "python"
|
||||||
@ -93,7 +103,7 @@ async def _test_reconnect(hass, caplog, config):
|
|||||||
|
|
||||||
https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html
|
https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html
|
||||||
"""
|
"""
|
||||||
patch_key, entity_id = _setup(hass, config)
|
patch_key, entity_id = _setup(config)
|
||||||
|
|
||||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||||
patch_key
|
patch_key
|
||||||
@ -164,7 +174,7 @@ 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.
|
||||||
"""
|
"""
|
||||||
patch_key, entity_id = _setup(hass, config)
|
patch_key, entity_id = _setup(config)
|
||||||
|
|
||||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||||
patch_key
|
patch_key
|
||||||
@ -272,7 +282,7 @@ async def test_setup_with_adbkey(hass):
|
|||||||
"""Test that setup succeeds when using an ADB key."""
|
"""Test that setup succeeds when using an ADB key."""
|
||||||
config = CONFIG_ANDROIDTV_PYTHON_ADB.copy()
|
config = CONFIG_ANDROIDTV_PYTHON_ADB.copy()
|
||||||
config[DOMAIN][CONF_ADBKEY] = hass.config.path("user_provided_adbkey")
|
config[DOMAIN][CONF_ADBKEY] = hass.config.path("user_provided_adbkey")
|
||||||
patch_key, entity_id = _setup(hass, config)
|
patch_key, entity_id = _setup(config)
|
||||||
|
|
||||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||||
patch_key
|
patch_key
|
||||||
@ -290,7 +300,7 @@ async def _test_sources(hass, config0):
|
|||||||
"""Test that sources (i.e., apps) are handled correctly for Android TV and Fire TV devices."""
|
"""Test that sources (i.e., apps) are handled correctly for Android TV and Fire TV devices."""
|
||||||
config = config0.copy()
|
config = config0.copy()
|
||||||
config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"}
|
config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"}
|
||||||
patch_key, entity_id = _setup(hass, config)
|
patch_key, entity_id = _setup(config)
|
||||||
|
|
||||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||||
patch_key
|
patch_key
|
||||||
@ -362,7 +372,7 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch)
|
|||||||
"""Test that the methods for launching and stopping apps are called correctly when selecting a source."""
|
"""Test that the methods for launching and stopping apps are called correctly when selecting a source."""
|
||||||
config = config0.copy()
|
config = config0.copy()
|
||||||
config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"}
|
config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"}
|
||||||
patch_key, entity_id = _setup(hass, config)
|
patch_key, entity_id = _setup(config)
|
||||||
|
|
||||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||||
patch_key
|
patch_key
|
||||||
@ -519,7 +529,7 @@ async def test_firetv_select_source_stop_app_id_no_name(hass):
|
|||||||
|
|
||||||
async def _test_setup_fail(hass, config):
|
async def _test_setup_fail(hass, config):
|
||||||
"""Test that the entity is not created when the ADB connection is not established."""
|
"""Test that the entity is not created when the ADB connection is not established."""
|
||||||
patch_key, entity_id = _setup(hass, config)
|
patch_key, entity_id = _setup(config)
|
||||||
|
|
||||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(False)[
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(False)[
|
||||||
patch_key
|
patch_key
|
||||||
@ -569,14 +579,216 @@ async def test_setup_two_devices(hass):
|
|||||||
|
|
||||||
async def test_setup_same_device_twice(hass):
|
async def test_setup_same_device_twice(hass):
|
||||||
"""Test that setup succeeds with a duplicated config entry."""
|
"""Test that setup succeeds with a duplicated config entry."""
|
||||||
|
patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER)
|
||||||
|
|
||||||
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||||
|
patch_key
|
||||||
|
], patchers.patch_shell("")[patch_key]:
|
||||||
|
assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is not None
|
||||||
|
|
||||||
|
assert hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND)
|
||||||
|
|
||||||
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||||
|
patch_key
|
||||||
|
], patchers.patch_shell("")[patch_key]:
|
||||||
|
assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_adb_command(hass):
|
||||||
|
"""Test sending a command via the `androidtv.adb_command` service."""
|
||||||
|
patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER)
|
||||||
|
command = "test command"
|
||||||
|
response = "test response"
|
||||||
|
|
||||||
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||||
|
patch_key
|
||||||
|
], patchers.patch_shell("")[patch_key]:
|
||||||
|
assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"androidtv.basetv.BaseTV.adb_shell", return_value=response
|
||||||
|
) as patch_shell:
|
||||||
|
await hass.services.async_call(
|
||||||
|
ANDROIDTV_DOMAIN,
|
||||||
|
SERVICE_ADB_COMMAND,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: command},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
patch_shell.assert_called_with(command)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes["adb_response"] == response
|
||||||
|
|
||||||
|
|
||||||
|
async def test_adb_command_key(hass):
|
||||||
|
"""Test sending a key command via the `androidtv.adb_command` service."""
|
||||||
patch_key = "server"
|
patch_key = "server"
|
||||||
|
entity_id = "media_player.android_tv"
|
||||||
|
command = "HOME"
|
||||||
|
response = None
|
||||||
|
|
||||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||||
patch_key
|
patch_key
|
||||||
], patchers.patch_shell("")[patch_key]:
|
], patchers.patch_shell("")[patch_key]:
|
||||||
assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER)
|
assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"androidtv.basetv.BaseTV.adb_shell", return_value=response
|
||||||
|
) as patch_shell:
|
||||||
|
await hass.services.async_call(
|
||||||
|
ANDROIDTV_DOMAIN,
|
||||||
|
SERVICE_ADB_COMMAND,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: command},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
patch_shell.assert_called_with(f"input keyevent {KEYS[command]}")
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes["adb_response"] is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_adb_command_get_properties(hass):
|
||||||
|
"""Test sending the "GET_PROPERTIES" command via the `androidtv.adb_command` service."""
|
||||||
|
patch_key = "server"
|
||||||
|
entity_id = "media_player.android_tv"
|
||||||
|
command = "GET_PROPERTIES"
|
||||||
|
response = {"test key": "test value"}
|
||||||
|
|
||||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||||
patch_key
|
patch_key
|
||||||
], patchers.patch_shell("")[patch_key]:
|
], patchers.patch_shell("")[patch_key]:
|
||||||
assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER)
|
assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"androidtv.androidtv.AndroidTV.get_properties_dict", return_value=response
|
||||||
|
) as patch_get_props:
|
||||||
|
await hass.services.async_call(
|
||||||
|
ANDROIDTV_DOMAIN,
|
||||||
|
SERVICE_ADB_COMMAND,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: command},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
patch_get_props.assert_called()
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes["adb_response"] == str(response)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_lock_not_acquired(hass):
|
||||||
|
"""Test that the state does not get updated when a `LockNotAcquiredException` is raised."""
|
||||||
|
patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER)
|
||||||
|
|
||||||
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||||
|
patch_key
|
||||||
|
], patchers.patch_shell("")[patch_key]:
|
||||||
|
assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER)
|
||||||
|
|
||||||
|
with patchers.patch_shell("")[patch_key]:
|
||||||
|
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
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"androidtv.androidtv.AndroidTV.update", side_effect=LockNotAcquiredException
|
||||||
|
):
|
||||||
|
with patchers.patch_shell("1")[patch_key]:
|
||||||
|
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
|
||||||
|
|
||||||
|
with patchers.patch_shell("1")[patch_key]:
|
||||||
|
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_IDLE
|
||||||
|
|
||||||
|
|
||||||
|
async def test_download(hass):
|
||||||
|
"""Test the `androidtv.download` service."""
|
||||||
|
patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER)
|
||||||
|
device_path = "device/path"
|
||||||
|
local_path = "local/path"
|
||||||
|
|
||||||
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||||
|
patch_key
|
||||||
|
], patchers.patch_shell("")[patch_key]:
|
||||||
|
assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER)
|
||||||
|
|
||||||
|
# Failed download because path is not whitelisted
|
||||||
|
with patch("androidtv.basetv.BaseTV.adb_pull") as patch_pull:
|
||||||
|
await hass.services.async_call(
|
||||||
|
ANDROIDTV_DOMAIN,
|
||||||
|
SERVICE_DOWNLOAD,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_DEVICE_PATH: device_path,
|
||||||
|
ATTR_LOCAL_PATH: local_path,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
patch_pull.assert_not_called()
|
||||||
|
|
||||||
|
# Successful download
|
||||||
|
with patch("androidtv.basetv.BaseTV.adb_pull") as patch_pull, patch.object(
|
||||||
|
hass.config, "is_allowed_path", return_value=True
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
ANDROIDTV_DOMAIN,
|
||||||
|
SERVICE_DOWNLOAD,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_DEVICE_PATH: device_path,
|
||||||
|
ATTR_LOCAL_PATH: local_path,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
patch_pull.assert_called_with(local_path, device_path)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_upload(hass):
|
||||||
|
"""Test the `androidtv.upload` service."""
|
||||||
|
patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER)
|
||||||
|
device_path = "device/path"
|
||||||
|
local_path = "local/path"
|
||||||
|
|
||||||
|
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||||
|
patch_key
|
||||||
|
], patchers.patch_shell("")[patch_key]:
|
||||||
|
assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER)
|
||||||
|
|
||||||
|
# Failed upload because path is not whitelisted
|
||||||
|
with patch("androidtv.basetv.BaseTV.adb_push") as patch_push:
|
||||||
|
await hass.services.async_call(
|
||||||
|
ANDROIDTV_DOMAIN,
|
||||||
|
SERVICE_UPLOAD,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_DEVICE_PATH: device_path,
|
||||||
|
ATTR_LOCAL_PATH: local_path,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
patch_push.assert_not_called()
|
||||||
|
|
||||||
|
# Successful upload
|
||||||
|
with patch("androidtv.basetv.BaseTV.adb_push") as patch_push, patch.object(
|
||||||
|
hass.config, "is_allowed_path", return_value=True
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
ANDROIDTV_DOMAIN,
|
||||||
|
SERVICE_UPLOAD,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_DEVICE_PATH: device_path,
|
||||||
|
ATTR_LOCAL_PATH: local_path,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
patch_push.assert_called_with(local_path, device_path)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user