"""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.info( ( "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