From 8e86c3c77599595c2e11a9434a57c7331c6015f3 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 23 Dec 2024 15:54:40 +0100 Subject: [PATCH] Add Ecovacs station entities (#133876) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/ecovacs/button.py | 50 +- homeassistant/components/ecovacs/const.py | 4 + homeassistant/components/ecovacs/icons.json | 12 + homeassistant/components/ecovacs/sensor.py | 12 +- homeassistant/components/ecovacs/strings.json | 16 + homeassistant/components/ecovacs/util.py | 11 + .../fixtures/devices/qhe2o2/device.json | 29 + .../ecovacs/snapshots/test_button.ambr | 322 ++++++++ .../ecovacs/snapshots/test_sensor.ambr | 775 ++++++++++++++++++ tests/components/ecovacs/test_button.py | 39 +- tests/components/ecovacs/test_sensor.py | 27 +- 11 files changed, 1290 insertions(+), 7 deletions(-) create mode 100644 tests/components/ecovacs/fixtures/devices/qhe2o2/device.json diff --git a/homeassistant/components/ecovacs/button.py b/homeassistant/components/ecovacs/button.py index 5d76b38bed8..2759ca972df 100644 --- a/homeassistant/components/ecovacs/button.py +++ b/homeassistant/components/ecovacs/button.py @@ -2,7 +2,12 @@ from dataclasses import dataclass -from deebot_client.capabilities import CapabilityExecute, CapabilityLifeSpan +from deebot_client.capabilities import ( + CapabilityExecute, + CapabilityExecuteTypes, + CapabilityLifeSpan, +) +from deebot_client.commands import StationAction from deebot_client.events import LifeSpan from homeassistant.components.button import ButtonEntity, ButtonEntityDescription @@ -11,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EcovacsConfigEntry -from .const import SUPPORTED_LIFESPANS +from .const import SUPPORTED_LIFESPANS, SUPPORTED_STATION_ACTIONS from .entity import ( EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, @@ -35,6 +40,13 @@ class EcovacsLifespanButtonEntityDescription(ButtonEntityDescription): component: LifeSpan +@dataclass(kw_only=True, frozen=True) +class EcovacsStationActionButtonEntityDescription(ButtonEntityDescription): + """Ecovacs station action button entity description.""" + + action: StationAction + + ENTITY_DESCRIPTIONS: tuple[EcovacsButtonEntityDescription, ...] = ( EcovacsButtonEntityDescription( capability_fn=lambda caps: caps.map.relocation if caps.map else None, @@ -44,6 +56,16 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsButtonEntityDescription, ...] = ( ), ) +STATION_ENTITY_DESCRIPTIONS = tuple( + EcovacsStationActionButtonEntityDescription( + action=action, + key=f"station_action_{action.name.lower()}", + translation_key=f"station_action_{action.name.lower()}", + ) + for action in SUPPORTED_STATION_ACTIONS +) + + LIFESPAN_ENTITY_DESCRIPTIONS = tuple( EcovacsLifespanButtonEntityDescription( component=component, @@ -74,6 +96,15 @@ async def async_setup_entry( for description in LIFESPAN_ENTITY_DESCRIPTIONS if description.component in device.capabilities.life_span.types ) + entities.extend( + EcovacsStationActionButtonEntity( + device, device.capabilities.station.action, description + ) + for device in controller.devices + if device.capabilities.station + for description in STATION_ENTITY_DESCRIPTIONS + if description.action in device.capabilities.station.action.types + ) async_add_entities(entities) @@ -103,3 +134,18 @@ class EcovacsResetLifespanButtonEntity( await self._device.execute_command( self._capability.reset(self.entity_description.component) ) + + +class EcovacsStationActionButtonEntity( + EcovacsDescriptionEntity[CapabilityExecuteTypes[StationAction]], + ButtonEntity, +): + """Ecovacs station action button entity.""" + + entity_description: EcovacsStationActionButtonEntityDescription + + async def async_press(self) -> None: + """Press the button.""" + await self._device.execute_command( + self._capability.execute(self.entity_description.action) + ) diff --git a/homeassistant/components/ecovacs/const.py b/homeassistant/components/ecovacs/const.py index ac7a268f1bd..0bfe9cfd544 100644 --- a/homeassistant/components/ecovacs/const.py +++ b/homeassistant/components/ecovacs/const.py @@ -2,6 +2,7 @@ from enum import StrEnum +from deebot_client.commands import StationAction from deebot_client.events import LifeSpan DOMAIN = "ecovacs" @@ -19,8 +20,11 @@ SUPPORTED_LIFESPANS = ( LifeSpan.SIDE_BRUSH, LifeSpan.UNIT_CARE, LifeSpan.ROUND_MOP, + LifeSpan.STATION_FILTER, ) +SUPPORTED_STATION_ACTIONS = (StationAction.EMPTY_DUSTBIN,) + LEGACY_SUPPORTED_LIFESPANS = ( "main_brush", "side_brush", diff --git a/homeassistant/components/ecovacs/icons.json b/homeassistant/components/ecovacs/icons.json index 6097f43a4e4..b0e2a0595bf 100644 --- a/homeassistant/components/ecovacs/icons.json +++ b/homeassistant/components/ecovacs/icons.json @@ -27,11 +27,17 @@ "reset_lifespan_side_brush": { "default": "mdi:broom" }, + "reset_lifespan_station_filter": { + "default": "mdi:air-filter" + }, "reset_lifespan_unit_care": { "default": "mdi:robot-vacuum" }, "reset_lifespan_round_mop": { "default": "mdi:broom" + }, + "station_action_empty_dustbin": { + "default": "mdi:delete-restore" } }, "event": { @@ -72,6 +78,9 @@ "lifespan_side_brush": { "default": "mdi:broom" }, + "lifespan_station_filter": { + "default": "mdi:air-filter" + }, "lifespan_unit_care": { "default": "mdi:robot-vacuum" }, @@ -87,6 +96,9 @@ "network_ssid": { "default": "mdi:wifi" }, + "station_state": { + "default": "mdi:home" + }, "stats_area": { "default": "mdi:floor-plan" }, diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index 7c190d27775..0e906c6cb16 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -16,6 +16,7 @@ from deebot_client.events import ( NetworkInfoEvent, StatsEvent, TotalStatsEvent, + station, ) from sucks import VacBot @@ -46,7 +47,7 @@ from .entity import ( EcovacsLegacyEntity, EventT, ) -from .util import get_supported_entitites +from .util import get_name_key, get_options, get_supported_entitites @dataclass(kw_only=True, frozen=True) @@ -136,6 +137,15 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), + # Station + EcovacsSensorEntityDescription[station.StationEvent]( + capability_fn=lambda caps: caps.station.state if caps.station else None, + value_fn=lambda e: get_name_key(e.state), + key="station_state", + translation_key="station_state", + device_class=SensorDeviceClass.ENUM, + options=get_options(station.State), + ), ) diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index c9de461ad5b..723bdef17f8 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -46,6 +46,9 @@ "relocate": { "name": "Relocate" }, + "reset_lifespan_base_station_filter": { + "name": "Reset station filter lifespan" + }, "reset_lifespan_blade": { "name": "Reset blade lifespan" }, @@ -66,6 +69,9 @@ }, "reset_lifespan_side_brush": { "name": "Reset side brush lifespan" + }, + "station_action_empty_dustbin": { + "name": "Empty dustbin" } }, "event": { @@ -107,6 +113,9 @@ } } }, + "lifespan_base_station_filter": { + "name": "Station filter lifespan" + }, "lifespan_blade": { "name": "Blade lifespan" }, @@ -140,6 +149,13 @@ "network_ssid": { "name": "Wi-Fi SSID" }, + "station_state": { + "name": "Station state", + "state": { + "idle": "[%key:common::state::idle%]", + "emptying_dustbin": "Emptying dustbin" + } + }, "stats_area": { "name": "Area cleaned" }, diff --git a/homeassistant/components/ecovacs/util.py b/homeassistant/components/ecovacs/util.py index a4894de8968..0cfbf1e8f91 100644 --- a/homeassistant/components/ecovacs/util.py +++ b/homeassistant/components/ecovacs/util.py @@ -7,6 +7,8 @@ import random import string from typing import TYPE_CHECKING +from deebot_client.events.station import State + from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify @@ -47,4 +49,13 @@ def get_supported_entitites( @callback def get_name_key(enum: Enum) -> str: """Return the lower case name of the enum.""" + if enum is State.EMPTYING: + # Will be fixed in the next major release of deebot-client + return "emptying_dustbin" return enum.name.lower() + + +@callback +def get_options(enum: type[Enum]) -> list[str]: + """Return the options for the enum.""" + return [get_name_key(option) for option in enum] diff --git a/tests/components/ecovacs/fixtures/devices/qhe2o2/device.json b/tests/components/ecovacs/fixtures/devices/qhe2o2/device.json new file mode 100644 index 00000000000..0fbaaf896ee --- /dev/null +++ b/tests/components/ecovacs/fixtures/devices/qhe2o2/device.json @@ -0,0 +1,29 @@ +{ + "did": "8516fbb1-17f1-4194-0000001", + "name": "E1234567890000000003", + "class": "qhe2o2", + "resource": "NHl5", + "company": "eco-ng", + "bindTs": 1734792100110, + "service": { + "jmq": "jmq-ngiot-eu.dc.ww.ecouser.net", + "mqs": "api-ngiot.dc-eu.ww.ecouser.net" + }, + "deviceName": "DEEBOT N20 PRO PLUS", + "icon": "https: //portal-ww.ecouser.net/api/pim/file/get/0000001", + "ota": true, + "UILogicId": "y2_ww_h_y2h5", + "materialNo": "110-2406-0001", + "pid": "0000001", + "product_category": "DEEBOT", + "model": "Y2_AES_BLACK_INT", + "updateInfo": { + "needUpdate": false, + "changeLog": "" + }, + "nick": "Dusty", + "homeId": "1234567890abcdef12345678", + "homeSort": 1, + "status": 1, + "otaUpgrade": {} +} diff --git a/tests/components/ecovacs/snapshots/test_button.ambr b/tests/components/ecovacs/snapshots/test_button.ambr index efae8896962..f21d019a7b1 100644 --- a/tests/components/ecovacs/snapshots/test_button.ambr +++ b/tests/components/ecovacs/snapshots/test_button.ambr @@ -91,6 +91,328 @@ 'state': '2024-01-01T00:00:00+00:00', }) # --- +# name: test_buttons[qhe2o2][button.dusty_empty_dustbin:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.dusty_empty_dustbin', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Empty dustbin', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'station_action_empty_dustbin', + 'unique_id': '8516fbb1-17f1-4194-0000001_station_action_empty_dustbin', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[qhe2o2][button.dusty_empty_dustbin:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Empty dustbin', + }), + 'context': , + 'entity_id': 'button.dusty_empty_dustbin', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-01T00:00:00+00:00', + }) +# --- +# name: test_buttons[qhe2o2][button.dusty_relocate:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.dusty_relocate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Relocate', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'relocate', + 'unique_id': '8516fbb1-17f1-4194-0000001_relocate', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[qhe2o2][button.dusty_relocate:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Relocate', + }), + 'context': , + 'entity_id': 'button.dusty_relocate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-01T00:00:00+00:00', + }) +# --- +# name: test_buttons[qhe2o2][button.dusty_reset_filter_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.dusty_reset_filter_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset filter lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset_lifespan_filter', + 'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_filter', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[qhe2o2][button.dusty_reset_filter_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Reset filter lifespan', + }), + 'context': , + 'entity_id': 'button.dusty_reset_filter_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-01T00:00:00+00:00', + }) +# --- +# name: test_buttons[qhe2o2][button.dusty_reset_main_brush_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.dusty_reset_main_brush_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset main brush lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset_lifespan_brush', + 'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_brush', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[qhe2o2][button.dusty_reset_main_brush_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Reset main brush lifespan', + }), + 'context': , + 'entity_id': 'button.dusty_reset_main_brush_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-01T00:00:00+00:00', + }) +# --- +# name: test_buttons[qhe2o2][button.dusty_reset_round_mop_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.dusty_reset_round_mop_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset round mop lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset_lifespan_round_mop', + 'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_round_mop', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[qhe2o2][button.dusty_reset_round_mop_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Reset round mop lifespan', + }), + 'context': , + 'entity_id': 'button.dusty_reset_round_mop_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-01T00:00:00+00:00', + }) +# --- +# name: test_buttons[qhe2o2][button.dusty_reset_side_brush_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.dusty_reset_side_brush_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset side brush lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset_lifespan_side_brush', + 'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_side_brush', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[qhe2o2][button.dusty_reset_side_brush_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Reset side brush lifespan', + }), + 'context': , + 'entity_id': 'button.dusty_reset_side_brush_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-01T00:00:00+00:00', + }) +# --- +# name: test_buttons[qhe2o2][button.dusty_reset_unit_care_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.dusty_reset_unit_care_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset unit care lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset_lifespan_unit_care', + 'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_unit_care', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[qhe2o2][button.dusty_reset_unit_care_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Reset unit care lifespan', + }), + 'context': , + 'entity_id': 'button.dusty_reset_unit_care_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-01T00:00:00+00:00', + }) +# --- # name: test_buttons[yna5x1][button.ozmo_950_relocate:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index 9c76c00b5b7..755fcda9e7d 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -725,6 +725,781 @@ 'state': 'Testnetwork', }) # --- +# name: test_sensors[qhe2o2][sensor.dusty_area_cleaned:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dusty_area_cleaned', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Area cleaned', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'stats_area', + 'unique_id': '8516fbb1-17f1-4194-0000001_stats_area', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_area_cleaned:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Area cleaned', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dusty_area_cleaned', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_battery:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dusty_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '8516fbb1-17f1-4194-0000001_battery_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_battery:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Dusty Battery', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dusty_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_cleaning_duration:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dusty_cleaning_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cleaning duration', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'stats_time', + 'unique_id': '8516fbb1-17f1-4194-0000001_stats_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_cleaning_duration:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Dusty Cleaning duration', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dusty_cleaning_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_error:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dusty_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Error', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'error', + 'unique_id': '8516fbb1-17f1-4194-0000001_error', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_error:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'description': 'NoError: Robot is operational', + 'friendly_name': 'Dusty Error', + }), + 'context': , + 'entity_id': 'sensor.dusty_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_filter_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dusty_filter_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Filter lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifespan_filter', + 'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_filter', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_filter_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Filter lifespan', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dusty_filter_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '56', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_ip_address:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dusty_ip_address', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'IP address', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_ip', + 'unique_id': '8516fbb1-17f1-4194-0000001_network_ip', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_ip_address:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty IP address', + }), + 'context': , + 'entity_id': 'sensor.dusty_ip_address', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '192.168.0.10', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_main_brush_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dusty_main_brush_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Main brush lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifespan_brush', + 'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_brush', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_main_brush_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Main brush lifespan', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dusty_main_brush_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_round_mop_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dusty_round_mop_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Round mop lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifespan_round_mop', + 'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_round_mop', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_round_mop_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Round mop lifespan', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dusty_round_mop_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_side_brush_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dusty_side_brush_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Side brush lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifespan_side_brush', + 'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_side_brush', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_side_brush_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Side brush lifespan', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dusty_side_brush_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_station_state:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'idle', + 'emptying_dustbin', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dusty_station_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Station state', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'station_state', + 'unique_id': '8516fbb1-17f1-4194-0000001_station_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_station_state:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Dusty Station state', + 'options': list([ + 'idle', + 'emptying_dustbin', + ]), + }), + 'context': , + 'entity_id': 'sensor.dusty_station_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'emptying_dustbin', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_total_area_cleaned:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dusty_total_area_cleaned', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total area cleaned', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_stats_area', + 'unique_id': '8516fbb1-17f1-4194-0000001_total_stats_area', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_total_area_cleaned:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Total area cleaned', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dusty_total_area_cleaned', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_total_cleaning_duration:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dusty_total_cleaning_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total cleaning duration', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_stats_time', + 'unique_id': '8516fbb1-17f1-4194-0000001_total_stats_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_total_cleaning_duration:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Dusty Total cleaning duration', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dusty_total_cleaning_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.000', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_total_cleanings:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dusty_total_cleanings', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total cleanings', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_stats_cleanings', + 'unique_id': '8516fbb1-17f1-4194-0000001_total_stats_cleanings', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_total_cleanings:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Total cleanings', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.dusty_total_cleanings', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '123', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_unit_care_lifespan:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dusty_unit_care_lifespan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Unit care lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifespan_unit_care', + 'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_unit_care', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_unit_care_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Unit care lifespan', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dusty_unit_care_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_wi_fi_rssi:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dusty_wi_fi_rssi', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wi-Fi RSSI', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_rssi', + 'unique_id': '8516fbb1-17f1-4194-0000001_network_rssi', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_wi_fi_rssi:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Wi-Fi RSSI', + }), + 'context': , + 'entity_id': 'sensor.dusty_wi_fi_rssi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-62', + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_wi_fi_ssid:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dusty_wi_fi_ssid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wi-Fi SSID', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_ssid', + 'unique_id': '8516fbb1-17f1-4194-0000001_network_ssid', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[qhe2o2][sensor.dusty_wi_fi_ssid:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dusty Wi-Fi SSID', + }), + 'context': , + 'entity_id': 'sensor.dusty_wi_fi_ssid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Testnetwork', + }) +# --- # name: test_sensors[yna5x1][sensor.ozmo_950_area_cleaned:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/ecovacs/test_button.py b/tests/components/ecovacs/test_button.py index 4b3068f6cda..65e0b19ea02 100644 --- a/tests/components/ecovacs/test_button.py +++ b/tests/components/ecovacs/test_button.py @@ -1,7 +1,12 @@ """Tests for Ecovacs sensors.""" from deebot_client.command import Command -from deebot_client.commands.json import ResetLifeSpan, SetRelocationState +from deebot_client.commands import StationAction +from deebot_client.commands.json import ( + ResetLifeSpan, + SetRelocationState, + station_action, +) from deebot_client.events import LifeSpan import pytest from syrupy import SnapshotAssertion @@ -60,8 +65,38 @@ def platforms() -> Platform | list[Platform]: ), ], ), + ( + "qhe2o2", + [ + ("button.dusty_relocate", SetRelocationState()), + ( + "button.dusty_reset_main_brush_lifespan", + ResetLifeSpan(LifeSpan.BRUSH), + ), + ( + "button.dusty_reset_filter_lifespan", + ResetLifeSpan(LifeSpan.FILTER), + ), + ( + "button.dusty_reset_side_brush_lifespan", + ResetLifeSpan(LifeSpan.SIDE_BRUSH), + ), + ( + "button.dusty_reset_unit_care_lifespan", + ResetLifeSpan(LifeSpan.UNIT_CARE), + ), + ( + "button.dusty_reset_round_mop_lifespan", + ResetLifeSpan(LifeSpan.ROUND_MOP), + ), + ( + "button.dusty_empty_dustbin", + station_action.StationAction(StationAction.EMPTY_DUSTBIN), + ), + ], + ), ], - ids=["yna5x1", "5xu9h3"], + ids=["yna5x1", "5xu9h3", "qhe2o2"], ) async def test_buttons( hass: HomeAssistant, diff --git a/tests/components/ecovacs/test_sensor.py b/tests/components/ecovacs/test_sensor.py index 53c57999776..5bcd8385320 100644 --- a/tests/components/ecovacs/test_sensor.py +++ b/tests/components/ecovacs/test_sensor.py @@ -11,6 +11,7 @@ from deebot_client.events import ( NetworkInfoEvent, StatsEvent, TotalStatsEvent, + station, ) import pytest from syrupy import SnapshotAssertion @@ -45,6 +46,7 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus): event_bus.notify(LifeSpanEvent(LifeSpan.FILTER, 56, 40 * 60)) event_bus.notify(LifeSpanEvent(LifeSpan.SIDE_BRUSH, 40, 20 * 60)) event_bus.notify(ErrorEvent(0, "NoError: Robot is operational")) + event_bus.notify(station.StationEvent(station.State.EMPTYING)) await block_till_done(hass, event_bus) @@ -87,8 +89,29 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus): "sensor.goat_g1_error", ], ), + ( + "qhe2o2", + [ + "sensor.dusty_area_cleaned", + "sensor.dusty_cleaning_duration", + "sensor.dusty_total_area_cleaned", + "sensor.dusty_total_cleaning_duration", + "sensor.dusty_total_cleanings", + "sensor.dusty_battery", + "sensor.dusty_ip_address", + "sensor.dusty_wi_fi_rssi", + "sensor.dusty_wi_fi_ssid", + "sensor.dusty_station_state", + "sensor.dusty_main_brush_lifespan", + "sensor.dusty_filter_lifespan", + "sensor.dusty_side_brush_lifespan", + "sensor.dusty_unit_care_lifespan", + "sensor.dusty_round_mop_lifespan", + "sensor.dusty_error", + ], + ), ], - ids=["yna5x1", "5xu9h3"], + ids=["yna5x1", "5xu9h3", "qhe2o2"], ) async def test_sensors( hass: HomeAssistant, @@ -99,7 +122,7 @@ async def test_sensors( entity_ids: list[str], ) -> None: """Test that sensor entity snapshots match.""" - assert entity_ids == hass.states.async_entity_ids() + assert hass.states.async_entity_ids() == entity_ids for entity_id in entity_ids: assert (state := hass.states.get(entity_id)), f"State of {entity_id} is missing" assert state.state == STATE_UNKNOWN