Limit AndroidTV screencap calls (#96485)
This commit is contained in:
parent
345df715d6
commit
2cfc11d4b9
3 changed files with 99 additions and 32 deletions
|
@ -2,8 +2,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
import functools
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Any, Concatenate, ParamSpec, TypeVar
|
||||
|
||||
|
@ -35,6 +36,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
|||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from . import ADB_PYTHON_EXCEPTIONS, ADB_TCP_EXCEPTIONS, get_androidtv_mac
|
||||
from .const import (
|
||||
|
@ -65,6 +67,8 @@ ATTR_DEVICE_PATH = "device_path"
|
|||
ATTR_HDMI_INPUT = "hdmi_input"
|
||||
ATTR_LOCAL_PATH = "local_path"
|
||||
|
||||
MIN_TIME_BETWEEN_SCREENCAPS = timedelta(seconds=60)
|
||||
|
||||
SERVICE_ADB_COMMAND = "adb_command"
|
||||
SERVICE_DOWNLOAD = "download"
|
||||
SERVICE_LEARN_SENDEVENT = "learn_sendevent"
|
||||
|
@ -228,6 +232,9 @@ class ADBDevice(MediaPlayerEntity):
|
|||
self._entry_id = entry_id
|
||||
self._entry_data = entry_data
|
||||
|
||||
self._media_image: tuple[bytes | None, str | None] = None, None
|
||||
self._attr_media_image_hash = None
|
||||
|
||||
info = aftv.device_properties
|
||||
model = info.get(ATTR_MODEL)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
|
@ -304,34 +311,39 @@ class ADBDevice(MediaPlayerEntity):
|
|||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def media_image_hash(self) -> str | None:
|
||||
"""Hash value for media image."""
|
||||
return f"{datetime.now().timestamp()}" if self._screencap else None
|
||||
|
||||
@adb_decorator()
|
||||
async def _adb_screencap(self) -> bytes | None:
|
||||
"""Take a screen capture from the device."""
|
||||
return await self.aftv.adb_screencap()
|
||||
|
||||
async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
|
||||
"""Fetch current playing image."""
|
||||
async def _async_get_screencap(self, prev_app_id: str | None = None) -> None:
|
||||
"""Take a screen capture from the device when enabled."""
|
||||
if (
|
||||
not self._screencap
|
||||
or self.state in {MediaPlayerState.OFF, None}
|
||||
or not self.available
|
||||
):
|
||||
return None, None
|
||||
self._media_image = None, None
|
||||
self._attr_media_image_hash = None
|
||||
else:
|
||||
force: bool = prev_app_id is not None
|
||||
if force:
|
||||
force = prev_app_id != self._attr_app_id
|
||||
await self._adb_get_screencap(no_throttle=force)
|
||||
|
||||
media_data = await self._adb_screencap()
|
||||
if media_data:
|
||||
return media_data, "image/png"
|
||||
@Throttle(MIN_TIME_BETWEEN_SCREENCAPS)
|
||||
async def _adb_get_screencap(self, **kwargs) -> None:
|
||||
"""Take a screen capture from the device every 60 seconds."""
|
||||
if media_data := await self._adb_screencap():
|
||||
self._media_image = media_data, "image/png"
|
||||
self._attr_media_image_hash = hashlib.sha256(media_data).hexdigest()[:16]
|
||||
else:
|
||||
self._media_image = None, None
|
||||
self._attr_media_image_hash = None
|
||||
|
||||
# If an exception occurred and the device is no longer available, write the state
|
||||
if not self.available:
|
||||
self.async_write_ha_state()
|
||||
|
||||
return None, None
|
||||
async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
|
||||
"""Fetch current playing image."""
|
||||
return self._media_image
|
||||
|
||||
@adb_decorator()
|
||||
async def async_media_play(self) -> None:
|
||||
|
@ -485,6 +497,7 @@ class AndroidTVDevice(ADBDevice):
|
|||
if not self.available:
|
||||
return
|
||||
|
||||
prev_app_id = self._attr_app_id
|
||||
# Get the updated state and attributes.
|
||||
(
|
||||
state,
|
||||
|
@ -514,6 +527,8 @@ class AndroidTVDevice(ADBDevice):
|
|||
else:
|
||||
self._attr_source_list = None
|
||||
|
||||
await self._async_get_screencap(prev_app_id)
|
||||
|
||||
@adb_decorator()
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Send stop command."""
|
||||
|
@ -575,6 +590,7 @@ class FireTVDevice(ADBDevice):
|
|||
if not self.available:
|
||||
return
|
||||
|
||||
prev_app_id = self._attr_app_id
|
||||
# Get the `state`, `current_app`, `running_apps` and `hdmi_input`.
|
||||
(
|
||||
state,
|
||||
|
@ -601,6 +617,8 @@ class FireTVDevice(ADBDevice):
|
|||
else:
|
||||
self._attr_source_list = None
|
||||
|
||||
await self._async_get_screencap(prev_app_id)
|
||||
|
||||
@adb_decorator()
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Send stop (back) command."""
|
||||
|
|
|
@ -185,6 +185,10 @@ def isfile(filepath):
|
|||
return filepath.endswith("adbkey")
|
||||
|
||||
|
||||
PATCH_SCREENCAP = patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap",
|
||||
return_value=b"image",
|
||||
)
|
||||
PATCH_SETUP_ENTRY = patch(
|
||||
"homeassistant.components.androidtv.async_setup_entry",
|
||||
return_value=True,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""The tests for the androidtv platform."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
from unittest.mock import Mock, patch
|
||||
|
@ -70,10 +71,11 @@ from homeassistant.const import (
|
|||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import patchers
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
|
@ -263,7 +265,7 @@ async def test_reconnect(
|
|||
caplog.set_level(logging.DEBUG)
|
||||
with patchers.patch_connect(True)[patch_key], patchers.patch_shell(
|
||||
SHELL_RESPONSE_STANDBY
|
||||
)[patch_key]:
|
||||
)[patch_key], patchers.PATCH_SCREENCAP:
|
||||
await async_update_entity(hass, entity_id)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
|
@ -751,7 +753,9 @@ async def test_update_lock_not_acquired(hass: HomeAssistant) -> None:
|
|||
assert state is not None
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[
|
||||
patch_key
|
||||
], patchers.PATCH_SCREENCAP:
|
||||
await async_update_entity(hass, entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
|
@ -890,8 +894,11 @@ async def test_get_image_http(
|
|||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell("11")[patch_key]:
|
||||
with patchers.patch_shell("11")[
|
||||
patch_key
|
||||
], patchers.PATCH_SCREENCAP as patch_screen_cap:
|
||||
await async_update_entity(hass, entity_id)
|
||||
patch_screen_cap.assert_called()
|
||||
|
||||
media_player_name = "media_player." + slugify(
|
||||
CONFIG_ANDROID_DEFAULT[TEST_ENTITY_NAME]
|
||||
|
@ -901,21 +908,53 @@ async def test_get_image_http(
|
|||
|
||||
client = await hass_client_no_auth()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", return_value=b"image"
|
||||
):
|
||||
resp = await client.get(state.attributes["entity_picture"])
|
||||
content = await resp.read()
|
||||
|
||||
resp = await client.get(state.attributes["entity_picture"])
|
||||
content = await resp.read()
|
||||
assert content == b"image"
|
||||
|
||||
with patch(
|
||||
next_update = utcnow() + timedelta(seconds=30)
|
||||
with patchers.patch_shell("11")[
|
||||
patch_key
|
||||
], patchers.PATCH_SCREENCAP as patch_screen_cap, patch(
|
||||
"homeassistant.util.utcnow", return_value=next_update
|
||||
):
|
||||
async_fire_time_changed(hass, next_update, True)
|
||||
await hass.async_block_till_done()
|
||||
patch_screen_cap.assert_not_called()
|
||||
|
||||
next_update = utcnow() + timedelta(seconds=60)
|
||||
with patchers.patch_shell("11")[
|
||||
patch_key
|
||||
], patchers.PATCH_SCREENCAP as patch_screen_cap, patch(
|
||||
"homeassistant.util.utcnow", return_value=next_update
|
||||
):
|
||||
async_fire_time_changed(hass, next_update, True)
|
||||
await hass.async_block_till_done()
|
||||
patch_screen_cap.assert_called()
|
||||
|
||||
|
||||
async def test_get_image_http_fail(hass: HomeAssistant) -> None:
|
||||
"""Test taking a screen capture fail."""
|
||||
|
||||
patch_key, entity_id, config_entry = _setup(CONFIG_ANDROID_DEFAULT)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patchers.patch_connect(True)[patch_key], patchers.patch_shell(
|
||||
SHELL_RESPONSE_OFF
|
||||
)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell("11")[patch_key], patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap",
|
||||
side_effect=ConnectionResetError,
|
||||
):
|
||||
resp = await client.get(state.attributes["entity_picture"])
|
||||
await async_update_entity(hass, entity_id)
|
||||
|
||||
# The device is unavailable, but getting the media image did not cause an exception
|
||||
media_player_name = "media_player." + slugify(
|
||||
CONFIG_ANDROID_DEFAULT[TEST_ENTITY_NAME]
|
||||
)
|
||||
state = hass.states.get(media_player_name)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
@ -986,7 +1025,9 @@ async def test_services_androidtv(hass: HomeAssistant) -> None:
|
|||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[
|
||||
patch_key
|
||||
], patchers.PATCH_SCREENCAP:
|
||||
await _test_service(
|
||||
hass, entity_id, SERVICE_MEDIA_NEXT_TRACK, "media_next_track"
|
||||
)
|
||||
|
@ -1034,7 +1075,9 @@ async def test_services_firetv(hass: HomeAssistant) -> None:
|
|||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[
|
||||
patch_key
|
||||
], patchers.PATCH_SCREENCAP:
|
||||
await _test_service(hass, entity_id, SERVICE_MEDIA_STOP, "back")
|
||||
await _test_service(hass, entity_id, SERVICE_TURN_OFF, "adb_shell")
|
||||
await _test_service(hass, entity_id, SERVICE_TURN_ON, "adb_shell")
|
||||
|
@ -1050,7 +1093,9 @@ async def test_volume_mute(hass: HomeAssistant) -> None:
|
|||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[
|
||||
patch_key
|
||||
], patchers.PATCH_SCREENCAP:
|
||||
service_data = {ATTR_ENTITY_ID: entity_id, ATTR_MEDIA_VOLUME_MUTED: True}
|
||||
with patch(
|
||||
"androidtv.androidtv.androidtv_async.AndroidTVAsync.mute_volume",
|
||||
|
|
Loading…
Add table
Reference in a new issue