From b1dd742a57b3eb52788b660a907821efa5b243ab Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:49:12 +0200 Subject: [PATCH] Move battery properties from legacy Ecovacs vacuum entity to separate entities (#149084) --- .../components/ecovacs/binary_sensor.py | 51 +++++++++++++++++- homeassistant/components/ecovacs/sensor.py | 50 +++++++++++++++++- homeassistant/components/ecovacs/vacuum.py | 24 +-------- .../ecovacs/snapshots/test_sensor.ambr | 52 +++++++++++++++++++ tests/components/ecovacs/test_init.py | 2 +- 5 files changed, 151 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/ecovacs/binary_sensor.py b/homeassistant/components/ecovacs/binary_sensor.py index 32bf5d3ba15..5997559c3cf 100644 --- a/homeassistant/components/ecovacs/binary_sensor.py +++ b/homeassistant/components/ecovacs/binary_sensor.py @@ -4,10 +4,12 @@ from collections.abc import Callable from dataclasses import dataclass from deebot_client.capabilities import CapabilityEvent -from deebot_client.events.base import Event +from deebot_client.events import Event from deebot_client.events.water_info import MopAttachedEvent +from sucks import VacBot from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -16,7 +18,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import EcovacsConfigEntry -from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity +from .entity import ( + EcovacsCapabilityEntityDescription, + EcovacsDescriptionEntity, + EcovacsLegacyEntity, +) from .util import get_supported_entities @@ -47,12 +53,23 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Add entities for passed config_entry in HA.""" + controller = config_entry.runtime_data + async_add_entities( get_supported_entities( config_entry.runtime_data, EcovacsBinarySensor, ENTITY_DESCRIPTIONS ) ) + legacy_entities = [] + for device in controller.legacy_devices: + if not controller.legacy_entity_is_added(device, "battery_charging"): + controller.add_legacy_entity(device, "battery_charging") + legacy_entities.append(EcovacsLegacyBatteryChargingSensor(device)) + + if legacy_entities: + async_add_entities(legacy_entities) + class EcovacsBinarySensor[EventT: Event]( EcovacsDescriptionEntity[CapabilityEvent[EventT]], @@ -71,3 +88,33 @@ class EcovacsBinarySensor[EventT: Event]( self.async_write_ha_state() self._subscribe(self._capability.event, on_event) + + +class EcovacsLegacyBatteryChargingSensor(EcovacsLegacyEntity, BinarySensorEntity): + """Legacy battery charging sensor.""" + + _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__( + self, + device: VacBot, + ) -> None: + """Initialize the entity.""" + super().__init__(device) + self._attr_unique_id = f"{device.vacuum['did']}_battery_charging" + + async def async_added_to_hass(self) -> None: + """Set up the event listeners now that hass is ready.""" + self._event_listeners.append( + self.device.statusEvents.subscribe( + lambda _: self.schedule_update_ha_state() + ) + ) + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + if self.device.charge_status is None: + return None + return bool(self.device.is_charging) diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index e84485228e4..b368b92a579 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -37,6 +37,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import StateType from . import EcovacsConfigEntry @@ -225,7 +226,7 @@ async def async_setup_entry( async_add_entities(entities) - async def _add_legacy_entities() -> None: + async def _add_legacy_lifespan_entities() -> None: entities = [] for device in controller.legacy_devices: for description in LEGACY_LIFESPAN_SENSORS: @@ -242,14 +243,21 @@ async def async_setup_entry( async_add_entities(entities) def _fire_ecovacs_legacy_lifespan_event(_: Any) -> None: - hass.create_task(_add_legacy_entities()) + hass.create_task(_add_legacy_lifespan_entities()) + legacy_entities = [] for device in controller.legacy_devices: config_entry.async_on_unload( device.lifespanEvents.subscribe( _fire_ecovacs_legacy_lifespan_event ).unsubscribe ) + if not controller.legacy_entity_is_added(device, "battery_status"): + controller.add_legacy_entity(device, "battery_status") + legacy_entities.append(EcovacsLegacyBatterySensor(device)) + + if legacy_entities: + async_add_entities(legacy_entities) class EcovacsSensor( @@ -344,6 +352,44 @@ class EcovacsErrorSensor( self._subscribe(self._capability.event, on_event) +class EcovacsLegacyBatterySensor(EcovacsLegacyEntity, SensorEntity): + """Legacy battery sensor.""" + + _attr_native_unit_of_measurement = PERCENTAGE + _attr_device_class = SensorDeviceClass.BATTERY + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__( + self, + device: VacBot, + ) -> None: + """Initialize the entity.""" + super().__init__(device) + self._attr_unique_id = f"{device.vacuum['did']}_battery_status" + + async def async_added_to_hass(self) -> None: + """Set up the event listeners now that hass is ready.""" + self._event_listeners.append( + self.device.batteryEvents.subscribe( + lambda _: self.schedule_update_ha_state() + ) + ) + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + if (status := self.device.battery_status) is not None: + return status * 100 # type: ignore[no-any-return] + return None + + @property + def icon(self) -> str | None: + """Return the icon to use in the frontend, if any.""" + return icon_for_battery_level( + battery_level=self.native_value, charging=self.device.is_charging + ) + + class EcovacsLegacyLifespanSensor(EcovacsLegacyEntity, SensorEntity): """Legacy Lifespan sensor.""" diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 6570b80e920..d432410c8c5 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -22,7 +22,6 @@ from homeassistant.core import HomeAssistant, SupportsResponse from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify from . import EcovacsConfigEntry @@ -71,8 +70,7 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): _attr_fan_speed_list = [sucks.FAN_SPEED_NORMAL, sucks.FAN_SPEED_HIGH] _attr_supported_features = ( - VacuumEntityFeature.BATTERY - | VacuumEntityFeature.RETURN_HOME + VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.CLEAN_SPOT | VacuumEntityFeature.STOP | VacuumEntityFeature.START @@ -89,11 +87,6 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): lambda _: self.schedule_update_ha_state() ) ) - self._event_listeners.append( - self.device.batteryEvents.subscribe( - lambda _: self.schedule_update_ha_state() - ) - ) self._event_listeners.append( self.device.lifespanEvents.subscribe( lambda _: self.schedule_update_ha_state() @@ -137,21 +130,6 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity): return None - @property - def battery_level(self) -> int | None: - """Return the battery level of the vacuum cleaner.""" - if self.device.battery_status is not None: - return self.device.battery_status * 100 # type: ignore[no-any-return] - - return None - - @property - def battery_icon(self) -> str: - """Return the battery icon for the vacuum cleaner.""" - return icon_for_battery_level( - battery_level=self.battery_level, charging=self.device.is_charging - ) - @property def fan_speed(self) -> str | None: """Return the fan speed of the vacuum cleaner.""" diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index fcd043e10fa..c216c4c9e4a 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -1,4 +1,55 @@ # serializer version: 1 +# name: test_legacy_sensors[123][sensor.e1234567890000000003_battery:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.e1234567890000000003_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:battery-unknown', + 'original_name': 'Battery', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'E1234567890000000003_battery_status', + 'unit_of_measurement': '%', + }) +# --- +# name: test_legacy_sensors[123][sensor.e1234567890000000003_battery:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'E1234567890000000003 Battery', + 'icon': 'mdi:battery-unknown', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.e1234567890000000003_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_legacy_sensors[123][sensor.e1234567890000000003_filter_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -148,6 +199,7 @@ # --- # name: test_legacy_sensors[123][states] list([ + 'sensor.e1234567890000000003_battery', 'sensor.e1234567890000000003_main_brush_lifespan', 'sensor.e1234567890000000003_side_brush_lifespan', 'sensor.e1234567890000000003_filter_lifespan', diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py index c0e5ce143c9..3115f1b4040 100644 --- a/tests/components/ecovacs/test_init.py +++ b/tests/components/ecovacs/test_init.py @@ -107,7 +107,7 @@ async def test_devices_in_dr( [ ("yna5x1", 26), ("5xu9h3", 25), - ("123", 1), + ("123", 2), ], ) async def test_all_entities_loaded(