"""Platform for Husqvarna Automower base entity.""" from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine import functools import logging from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, overload from aioautomower.exceptions import ApiError from aioautomower.model import MowerActivities, MowerAttributes, MowerStates, WorkArea from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import AutomowerDataUpdateCoordinator from .const import DOMAIN, EXECUTION_TIME_DELAY _LOGGER = logging.getLogger(__name__) ERROR_ACTIVITIES = ( MowerActivities.STOPPED_IN_GARDEN, MowerActivities.UNKNOWN, MowerActivities.NOT_APPLICABLE, ) ERROR_STATES = [ MowerStates.FATAL_ERROR, MowerStates.ERROR, MowerStates.ERROR_AT_POWER_UP, MowerStates.NOT_APPLICABLE, MowerStates.UNKNOWN, MowerStates.STOPPED, MowerStates.OFF, ] _Entity = TypeVar("_Entity", bound="AutomowerBaseEntity") _P = ParamSpec("_P") @overload def handle_sending_exception( _func: Callable[Concatenate[_Entity, _P], Coroutine[Any, Any, Any]], ) -> Callable[Concatenate[_Entity, _P], Coroutine[Any, Any, None]]: ... @overload def handle_sending_exception( *, poll_after_sending: bool = False, ) -> Callable[ [Callable[Concatenate[_Entity, _P], Coroutine[Any, Any, Any]]], Callable[Concatenate[_Entity, _P], Coroutine[Any, Any, None]], ]: ... def handle_sending_exception( _func: Callable[Concatenate[_Entity, _P], Coroutine[Any, Any, Any]] | None = None, *, poll_after_sending: bool = False, ) -> ( Callable[Concatenate[_Entity, _P], Coroutine[Any, Any, None]] | Callable[ [Callable[Concatenate[_Entity, _P], Coroutine[Any, Any, Any]]], Callable[Concatenate[_Entity, _P], Coroutine[Any, Any, None]], ] ): """Handle exceptions while sending a command and optionally refresh coordinator.""" def decorator( func: Callable[Concatenate[_Entity, _P], Coroutine[Any, Any, Any]], ) -> Callable[Concatenate[_Entity, _P], Coroutine[Any, Any, None]]: @functools.wraps(func) async def wrapper(self: _Entity, *args: _P.args, **kwargs: _P.kwargs) -> None: try: await func(self, *args, **kwargs) except ApiError as exception: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="command_send_failed", translation_placeholders={"exception": str(exception)}, ) from exception else: if poll_after_sending: # As there are no updates from the websocket for this attribute, # we need to wait until the command is executed and then poll the API. await asyncio.sleep(EXECUTION_TIME_DELAY) await self.coordinator.async_request_refresh() return wrapper if _func is None: # call with brackets: @handle_sending_exception(...) return decorator # call without brackets: @handle_sending_exception return decorator(_func) @callback def _work_area_translation_key(work_area_id: int, key: str) -> str: """Return the translation key.""" if work_area_id == 0: return f"my_lawn_{key}" return f"work_area_{key}" class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]): """Defining the Automower base Entity.""" _attr_has_entity_name = True def __init__( self, mower_id: str, coordinator: AutomowerDataUpdateCoordinator, ) -> None: """Initialize AutomowerEntity.""" super().__init__(coordinator) self.mower_id = mower_id model_witout_manufacturer = self.mower_attributes.system.model.removeprefix( "Husqvarna " ).removeprefix("HUSQVARNA ") parts = model_witout_manufacturer.split(maxsplit=1) self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, mower_id)}, manufacturer="Husqvarna", model=parts[0].capitalize().removesuffix("®"), model_id=parts[1], name=self.mower_attributes.system.name, serial_number=self.mower_attributes.system.serial_number, suggested_area="Garden", ) @property def mower_attributes(self) -> MowerAttributes: """Get the mower attributes of the current mower.""" return self.coordinator.data[self.mower_id] @property def available(self) -> bool: """Return True if the device is available.""" return super().available and self.mower_id in self.coordinator.data class AutomowerControlEntity(AutomowerBaseEntity): """Replies available when the mower is connected.""" @property def available(self) -> bool: """Return True if the device is available.""" return ( super().available and self.mower_attributes.metadata.connected and self.mower_attributes.mower.state != MowerStates.OFF ) class WorkAreaAvailableEntity(AutomowerControlEntity): """Base entity for work areas.""" def __init__( self, mower_id: str, coordinator: AutomowerDataUpdateCoordinator, work_area_id: int, ) -> None: """Initialize AutomowerEntity.""" super().__init__(mower_id, coordinator) self.work_area_id = work_area_id @property def work_areas(self) -> dict[int, WorkArea]: """Get the work areas from the mower attributes.""" if TYPE_CHECKING: assert self.mower_attributes.work_areas is not None return self.mower_attributes.work_areas @property def work_area_attributes(self) -> WorkArea: """Get the work area attributes of the current work area.""" return self.work_areas[self.work_area_id] @property def available(self) -> bool: """Return True if the work area is available and the mower has no errors.""" return super().available and self.work_area_id in self.work_areas class WorkAreaControlEntity(WorkAreaAvailableEntity, AutomowerControlEntity): """Base entity for work areas with control function."""