1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 09:38:58 +01:00

Add Ecovacs active map select entity (#153748)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Robert Resch
2025-10-06 14:42:01 +02:00
committed by GitHub
parent ac79b3072e
commit 553d896899
6 changed files with 345 additions and 6 deletions
@@ -116,6 +116,9 @@
}
},
"select": {
"active_map": {
"default": "mdi:floor-plan"
},
"water_amount": {
"default": "mdi:water"
},
+87 -3
View File
@@ -2,12 +2,13 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from typing import TYPE_CHECKING, Any
from deebot_client.capabilities import CapabilitySetTypes
from deebot_client.capabilities import CapabilityMap, CapabilitySet, CapabilitySetTypes
from deebot_client.device import Device
from deebot_client.events import WorkModeEvent
from deebot_client.events.base import Event
from deebot_client.events.map import CachedMapInfoEvent, MajorMapEvent
from deebot_client.events.water_info import WaterAmountEvent
from homeassistant.components.select import SelectEntity, SelectEntityDescription
@@ -16,7 +17,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,
EcovacsEntity,
)
from .util import get_name_key, get_supported_entities
@@ -66,6 +71,12 @@ async def async_setup_entry(
entities = get_supported_entities(
controller, EcovacsSelectEntity, ENTITY_DESCRIPTIONS
)
entities.extend(
EcovacsActiveMapSelectEntity(device, device.capabilities.map)
for device in controller.devices
if (map_cap := device.capabilities.map)
and isinstance(map_cap.major, CapabilitySet)
)
if entities:
async_add_entities(entities)
@@ -103,3 +114,76 @@ class EcovacsSelectEntity[EventT: Event](
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await self._device.execute_command(self._capability.set(option))
class EcovacsActiveMapSelectEntity(
EcovacsEntity[CapabilityMap],
SelectEntity,
):
"""Ecovacs active map select entity."""
entity_description = SelectEntityDescription(
key="active_map",
translation_key="active_map",
entity_category=EntityCategory.CONFIG,
)
def __init__(
self,
device: Device,
capability: CapabilityMap,
**kwargs: Any,
) -> None:
"""Initialize entity."""
super().__init__(device, capability, **kwargs)
self._option_to_id: dict[str, str] = {}
self._id_to_option: dict[str, str] = {}
self._handle_on_cached_map(
device.events.get_last_event(CachedMapInfoEvent)
or CachedMapInfoEvent(set())
)
def _handle_on_cached_map(self, event: CachedMapInfoEvent) -> None:
self._id_to_option.clear()
self._option_to_id.clear()
for map_info in event.maps:
name = map_info.name if map_info.name else map_info.id
self._id_to_option[map_info.id] = name
self._option_to_id[name] = map_info.id
if map_info.using:
self._attr_current_option = name
if self._attr_current_option not in self._option_to_id:
self._attr_current_option = None
# Sort named maps first, then numeric IDs (unnamed maps during building) in ascending order.
self._attr_options = sorted(
self._option_to_id.keys(), key=lambda x: (x.isdigit(), x.lower())
)
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_cached_map(event: CachedMapInfoEvent) -> None:
self._handle_on_cached_map(event)
self.async_write_ha_state()
self._subscribe(self._capability.cached_info.event, on_cached_map)
async def on_major_map(event: MajorMapEvent) -> None:
self._attr_current_option = self._id_to_option.get(event.map_id)
self.async_write_ha_state()
self._subscribe(self._capability.major.event, on_major_map)
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
if TYPE_CHECKING:
assert isinstance(self._capability.major, CapabilitySet)
await self._device.execute_command(
self._capability.major.set(self._option_to_id[option])
)
@@ -178,6 +178,9 @@
}
},
"select": {
"active_map": {
"name": "Active map"
},
"water_amount": {
"name": "[%key:component::ecovacs::entity::number::water_amount::name%]",
"state": {
@@ -1,5 +1,62 @@
# serializer version: 1
# name: test_selects[n0vyif-entity_ids1][select.x8_pro_omni_work_mode:entity-registry]
# name: test_selects[n0vyif-entity_ids2][select.x8_pro_omni_active_map:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'Map 2',
'1',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.x8_pro_omni_active_map',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Active map',
'platform': 'ecovacs',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'active_map',
'unique_id': 'E1234567890000000009_active_map',
'unit_of_measurement': None,
})
# ---
# name: test_selects[n0vyif-entity_ids2][select.x8_pro_omni_active_map:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'X8 PRO OMNI Active map',
'options': list([
'Map 2',
'1',
]),
}),
'context': <ANY>,
'entity_id': 'select.x8_pro_omni_active_map',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'Map 2',
})
# ---
# name: test_selects[n0vyif-entity_ids2][select.x8_pro_omni_work_mode:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -41,7 +98,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_selects[n0vyif-entity_ids1][select.x8_pro_omni_work_mode:state]
# name: test_selects[n0vyif-entity_ids2][select.x8_pro_omni_work_mode:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'X8 PRO OMNI Work mode',
@@ -60,6 +117,179 @@
'state': 'vacuum',
})
# ---
# name: test_selects[qhe2o2-entity_ids1][select.dusty_active_map:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'Map 2',
'1',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.dusty_active_map',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Active map',
'platform': 'ecovacs',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'active_map',
'unique_id': '8516fbb1-17f1-4194-0000001_active_map',
'unit_of_measurement': None,
})
# ---
# name: test_selects[qhe2o2-entity_ids1][select.dusty_active_map:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Active map',
'options': list([
'Map 2',
'1',
]),
}),
'context': <ANY>,
'entity_id': 'select.dusty_active_map',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'Map 2',
})
# ---
# name: test_selects[qhe2o2-entity_ids1][select.dusty_water_flow_level:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'low',
'medium',
'high',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.dusty_water_flow_level',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Water flow level',
'platform': 'ecovacs',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'water_amount',
'unique_id': '8516fbb1-17f1-4194-0000001_water_amount',
'unit_of_measurement': None,
})
# ---
# name: test_selects[qhe2o2-entity_ids1][select.dusty_water_flow_level:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dusty Water flow level',
'options': list([
'low',
'medium',
'high',
]),
}),
'context': <ANY>,
'entity_id': 'select.dusty_water_flow_level',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_active_map:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'Map 2',
'1',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.ozmo_950_active_map',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Active map',
'platform': 'ecovacs',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'active_map',
'unique_id': 'E1234567890000000001_active_map',
'unit_of_measurement': None,
})
# ---
# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_active_map:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Ozmo 950 Active map',
'options': list([
'Map 2',
'1',
]),
}),
'context': <ANY>,
'entity_id': 'select.ozmo_950_active_map',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'Map 2',
})
# ---
# name: test_selects[yna5x1-entity_ids0][select.ozmo_950_water_flow_level:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
+1 -1
View File
@@ -105,7 +105,7 @@ async def test_devices_in_dr(
@pytest.mark.parametrize(
("device_fixture", "entities"),
[
("yna5x1", 26),
("yna5x1", 27),
("5xu9h3", 25),
("123", 3),
],
+19
View File
@@ -3,6 +3,7 @@
from deebot_client.command import Command
from deebot_client.commands.json import SetWaterInfo
from deebot_client.event_bus import EventBus
from deebot_client.events.map import CachedMapInfoEvent, MajorMapEvent, Map
from deebot_client.events.water_info import WaterAmount, WaterAmountEvent
from deebot_client.events.work_mode import WorkMode, WorkModeEvent
import pytest
@@ -36,6 +37,15 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus):
"""Notify events."""
event_bus.notify(WaterAmountEvent(WaterAmount.ULTRAHIGH))
event_bus.notify(WorkModeEvent(WorkMode.VACUUM))
event_bus.notify(
CachedMapInfoEvent(
{
Map(id="1", name="", using=False, built=False),
Map(id="2", name="Map 2", using=True, built=True),
}
)
)
event_bus.notify(MajorMapEvent("2", [], requested=False))
await block_till_done(hass, event_bus)
@@ -47,12 +57,21 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus):
"yna5x1",
[
"select.ozmo_950_water_flow_level",
"select.ozmo_950_active_map",
],
),
(
"qhe2o2",
[
"select.dusty_water_flow_level",
"select.dusty_active_map",
],
),
(
"n0vyif",
[
"select.x8_pro_omni_work_mode",
"select.x8_pro_omni_active_map",
],
),
],