From 3703698f778d9e59b96cbe97dfce4160c7c41f56 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 27 Feb 2024 09:39:49 +0100 Subject: [PATCH] Bump deebot-client to 6.0.2 (#111507) --- .../components/ecovacs/binary_sensor.py | 12 +++++-- homeassistant/components/ecovacs/button.py | 15 ++++++--- .../components/ecovacs/controller.py | 18 ++++++++--- .../components/ecovacs/diagnostics.py | 6 ++-- homeassistant/components/ecovacs/entity.py | 32 +++++++++++-------- homeassistant/components/ecovacs/image.py | 11 ++++--- .../components/ecovacs/manifest.json | 2 +- homeassistant/components/ecovacs/number.py | 7 ++-- homeassistant/components/ecovacs/select.py | 13 ++++++-- homeassistant/components/ecovacs/sensor.py | 20 +++++++++--- homeassistant/components/ecovacs/switch.py | 26 ++++++++++----- homeassistant/components/ecovacs/util.py | 8 +++-- homeassistant/components/ecovacs/vacuum.py | 17 +++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/ecovacs/test_binary_sensor.py | 5 +-- tests/components/ecovacs/test_button.py | 5 +-- tests/components/ecovacs/test_init.py | 7 ++-- tests/components/ecovacs/test_number.py | 7 ++-- tests/components/ecovacs/test_select.py | 7 ++-- tests/components/ecovacs/test_sensor.py | 5 +-- tests/components/ecovacs/test_switch.py | 5 +-- 22 files changed, 155 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/ecovacs/binary_sensor.py b/homeassistant/components/ecovacs/binary_sensor.py index 95e87a04b18..f04f2110003 100644 --- a/homeassistant/components/ecovacs/binary_sensor.py +++ b/homeassistant/components/ecovacs/binary_sensor.py @@ -3,7 +3,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Generic -from deebot_client.capabilities import CapabilityEvent +from deebot_client.capabilities import CapabilityEvent, VacuumCapabilities from deebot_client.events.water_info import WaterInfoEvent from homeassistant.components.binary_sensor import ( @@ -17,7 +17,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .controller import EcovacsController -from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT +from .entity import ( + CapabilityDevice, + EcovacsCapabilityEntityDescription, + EcovacsDescriptionEntity, + EventT, +) from .util import get_supported_entitites @@ -34,6 +39,7 @@ class EcovacsBinarySensorEntityDescription( ENTITY_DESCRIPTIONS: tuple[EcovacsBinarySensorEntityDescription, ...] = ( EcovacsBinarySensorEntityDescription[WaterInfoEvent]( + device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.water, value_fn=lambda e: e.mop_attached, key="water_mop_attached", @@ -56,7 +62,7 @@ async def async_setup_entry( class EcovacsBinarySensor( - EcovacsDescriptionEntity[CapabilityEvent[EventT]], + EcovacsDescriptionEntity[CapabilityDevice, CapabilityEvent[EventT]], BinarySensorEntity, ): """Ecovacs binary sensor.""" diff --git a/homeassistant/components/ecovacs/button.py b/homeassistant/components/ecovacs/button.py index c2e5458c2ed..0e011726010 100644 --- a/homeassistant/components/ecovacs/button.py +++ b/homeassistant/components/ecovacs/button.py @@ -1,7 +1,12 @@ """Ecovacs button module.""" from dataclasses import dataclass -from deebot_client.capabilities import CapabilityExecute, CapabilityLifeSpan +from deebot_client.capabilities import ( + Capabilities, + CapabilityExecute, + CapabilityLifeSpan, + VacuumCapabilities, +) from deebot_client.events import LifeSpan from homeassistant.components.button import ButtonEntity, ButtonEntityDescription @@ -13,6 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, SUPPORTED_LIFESPANS from .controller import EcovacsController from .entity import ( + CapabilityDevice, EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EcovacsEntity, @@ -37,6 +43,7 @@ class EcovacsLifespanButtonEntityDescription(ButtonEntityDescription): ENTITY_DESCRIPTIONS: tuple[EcovacsButtonEntityDescription, ...] = ( EcovacsButtonEntityDescription( + device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.map.relocation if caps.map else None, key="relocate", translation_key="relocate", @@ -66,7 +73,7 @@ async def async_setup_entry( entities: list[EcovacsEntity] = get_supported_entitites( controller, EcovacsButtonEntity, ENTITY_DESCRIPTIONS ) - for device in controller.devices: + for device in controller.devices(Capabilities): lifespan_capability = device.capabilities.life_span for description in LIFESPAN_ENTITY_DESCRIPTIONS: if description.component in lifespan_capability.types: @@ -81,7 +88,7 @@ async def async_setup_entry( class EcovacsButtonEntity( - EcovacsDescriptionEntity[CapabilityExecute], + EcovacsDescriptionEntity[CapabilityDevice, CapabilityExecute], ButtonEntity, ): """Ecovacs button entity.""" @@ -94,7 +101,7 @@ class EcovacsButtonEntity( class EcovacsResetLifespanButtonEntity( - EcovacsDescriptionEntity[CapabilityLifeSpan], + EcovacsDescriptionEntity[Capabilities, CapabilityLifeSpan], ButtonEntity, ): """Ecovacs reset lifespan button entity.""" diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py index 27b64db20b6..6ba5dcdba6c 100644 --- a/homeassistant/components/ecovacs/controller.py +++ b/homeassistant/components/ecovacs/controller.py @@ -1,13 +1,14 @@ """Controller module.""" from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Generator, Mapping import logging import ssl from typing import Any from deebot_client.api_client import ApiClient from deebot_client.authentication import Authenticator, create_rest_config +from deebot_client.capabilities import Capabilities from deebot_client.const import UNDEFINED, UndefinedType from deebot_client.device import Device from deebot_client.exceptions import DeebotError, InvalidAuthenticationError @@ -18,7 +19,7 @@ from deebot_client.util.continents import get_continent from sucks import EcoVacsAPI, VacBot from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.util.ssl import get_default_no_verify_context @@ -39,7 +40,7 @@ class EcovacsController: def __init__(self, hass: HomeAssistant, config: Mapping[str, Any]) -> None: """Initialize controller.""" self._hass = hass - self.devices: list[Device] = [] + self._devices: list[Device] = [] self.legacy_devices: list[VacBot] = [] self._device_id = get_client_device_id() country = config[CONF_COUNTRY] @@ -86,7 +87,7 @@ class EcovacsController: mqtt_config_verfied = True device = Device(device_config, self._authenticator) await device.initialize(self._mqtt) - self.devices.append(device) + self._devices.append(device) else: # Legacy device bot = VacBot( @@ -108,9 +109,16 @@ class EcovacsController: async def teardown(self) -> None: """Disconnect controller.""" - for device in self.devices: + for device in self._devices: await device.teardown() for legacy_device in self.legacy_devices: await self._hass.async_add_executor_job(legacy_device.disconnect) await self._mqtt.disconnect() await self._authenticator.teardown() + + @callback + def devices(self, capability: type[Capabilities]) -> Generator[Device, None, None]: + """Return generator for devices with a specific capability.""" + for device in self._devices: + if isinstance(device.capabilities, capability): + yield device diff --git a/homeassistant/components/ecovacs/diagnostics.py b/homeassistant/components/ecovacs/diagnostics.py index d961e231631..6493dce2712 100644 --- a/homeassistant/components/ecovacs/diagnostics.py +++ b/homeassistant/components/ecovacs/diagnostics.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Any +from deebot_client.capabilities import Capabilities + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -31,8 +33,8 @@ async def async_get_config_entry_diagnostics( } diag["devices"] = [ - async_redact_data(device.device_info.api_device_info, REDACT_DEVICE) - for device in controller.devices + async_redact_data(device.device_info, REDACT_DEVICE) + for device in controller.devices(Capabilities) ] diag["legacy_devices"] = [ async_redact_data(device.vacuum, REDACT_DEVICE) diff --git a/homeassistant/components/ecovacs/entity.py b/homeassistant/components/ecovacs/entity.py index 20de6914700..817172016bc 100644 --- a/homeassistant/components/ecovacs/entity.py +++ b/homeassistant/components/ecovacs/entity.py @@ -16,11 +16,12 @@ from homeassistant.helpers.entity import Entity, EntityDescription from .const import DOMAIN -CapabilityT = TypeVar("CapabilityT") +CapabilityEntity = TypeVar("CapabilityEntity") +CapabilityDevice = TypeVar("CapabilityDevice", bound=Capabilities) EventT = TypeVar("EventT", bound=Event) -class EcovacsEntity(Entity, Generic[CapabilityT]): +class EcovacsEntity(Entity, Generic[CapabilityDevice, CapabilityEntity]): """Ecovacs entity.""" _attr_should_poll = False @@ -29,13 +30,15 @@ class EcovacsEntity(Entity, Generic[CapabilityT]): def __init__( self, - device: Device, - capability: CapabilityT, + device: Device[CapabilityDevice], + capability: CapabilityEntity, **kwargs: Any, ) -> None: """Initialize entity.""" super().__init__(**kwargs) - self._attr_unique_id = f"{device.device_info.did}_{self.entity_description.key}" + self._attr_unique_id = ( + f"{device.device_info['did']}_{self.entity_description.key}" + ) self._device = device self._capability = capability @@ -46,16 +49,16 @@ class EcovacsEntity(Entity, Generic[CapabilityT]): """Return device specific attributes.""" device_info = self._device.device_info info = DeviceInfo( - identifiers={(DOMAIN, device_info.did)}, + identifiers={(DOMAIN, device_info["did"])}, manufacturer="Ecovacs", sw_version=self._device.fw_version, - serial_number=device_info.name, + serial_number=device_info["name"], ) - if nick := device_info.api_device_info.get("nick"): + if nick := device_info.get("nick"): info["name"] = nick - if model := device_info.api_device_info.get("deviceName"): + if model := device_info.get("deviceName"): info["model"] = model if mac := self._device.mac: @@ -93,13 +96,13 @@ class EcovacsEntity(Entity, Generic[CapabilityT]): self._device.events.request_refresh(event_type) -class EcovacsDescriptionEntity(EcovacsEntity[CapabilityT]): +class EcovacsDescriptionEntity(EcovacsEntity[CapabilityDevice, CapabilityEntity]): """Ecovacs entity.""" def __init__( self, - device: Device, - capability: CapabilityT, + device: Device[CapabilityDevice], + capability: CapabilityEntity, entity_description: EntityDescription, **kwargs: Any, ) -> None: @@ -111,8 +114,9 @@ class EcovacsDescriptionEntity(EcovacsEntity[CapabilityT]): @dataclass(kw_only=True, frozen=True) class EcovacsCapabilityEntityDescription( EntityDescription, - Generic[CapabilityT], + Generic[CapabilityDevice, CapabilityEntity], ): """Ecovacs entity description.""" - capability_fn: Callable[[Capabilities], CapabilityT | None] + device_capabilities: type[CapabilityDevice] + capability_fn: Callable[[CapabilityDevice], CapabilityEntity | None] diff --git a/homeassistant/components/ecovacs/image.py b/homeassistant/components/ecovacs/image.py index 18c162138fb..82e20e19732 100644 --- a/homeassistant/components/ecovacs/image.py +++ b/homeassistant/components/ecovacs/image.py @@ -1,6 +1,6 @@ """Ecovacs image entities.""" -from deebot_client.capabilities import CapabilityMap +from deebot_client.capabilities import CapabilityMap, VacuumCapabilities from deebot_client.device import Device from deebot_client.events.map import CachedMapInfoEvent, MapChangedEvent @@ -23,8 +23,9 @@ async def async_setup_entry( """Add entities for passed config_entry in HA.""" controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id] entities = [] - for device in controller.devices: - if caps := device.capabilities.map: + for device in controller.devices(VacuumCapabilities): + capabilities: VacuumCapabilities = device.capabilities + if caps := capabilities.map: entities.append(EcovacsMap(device, caps, hass)) if entities: @@ -32,7 +33,7 @@ async def async_setup_entry( class EcovacsMap( - EcovacsEntity[CapabilityMap], + EcovacsEntity[VacuumCapabilities, CapabilityMap], ImageEntity, ): """Ecovacs map.""" @@ -72,7 +73,7 @@ class EcovacsMap( self._attr_image_last_updated = event.when self.async_write_ha_state() - self._subscribe(self._capability.chached_info.event, on_info) + self._subscribe(self._capability.cached_info.event, on_info) self._subscribe(self._capability.changed.event, on_changed) async def async_update(self) -> None: diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 326c2916bed..837c48b45d8 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.9", "deebot-client==5.2.2"] + "requirements": ["py-sucks==0.9.9", "deebot-client==6.0.2"] } diff --git a/homeassistant/components/ecovacs/number.py b/homeassistant/components/ecovacs/number.py index 45250ab69b1..0dc379c68f0 100644 --- a/homeassistant/components/ecovacs/number.py +++ b/homeassistant/components/ecovacs/number.py @@ -5,7 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Generic -from deebot_client.capabilities import CapabilitySet +from deebot_client.capabilities import Capabilities, CapabilitySet, VacuumCapabilities from deebot_client.events import CleanCountEvent, VolumeEvent from homeassistant.components.number import NumberEntity, NumberEntityDescription @@ -17,6 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .controller import EcovacsController from .entity import ( + CapabilityDevice, EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EcovacsEntity, @@ -39,6 +40,7 @@ class EcovacsNumberEntityDescription( ENTITY_DESCRIPTIONS: tuple[EcovacsNumberEntityDescription, ...] = ( EcovacsNumberEntityDescription[VolumeEvent]( + device_capabilities=Capabilities, capability_fn=lambda caps: caps.settings.volume, value_fn=lambda e: e.volume, native_max_value_fn=lambda e: e.maximum, @@ -51,6 +53,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsNumberEntityDescription, ...] = ( native_step=1.0, ), EcovacsNumberEntityDescription[CleanCountEvent]( + device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.clean.count, value_fn=lambda e: e.count, key="clean_count", @@ -79,7 +82,7 @@ async def async_setup_entry( class EcovacsNumberEntity( - EcovacsDescriptionEntity[CapabilitySet[EventT, int]], + EcovacsDescriptionEntity[CapabilityDevice, CapabilitySet[EventT, int]], NumberEntity, ): """Ecovacs number entity.""" diff --git a/homeassistant/components/ecovacs/select.py b/homeassistant/components/ecovacs/select.py index cd1cdd10379..00e7134266b 100644 --- a/homeassistant/components/ecovacs/select.py +++ b/homeassistant/components/ecovacs/select.py @@ -3,7 +3,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any, Generic -from deebot_client.capabilities import CapabilitySetTypes +from deebot_client.capabilities import CapabilitySetTypes, VacuumCapabilities from deebot_client.device import Device from deebot_client.events import WaterInfoEvent, WorkModeEvent @@ -15,7 +15,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .controller import EcovacsController -from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT +from .entity import ( + CapabilityDevice, + EcovacsCapabilityEntityDescription, + EcovacsDescriptionEntity, + EventT, +) from .util import get_supported_entitites @@ -33,6 +38,7 @@ class EcovacsSelectEntityDescription( ENTITY_DESCRIPTIONS: tuple[EcovacsSelectEntityDescription, ...] = ( EcovacsSelectEntityDescription[WaterInfoEvent]( + device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.water, current_option_fn=lambda e: e.amount.display_name, options_fn=lambda water: [amount.display_name for amount in water.types], @@ -41,6 +47,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSelectEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ), EcovacsSelectEntityDescription[WorkModeEvent]( + device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.clean.work_mode, current_option_fn=lambda e: e.mode.display_name, options_fn=lambda cap: [mode.display_name for mode in cap.types], @@ -67,7 +74,7 @@ async def async_setup_entry( class EcovacsSelectEntity( - EcovacsDescriptionEntity[CapabilitySetTypes[EventT, str]], + EcovacsDescriptionEntity[CapabilityDevice, CapabilitySetTypes[EventT, str]], SelectEntity, ): """Ecovacs select entity.""" diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index 16a1b4acd43..6efc9ec0385 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -5,7 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Generic -from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan +from deebot_client.capabilities import Capabilities, CapabilityEvent, CapabilityLifeSpan from deebot_client.events import ( BatteryEvent, ErrorEvent, @@ -39,6 +39,7 @@ from homeassistant.helpers.typing import StateType from .const import DOMAIN, SUPPORTED_LIFESPANS from .controller import EcovacsController from .entity import ( + CapabilityDevice, EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EcovacsEntity, @@ -62,6 +63,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( # Stats EcovacsSensorEntityDescription[StatsEvent]( key="stats_area", + device_capabilities=Capabilities, capability_fn=lambda caps: caps.stats.clean, value_fn=lambda e: e.area, translation_key="stats_area", @@ -69,6 +71,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( ), EcovacsSensorEntityDescription[StatsEvent]( key="stats_time", + device_capabilities=Capabilities, capability_fn=lambda caps: caps.stats.clean, value_fn=lambda e: e.time, translation_key="stats_time", @@ -78,6 +81,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( ), # TotalStats EcovacsSensorEntityDescription[TotalStatsEvent]( + device_capabilities=Capabilities, capability_fn=lambda caps: caps.stats.total, value_fn=lambda e: e.area, key="total_stats_area", @@ -86,6 +90,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL_INCREASING, ), EcovacsSensorEntityDescription[TotalStatsEvent]( + device_capabilities=Capabilities, capability_fn=lambda caps: caps.stats.total, value_fn=lambda e: e.time, key="total_stats_time", @@ -96,6 +101,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL_INCREASING, ), EcovacsSensorEntityDescription[TotalStatsEvent]( + device_capabilities=Capabilities, capability_fn=lambda caps: caps.stats.total, value_fn=lambda e: e.cleanings, key="total_stats_cleanings", @@ -103,6 +109,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL_INCREASING, ), EcovacsSensorEntityDescription[BatteryEvent]( + device_capabilities=Capabilities, capability_fn=lambda caps: caps.battery, value_fn=lambda e: e.value, key=ATTR_BATTERY_LEVEL, @@ -111,6 +118,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), EcovacsSensorEntityDescription[NetworkInfoEvent]( + device_capabilities=Capabilities, capability_fn=lambda caps: caps.network, value_fn=lambda e: e.ip, key="network_ip", @@ -119,6 +127,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), EcovacsSensorEntityDescription[NetworkInfoEvent]( + device_capabilities=Capabilities, capability_fn=lambda caps: caps.network, value_fn=lambda e: e.rssi, key="network_rssi", @@ -127,6 +136,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), EcovacsSensorEntityDescription[NetworkInfoEvent]( + device_capabilities=Capabilities, capability_fn=lambda caps: caps.network, value_fn=lambda e: e.ssid, key="network_ssid", @@ -169,7 +179,7 @@ async def async_setup_entry( entities: list[EcovacsEntity] = get_supported_entitites( controller, EcovacsSensor, ENTITY_DESCRIPTIONS ) - for device in controller.devices: + for device in controller.devices(Capabilities): lifespan_capability = device.capabilities.life_span for description in LIFESPAN_ENTITY_DESCRIPTIONS: if description.component in lifespan_capability.types: @@ -184,7 +194,7 @@ async def async_setup_entry( class EcovacsSensor( - EcovacsDescriptionEntity[CapabilityEvent], + EcovacsDescriptionEntity[CapabilityDevice, CapabilityEvent], SensorEntity, ): """Ecovacs sensor.""" @@ -207,7 +217,7 @@ class EcovacsSensor( class EcovacsLifespanSensor( - EcovacsDescriptionEntity[CapabilityLifeSpan], + EcovacsDescriptionEntity[Capabilities, CapabilityLifeSpan], SensorEntity, ): """Lifespan sensor.""" @@ -227,7 +237,7 @@ class EcovacsLifespanSensor( class EcovacsErrorSensor( - EcovacsEntity[CapabilityEvent[ErrorEvent]], + EcovacsEntity[Capabilities, CapabilityEvent[ErrorEvent]], SensorEntity, ): """Error sensor.""" diff --git a/homeassistant/components/ecovacs/switch.py b/homeassistant/components/ecovacs/switch.py index e9e915877d8..119de35e786 100644 --- a/homeassistant/components/ecovacs/switch.py +++ b/homeassistant/components/ecovacs/switch.py @@ -2,7 +2,11 @@ from dataclasses import dataclass from typing import Any -from deebot_client.capabilities import CapabilitySetEnable +from deebot_client.capabilities import ( + Capabilities, + CapabilitySetEnable, + VacuumCapabilities, +) from deebot_client.events import EnableEvent from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription @@ -14,6 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .controller import EcovacsController from .entity import ( + CapabilityDevice, EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EcovacsEntity, @@ -24,41 +29,46 @@ from .util import get_supported_entitites @dataclass(kw_only=True, frozen=True) class EcovacsSwitchEntityDescription( SwitchEntityDescription, - EcovacsCapabilityEntityDescription, + EcovacsCapabilityEntityDescription[CapabilityDevice, CapabilitySetEnable], ): """Ecovacs switch entity description.""" ENTITY_DESCRIPTIONS: tuple[EcovacsSwitchEntityDescription, ...] = ( - EcovacsSwitchEntityDescription( + EcovacsSwitchEntityDescription[Capabilities]( + device_capabilities=Capabilities, capability_fn=lambda c: c.settings.advanced_mode, key="advanced_mode", translation_key="advanced_mode", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), - EcovacsSwitchEntityDescription( + EcovacsSwitchEntityDescription[VacuumCapabilities]( + device_capabilities=VacuumCapabilities, capability_fn=lambda c: c.clean.continuous, key="continuous_cleaning", translation_key="continuous_cleaning", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), - EcovacsSwitchEntityDescription( + EcovacsSwitchEntityDescription[VacuumCapabilities]( + device_capabilities=VacuumCapabilities, capability_fn=lambda c: c.settings.carpet_auto_fan_boost, key="carpet_auto_fan_boost", translation_key="carpet_auto_fan_boost", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), - EcovacsSwitchEntityDescription( + EcovacsSwitchEntityDescription[VacuumCapabilities]( + device_capabilities=VacuumCapabilities, capability_fn=lambda c: c.clean.preference, key="clean_preference", translation_key="clean_preference", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), - EcovacsSwitchEntityDescription( + EcovacsSwitchEntityDescription[Capabilities]( + device_capabilities=Capabilities, capability_fn=lambda c: c.settings.true_detect, key="true_detect", translation_key="true_detect", @@ -83,7 +93,7 @@ async def async_setup_entry( class EcovacsSwitchEntity( - EcovacsDescriptionEntity[CapabilitySetEnable], + EcovacsDescriptionEntity[CapabilityDevice, CapabilitySetEnable], SwitchEntity, ): """Ecovacs switch entity.""" diff --git a/homeassistant/components/ecovacs/util.py b/homeassistant/components/ecovacs/util.py index 28750d4f9de..b3e0d4d96be 100644 --- a/homeassistant/components/ecovacs/util.py +++ b/homeassistant/components/ecovacs/util.py @@ -5,6 +5,8 @@ import random import string from typing import TYPE_CHECKING +from deebot_client.capabilities import Capabilities + from .entity import ( EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, @@ -30,9 +32,11 @@ def get_supported_entitites( """Return all supported entities for all devices.""" entities: list[EcovacsEntity] = [] - for device in controller.devices: + for device in controller.devices(Capabilities): for description in descriptions: - if capability := description.capability_fn(device.capabilities): + if isinstance(device.capabilities, description.device_capabilities) and ( + capability := description.capability_fn(device.capabilities) + ): entities.append(entity_class(device, capability, description)) return entities diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index a9990bc6fff..0d65d58d84c 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -5,7 +5,7 @@ from collections.abc import Mapping import logging from typing import Any -from deebot_client.capabilities import Capabilities +from deebot_client.capabilities import VacuumCapabilities from deebot_client.device import Device from deebot_client.events import BatteryEvent, FanSpeedEvent, RoomsEvent, StateEvent from deebot_client.models import CleanAction, CleanMode, Room, State @@ -50,7 +50,7 @@ async def async_setup_entry( for device in controller.legacy_devices: await hass.async_add_executor_job(device.connect_and_wait_until_ready) vacuums.append(EcovacsLegacyVacuum(device)) - for device in controller.devices: + for device in controller.devices(VacuumCapabilities): vacuums.append(EcovacsVacuum(device)) _LOGGER.debug("Adding Ecovacs Vacuums to Home Assistant: %s", vacuums) async_add_entities(vacuums) @@ -210,7 +210,7 @@ _ATTR_ROOMS = "rooms" class EcovacsVacuum( - EcovacsEntity[Capabilities], + EcovacsEntity[VacuumCapabilities, VacuumCapabilities], StateVacuumEntity, ): """Ecovacs vacuum.""" @@ -233,7 +233,7 @@ class EcovacsVacuum( key="vacuum", translation_key="vacuum", name=None ) - def __init__(self, device: Device) -> None: + def __init__(self, device: Device[VacuumCapabilities]) -> None: """Initialize the vacuum.""" capabilities = device.capabilities super().__init__(device, capabilities) @@ -349,6 +349,15 @@ class EcovacsVacuum( translation_key="vacuum_send_command_params_required", translation_placeholders={"command": command}, ) + if self._capability.clean.action.area is None: + info = self._device.device_info + name = info.get("nick", info["name"]) + raise ServiceValidationError( + f"Vacuum {name} does not support area capability!", + translation_domain=DOMAIN, + translation_key="vacuum_send_command_area_not_supported", + translation_placeholders={"name": name}, + ) if command in "spot_area": await self._device.execute_command( diff --git a/requirements_all.txt b/requirements_all.txt index 5b7869972e7..4ec11c555a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -699,7 +699,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==5.2.2 +deebot-client==6.0.2 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b8777ef225..2e005695a83 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -574,7 +574,7 @@ dbus-fast==2.21.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==5.2.2 +deebot-client==6.0.2 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/tests/components/ecovacs/test_binary_sensor.py b/tests/components/ecovacs/test_binary_sensor.py index f72ad6bd7e5..2ca0100be31 100644 --- a/tests/components/ecovacs/test_binary_sensor.py +++ b/tests/components/ecovacs/test_binary_sensor.py @@ -1,5 +1,6 @@ """Tests for Ecovacs binary sensors.""" +from deebot_client.capabilities import Capabilities from deebot_client.events import WaterAmount, WaterInfoEvent import pytest from syrupy import SnapshotAssertion @@ -37,10 +38,10 @@ async def test_mop_attached( assert entity_entry == snapshot(name=f"{entity_id}-entity_entry") assert entity_entry.device_id - device = controller.devices[0] + device = next(controller.devices(Capabilities)) assert (device_entry := device_registry.async_get(entity_entry.device_id)) - assert device_entry.identifiers == {(DOMAIN, device.device_info.did)} + assert device_entry.identifiers == {(DOMAIN, device.device_info["did"])} event_bus = device.events await notify_and_wait( diff --git a/tests/components/ecovacs/test_button.py b/tests/components/ecovacs/test_button.py index 24c926b1f77..8e583e6342b 100644 --- a/tests/components/ecovacs/test_button.py +++ b/tests/components/ecovacs/test_button.py @@ -1,5 +1,6 @@ """Tests for Ecovacs sensors.""" +from deebot_client.capabilities import Capabilities from deebot_client.command import Command from deebot_client.commands.json import ResetLifeSpan, SetRelocationState from deebot_client.events import LifeSpan @@ -60,7 +61,7 @@ async def test_buttons( ) -> None: """Test that sensor entity snapshots match.""" assert hass.states.async_entity_ids() == [e[0] for e in entities] - device = controller.devices[0] + device = next(controller.devices(Capabilities)) for entity_id, command in entities: assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" assert state.state == STATE_UNKNOWN @@ -83,7 +84,7 @@ async def test_buttons( assert entity_entry.device_id assert (device_entry := device_registry.async_get(entity_entry.device_id)) - assert device_entry.identifiers == {(DOMAIN, device.device_info.did)} + assert device_entry.identifiers == {(DOMAIN, device.device_info["did"])} @pytest.mark.parametrize( diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py index e76001fbaeb..7a67b0716f9 100644 --- a/tests/components/ecovacs/test_init.py +++ b/tests/components/ecovacs/test_init.py @@ -2,6 +2,7 @@ from typing import Any from unittest.mock import AsyncMock, Mock, patch +from deebot_client.capabilities import Capabilities from deebot_client.exceptions import DeebotError, InvalidAuthenticationError import pytest from syrupy import SnapshotAssertion @@ -106,13 +107,13 @@ async def test_devices_in_dr( snapshot: SnapshotAssertion, ) -> None: """Test all devices are in the device registry.""" - for device in controller.devices: + for device in controller.devices(Capabilities): assert ( device_entry := device_registry.async_get_device( - identifiers={(DOMAIN, device.device_info.did)} + identifiers={(DOMAIN, device.device_info["did"])} ) ) - assert device_entry == snapshot(name=device.device_info.did) + assert device_entry == snapshot(name=device.device_info["did"]) @pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration") diff --git a/tests/components/ecovacs/test_number.py b/tests/components/ecovacs/test_number.py index 3d9607fc9af..6d8941506b5 100644 --- a/tests/components/ecovacs/test_number.py +++ b/tests/components/ecovacs/test_number.py @@ -2,6 +2,7 @@ from dataclasses import dataclass +from deebot_client.capabilities import Capabilities from deebot_client.command import Command from deebot_client.commands.json import SetVolume from deebot_client.events import Event, VolumeEvent @@ -65,7 +66,7 @@ async def test_number_entities( tests: list[NumberTestCase], ) -> None: """Test that number entity snapshots match.""" - device = controller.devices[0] + device = next(controller.devices(Capabilities)) event_bus = device.events assert sorted(hass.states.async_entity_ids()) == sorted( @@ -88,7 +89,7 @@ async def test_number_entities( assert entity_entry.device_id assert (device_entry := device_registry.async_get(entity_entry.device_id)) - assert device_entry.identifiers == {(DOMAIN, device.device_info.did)} + assert device_entry.identifiers == {(DOMAIN, device.device_info["did"])} device._execute_command.reset_mock() await hass.services.async_call( @@ -130,7 +131,7 @@ async def test_volume_maximum( controller: EcovacsController, ) -> None: """Test volume maximum.""" - device = controller.devices[0] + device = next(controller.devices(Capabilities)) event_bus = device.events entity_id = "number.ozmo_950_volume" assert (state := hass.states.get(entity_id)) diff --git a/tests/components/ecovacs/test_select.py b/tests/components/ecovacs/test_select.py index 0d1a5d19116..b7e9435b416 100644 --- a/tests/components/ecovacs/test_select.py +++ b/tests/components/ecovacs/test_select.py @@ -1,5 +1,6 @@ """Tests for Ecovacs select entities.""" +from deebot_client.capabilities import Capabilities from deebot_client.command import Command from deebot_client.commands.json import SetWaterInfo from deebot_client.event_bus import EventBus @@ -63,7 +64,7 @@ async def test_selects( assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" assert state.state == STATE_UNKNOWN - device = controller.devices[0] + device = next(controller.devices(Capabilities)) await notify_events(hass, device.events) for entity_id in entity_ids: assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" @@ -74,7 +75,7 @@ async def test_selects( assert entity_entry.device_id assert (device_entry := device_registry.async_get(entity_entry.device_id)) - assert device_entry.identifiers == {(DOMAIN, device.device_info.did)} + assert device_entry.identifiers == {(DOMAIN, device.device_info["did"])} @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -99,7 +100,7 @@ async def test_selects_change( command: Command, ) -> None: """Test that changing select entities works.""" - device = controller.devices[0] + device = next(controller.devices(Capabilities)) await notify_events(hass, device.events) assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" diff --git a/tests/components/ecovacs/test_sensor.py b/tests/components/ecovacs/test_sensor.py index 78755668f0f..7ff4ab3f009 100644 --- a/tests/components/ecovacs/test_sensor.py +++ b/tests/components/ecovacs/test_sensor.py @@ -1,5 +1,6 @@ """Tests for Ecovacs sensors.""" +from deebot_client.capabilities import Capabilities from deebot_client.event_bus import EventBus from deebot_client.events import ( BatteryEvent, @@ -84,7 +85,7 @@ async def test_sensors( assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" assert state.state == STATE_UNKNOWN - device = controller.devices[0] + device = next(controller.devices(Capabilities)) await notify_events(hass, device.events) for entity_id in entity_ids: assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" @@ -95,7 +96,7 @@ async def test_sensors( assert entity_entry.device_id assert (device_entry := device_registry.async_get(entity_entry.device_id)) - assert device_entry.identifiers == {(DOMAIN, device.device_info.did)} + assert device_entry.identifiers == {(DOMAIN, device.device_info["did"])} @pytest.mark.parametrize( diff --git a/tests/components/ecovacs/test_switch.py b/tests/components/ecovacs/test_switch.py index 35d2f487b95..e405a1ee3ec 100644 --- a/tests/components/ecovacs/test_switch.py +++ b/tests/components/ecovacs/test_switch.py @@ -2,6 +2,7 @@ from dataclasses import dataclass +from deebot_client.capabilities import Capabilities from deebot_client.command import Command from deebot_client.commands.json import ( SetAdvancedMode, @@ -87,7 +88,7 @@ async def test_switch_entities( tests: list[SwitchTestCase], ) -> None: """Test switch entities.""" - device = controller.devices[0] + device = next(controller.devices(Capabilities)) event_bus = device.events assert hass.states.async_entity_ids() == [test.entity_id for test in tests] @@ -108,7 +109,7 @@ async def test_switch_entities( assert entity_entry.device_id assert (device_entry := device_registry.async_get(entity_entry.device_id)) - assert device_entry.identifiers == {(DOMAIN, device.device_info.did)} + assert device_entry.identifiers == {(DOMAIN, device.device_info["did"])} device._execute_command.reset_mock() await hass.services.async_call(