"""Litter-Robot entities for common data and methods.""" from __future__ import annotations from datetime import time import logging from types import MethodType from typing import Any from pylitterbot import Robot from pylitterbot.exceptions import InvalidCommandException from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.dt as dt_util from .const import DOMAIN from .hub import LitterRobotHub _LOGGER = logging.getLogger(__name__) REFRESH_WAIT_TIME_SECONDS = 8 class LitterRobotEntity(CoordinatorEntity): """Generic Litter-Robot entity representing common data and methods.""" def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(hub.coordinator) self.robot = robot self.entity_type = entity_type self.hub = hub @property def name(self) -> str: """Return the name of this entity.""" return f"{self.robot.name} {self.entity_type}" @property def unique_id(self) -> str: """Return a unique ID.""" return f"{self.robot.serial}-{self.entity_type}" @property def device_info(self) -> DeviceInfo: """Return the device information for a Litter-Robot.""" return DeviceInfo( identifiers={(DOMAIN, self.robot.serial)}, manufacturer="Litter-Robot", model=self.robot.model, name=self.robot.name, ) class LitterRobotControlEntity(LitterRobotEntity): """A Litter-Robot entity that can control the unit.""" def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) self._refresh_callback: CALLBACK_TYPE | None = None async def perform_action_and_refresh( self, action: MethodType, *args: Any, **kwargs: Any ) -> bool: """Perform an action and initiates a refresh of the robot data after a few seconds.""" success = False try: success = await action(*args, **kwargs) except InvalidCommandException as ex: # pragma: no cover # this exception should only occur if the underlying API for commands changes _LOGGER.error(ex) success = False if success: self.async_cancel_refresh_callback() self._refresh_callback = async_call_later( self.hass, REFRESH_WAIT_TIME_SECONDS, self.async_call_later_callback ) return success async def async_call_later_callback(self, *_) -> None: """Perform refresh request on callback.""" self._refresh_callback = None await self.coordinator.async_request_refresh() async def async_will_remove_from_hass(self) -> None: """Cancel refresh callback when entity is being removed from hass.""" self.async_cancel_refresh_callback() @callback def async_cancel_refresh_callback(self): """Clear the refresh callback if it has not already fired.""" if self._refresh_callback is not None: self._refresh_callback() self._refresh_callback = None @staticmethod def parse_time_at_default_timezone(time_str: str | None) -> time | None: """Parse a time string and add default timezone.""" if time_str is None: return None if (parsed_time := dt_util.parse_time(time_str)) is None: return None return ( dt_util.start_of_local_day() .replace( hour=parsed_time.hour, minute=parsed_time.minute, second=parsed_time.second, ) .timetz() ) class LitterRobotConfigEntity(LitterRobotControlEntity): """A Litter-Robot entity that can control configuration of the unit.""" _attr_entity_category = EntityCategory.CONFIG def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) self._assumed_state: Any = None async def perform_action_and_assume_state( self, action: MethodType, assumed_state: Any ) -> None: """Perform an action and assume the state passed in if call is successful.""" if await self.perform_action_and_refresh(action, assumed_state): self._assumed_state = assumed_state self.async_write_ha_state()