diff --git a/custom_components/deebot/const.py b/custom_components/deebot/const.py index 3b224a6..a3c753f 100644 --- a/custom_components/deebot/const.py +++ b/custom_components/deebot/const.py @@ -54,9 +54,6 @@ DEEBOT_DEVICES = f"{DOMAIN}_devices" -LAST_ERROR = "last_error" - - REFRESH_STR_TO_EVENT_DTO: Mapping[str, type[Event]] = { "battery": BatteryEvent, "clean_logs": CleanLogEvent, diff --git a/custom_components/deebot/controller.py b/custom_components/deebot/controller.py index 8473253..cde23d1 100644 --- a/custom_components/deebot/controller.py +++ b/custom_components/deebot/controller.py @@ -31,6 +31,10 @@ _LOGGER = logging.getLogger(__name__) +_EntityGeneratorType = Callable[ + [Device], Sequence[DeebotEntity[Any, EntityDescription]] +] + class DeebotController: """Deebot Controller.""" @@ -114,13 +118,16 @@ def register_platform_add_entities( def register_platform_add_entities_generator( self, async_add_entities: AddEntitiesCallback, - func: Callable[[Device], Sequence[DeebotEntity[Any, EntityDescription]]], + functions: _EntityGeneratorType | tuple[_EntityGeneratorType, ...], ) -> None: """Add entities generated through the provided function.""" new_entites: list[DeebotEntity[Any, EntityDescription]] = [] for device in self._devices: - new_entites.extend(func(device)) + if callable(functions): + functions = (functions,) + for func in functions: + new_entites.extend(func(device)) if new_entites: async_add_entities(new_entites) diff --git a/custom_components/deebot/entity.py b/custom_components/deebot/entity.py index 0d46e6f..d2676a1 100644 --- a/custom_components/deebot/entity.py +++ b/custom_components/deebot/entity.py @@ -24,7 +24,6 @@ class DeebotEntityDescription( ): """Deebot Entity Description.""" - always_available: bool = False capability_fn: Callable[[Capabilities], CapabilityT | None] diff --git a/custom_components/deebot/sensor.py b/custom_components/deebot/sensor.py index 272b26c..89a89ab 100644 --- a/custom_components/deebot/sensor.py +++ b/custom_components/deebot/sensor.py @@ -37,7 +37,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import DOMAIN, LAST_ERROR +from .const import DOMAIN from .controller import DeebotController from .entity import DeebotEntity, DeebotEntityDescription, EventT @@ -144,27 +144,6 @@ def _clean_log_event_attributes(event: CleanLogEvent) -> MutableMapping[str, Any device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, ), - DeebotSensorEntityDescription[ErrorEvent]( - capability_fn=lambda caps: caps.error, - value_fn=lambda e: e.code, - extra_state_attributes_fn=lambda e: {CONF_DESCRIPTION: e.description}, - always_available=True, - key=LAST_ERROR, - translation_key=LAST_ERROR, - icon="mdi:alert-circle", - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DeebotSensorEntityDescription[CleanLogEvent]( - capability_fn=lambda caps: caps.clean.log, - value_fn=_clean_log_event_value, - extra_state_attributes_fn=_clean_log_event_attributes, - always_available=True, - key="last_cleaning", - translation_key="last_cleaning", - icon="mdi:history", - entity_registry_enabled_default=False, - ), DeebotSensorEntityDescription[NetworkInfoEvent]( capability_fn=lambda caps: caps.network, value_fn=lambda e: e.ip, @@ -251,9 +230,21 @@ async def async_setup_entry( DeebotSensor, ENTITY_DESCRIPTIONS, async_add_entities ) - def life_span_entity_generator( + def last_error_entity_generator( + device: Device, + ) -> Sequence[LastErrorSensor]: + if capability := device.capabilities.error: + return [(LastErrorSensor(device, capability))] + return [] + + def last_cleaning_entity_generator( device: Device, - ) -> Sequence[LifeSpanSensor]: + ) -> Sequence[LastCleaningSensor]: + if capability := device.capabilities.clean.log: + return [(LastCleaningSensor(device, capability))] + return [] + + def life_span_entity_generator(device: Device) -> Sequence[LifeSpanSensor]: new_entities = [] capability = device.capabilities.life_span for description in LIFE_SPAN_DESCRIPTIONS: @@ -262,7 +253,12 @@ def life_span_entity_generator( return new_entities controller.register_platform_add_entities_generator( - async_add_entities, life_span_entity_generator + async_add_entities, + ( + life_span_entity_generator, + last_error_entity_generator, + last_cleaning_entity_generator, + ), ) @@ -315,3 +311,71 @@ async def on_event(event: LifeSpanEvent) -> None: self.async_on_remove( self._device.events.subscribe(self._capability.event, on_event) ) + + +class LastErrorSensor( + DeebotEntity[CapabilityEvent[ErrorEvent], SensorEntityDescription], + SensorEntity, # type: ignore +): + """Last error sensor.""" + + _always_available: bool = True + _unrecorded_attributes = frozenset({CONF_DESCRIPTION}) + entity_description: SensorEntityDescription = SensorEntityDescription( + key="last_error", + translation_key="last_error", + icon="mdi:alert-circle", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ) + + async def async_added_to_hass(self) -> None: + """Set up the event listeners now that hass is ready.""" + await super().async_added_to_hass() + + async def on_event(event: ErrorEvent) -> None: + self._attr_native_value = event.code + self._attr_extra_state_attributes = {CONF_DESCRIPTION: event.description} + + self.async_write_ha_state() + + self.async_on_remove( + self._device.events.subscribe(self._capability.event, on_event) + ) + + +class LastCleaningSensor( + DeebotEntity[CapabilityEvent[CleanLogEvent], SensorEntityDescription], + SensorEntity, # type: ignore +): + """Last cleaning sensor.""" + + _always_available: bool = True + entity_description: SensorEntityDescription = SensorEntityDescription( + key="last_cleaning", + translation_key="last_cleaning", + icon="mdi:history", + entity_registry_enabled_default=False, + ) + + async def async_added_to_hass(self) -> None: + """Set up the event listeners now that hass is ready.""" + await super().async_added_to_hass() + + async def on_event(event: CleanLogEvent) -> None: + if event.logs: + log = event.logs[0] + self._attr_native_value = log.stop_reason.display_name + self._attr_extra_state_attributes = { + "timestamp": log.timestamp, + "image_url": log.image_url, + "type": log.type, + "area": log.area, + "duration": log.duration / 60, + } + + self.async_write_ha_state() + + self.async_on_remove( + self._device.events.subscribe(self._capability.event, on_event) + ) diff --git a/custom_components/deebot/vacuum.py b/custom_components/deebot/vacuum.py index c614d6f..53e1c35 100644 --- a/custom_components/deebot/vacuum.py +++ b/custom_components/deebot/vacuum.py @@ -9,7 +9,6 @@ from deebot_client.events import ( BatteryEvent, CustomCommandEvent, - ErrorEvent, FanSpeedEvent, ReportStatsEvent, RoomsEvent, @@ -38,7 +37,6 @@ DOMAIN, EVENT_CLEANING_JOB, EVENT_CUSTOM_COMMAND, - LAST_ERROR, REFRESH_MAP, REFRESH_STR_TO_EVENT_DTO, ) @@ -96,12 +94,17 @@ def vacuum_entity_generator( ) +_ATTR_ROOMS = "rooms" + + class DeebotVacuum( DeebotEntity[Capabilities, StateVacuumEntityDescription], StateVacuumEntity, # type: ignore ): """Deebot Vacuum.""" + _unrecorded_attributes = frozenset({_ATTR_ROOMS}) + _attr_supported_features = ( VacuumEntityFeature.PAUSE | VacuumEntityFeature.STOP @@ -124,7 +127,6 @@ def __init__(self, device: Device): ) self._rooms: list[Room] = [] - self._last_error: ErrorEvent | None = None self._attr_fan_speed_list = [ level.display_name for level in capabilities.fan_speed.types @@ -204,12 +206,7 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None: rooms[room_name] = [room_values, room.id] if rooms: - attributes["rooms"] = rooms - - if self._last_error: - attributes[ - LAST_ERROR - ] = f"{self._last_error.description} ({self._last_error.code})" + attributes[_ATTR_ROOMS] = rooms return attributes