core/homeassistant/components/androidtv/entity.py

156 lines
5.4 KiB
Python

"""Base AndroidTV Entity."""
from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
import functools
import logging
from typing import Any, Concatenate
from androidtv.exceptions import LockNotAcquiredException
from homeassistant.const import (
ATTR_CONNECTIONS,
ATTR_IDENTIFIERS,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_SW_VERSION,
CONF_HOST,
CONF_NAME,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import Entity
from . import (
ADB_PYTHON_EXCEPTIONS,
ADB_TCP_EXCEPTIONS,
AndroidTVConfigEntry,
get_androidtv_mac,
)
from .const import DEVICE_ANDROIDTV, DOMAIN
PREFIX_ANDROIDTV = "Android TV"
PREFIX_FIRETV = "Fire TV"
_LOGGER = logging.getLogger(__name__)
type _FuncType[_T, **_P, _R] = Callable[Concatenate[_T, _P], Awaitable[_R]]
type _ReturnFuncType[_T, **_P, _R] = Callable[
Concatenate[_T, _P], Coroutine[Any, Any, _R | None]
]
def adb_decorator[_ADBDeviceT: AndroidTVEntity, **_P, _R](
override_available: bool = False,
) -> Callable[[_FuncType[_ADBDeviceT, _P, _R]], _ReturnFuncType[_ADBDeviceT, _P, _R]]:
"""Wrap ADB methods and catch exceptions.
Allows for overriding the available status of the ADB connection via the
`override_available` parameter.
"""
def _adb_decorator(
func: _FuncType[_ADBDeviceT, _P, _R],
) -> _ReturnFuncType[_ADBDeviceT, _P, _R]:
"""Wrap the provided ADB method and catch exceptions."""
@functools.wraps(func)
async def _adb_exception_catcher(
self: _ADBDeviceT, *args: _P.args, **kwargs: _P.kwargs
) -> _R | None:
"""Call an ADB-related method and catch exceptions."""
if not self.available and not override_available:
return None
try:
return await func(self, *args, **kwargs)
except LockNotAcquiredException:
# If the ADB lock could not be acquired, skip this command
_LOGGER.debug(
(
"ADB command %s not executed because the connection is"
" currently in use"
),
func.__name__,
)
return None
except self.exceptions as err:
if self.available:
_LOGGER.error(
(
"Failed to execute an ADB command. ADB connection re-"
"establishing attempt in the next update. Error: %s"
),
err,
)
await self.aftv.adb_close()
self._attr_available = False
return None
except ServiceValidationError:
# Service validation error is thrown because raised by remote services
raise
except Exception as err: # noqa: BLE001
# An unforeseen exception occurred. Close the ADB connection so that
# it doesn't happen over and over again.
if self.available:
_LOGGER.error(
(
"Unexpected exception executing an ADB command. ADB connection"
" re-establishing attempt in the next update. Error: %s"
),
err,
)
await self.aftv.adb_close()
self._attr_available = False
return None
return _adb_exception_catcher
return _adb_decorator
class AndroidTVEntity(Entity):
"""Defines a base AndroidTV entity."""
_attr_has_entity_name = True
def __init__(self, entry: AndroidTVConfigEntry) -> None:
"""Initialize the AndroidTV base entity."""
self.aftv = entry.runtime_data.aftv
self._attr_unique_id = entry.unique_id
self._entry_runtime_data = entry.runtime_data
device_class = self.aftv.DEVICE_CLASS
device_type = (
PREFIX_ANDROIDTV if device_class == DEVICE_ANDROIDTV else PREFIX_FIRETV
)
# CONF_NAME may be present in entry.data for configuration imported from YAML
device_name = entry.data.get(
CONF_NAME, f"{device_type} {entry.data[CONF_HOST]}"
)
info = self.aftv.device_properties
model = info.get(ATTR_MODEL)
self._attr_device_info = DeviceInfo(
model=f"{model} ({device_type})" if model else device_type,
name=device_name,
)
if self.unique_id:
self._attr_device_info[ATTR_IDENTIFIERS] = {(DOMAIN, self.unique_id)}
if manufacturer := info.get(ATTR_MANUFACTURER):
self._attr_device_info[ATTR_MANUFACTURER] = manufacturer
if sw_version := info.get(ATTR_SW_VERSION):
self._attr_device_info[ATTR_SW_VERSION] = sw_version
if mac := get_androidtv_mac(info):
self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)}
# ADB exceptions to catch
if not self.aftv.adb_server_ip:
# Using "adb_shell" (Python ADB implementation)
self.exceptions = ADB_PYTHON_EXCEPTIONS
else:
# Using "pure-python-adb" (communicate with ADB server)
self.exceptions = ADB_TCP_EXCEPTIONS