Use dedicated translation on sensor entities if it's Ecovacs mower (#179416)

This commit is contained in:
Robert Resch
2026-08-18 13:52:26 +02:00
committed by GitHub
parent e4a52c0dd5
commit 64419c6435
5 changed files with 186 additions and 120 deletions
@@ -176,17 +176,32 @@
"stats_area": {
"default": "mdi:floor-plan"
},
"stats_area_mower": {
"default": "mdi:floor-plan"
},
"stats_time": {
"default": "mdi:timer-outline"
},
"stats_time_mower": {
"default": "mdi:timer-outline"
},
"total_stats_area": {
"default": "mdi:floor-plan"
},
"total_stats_area_mower": {
"default": "mdi:floor-plan"
},
"total_stats_cleanings": {
"default": "mdi:counter"
},
"total_stats_cleanings_mower": {
"default": "mdi:counter"
},
"total_stats_time": {
"default": "mdi:timer-outline"
},
"total_stats_time_mower": {
"default": "mdi:timer-outline"
}
},
"switch": {
+61 -25
View File
@@ -1,8 +1,8 @@
"""Ecovacs sensor module."""
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, override
from collections.abc import Callable, Mapping
from dataclasses import dataclass, field, fields, replace
from typing import Any, Self, override
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
from deebot_client.device import Device
@@ -33,10 +33,10 @@ from homeassistant.const import (
UnitOfArea,
UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.typing import UNDEFINED, StateType, UndefinedType
from . import EcovacsConfigEntry
from .const import LEGACY_SUPPORTED_LIFESPANS, SUPPORTED_LIFESPANS
@@ -49,6 +49,14 @@ from .entity import (
from .util import get_name_key, get_options, get_supported_entities
@dataclass(kw_only=True, frozen=True)
class EcovacsSensorDeviceTypeOverride:
"""Description values, which differ for a specific device type."""
native_unit_of_measurement: str | UndefinedType | None = UNDEFINED
translation_key: str | UndefinedType | None = UNDEFINED
@dataclass(kw_only=True, frozen=True)
class EcovacsSensorEntityDescription[EventT: Event](
EcovacsCapabilityEntityDescription,
@@ -57,15 +65,23 @@ class EcovacsSensorEntityDescription[EventT: Event](
"""Ecovacs sensor entity description."""
value_fn: Callable[[EventT], StateType]
native_unit_of_measurement_fn: Callable[[DeviceType], str | None] | None = None
device_type_overrides: Mapping[DeviceType, EcovacsSensorDeviceTypeOverride] = field(
default_factory=dict
)
def get_for(self, device: DeviceType) -> Self:
"""Get entity description for specific device type."""
if (overrides := self.device_type_overrides.get(device)) is None:
return self
@callback
def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None:
"""Get the area native unit of measurement based on device type."""
if device_type is DeviceType.MOWER:
return UnitOfArea.SQUARE_CENTIMETERS
return UnitOfArea.SQUARE_METERS
return replace(
self,
**{
f.name: value
for f in fields(overrides)
if (value := getattr(overrides, f.name)) is not UNDEFINED
},
)
ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
@@ -76,8 +92,14 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
value_fn=lambda e: e.area,
translation_key="stats_area",
device_class=SensorDeviceClass.AREA,
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS,
device_type_overrides={
DeviceType.MOWER: EcovacsSensorDeviceTypeOverride(
native_unit_of_measurement=UnitOfArea.SQUARE_CENTIMETERS,
translation_key="stats_area_mower",
)
},
),
EcovacsSensorEntityDescription[StatsEvent](
key="stats_time",
@@ -87,6 +109,11 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.MINUTES,
device_type_overrides={
DeviceType.MOWER: EcovacsSensorDeviceTypeOverride(
translation_key="stats_time_mower",
)
},
),
# TotalStats
EcovacsSensorEntityDescription[TotalStatsEvent](
@@ -97,6 +124,11 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.AREA,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
state_class=SensorStateClass.TOTAL_INCREASING,
device_type_overrides={
DeviceType.MOWER: EcovacsSensorDeviceTypeOverride(
translation_key="total_stats_area_mower",
)
},
),
EcovacsSensorEntityDescription[TotalStatsEvent](
capability_fn=lambda caps: caps.stats.total,
@@ -107,6 +139,11 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=SensorStateClass.TOTAL_INCREASING,
device_type_overrides={
DeviceType.MOWER: EcovacsSensorDeviceTypeOverride(
translation_key="total_stats_time_mower",
)
},
),
EcovacsSensorEntityDescription[TotalStatsEvent](
capability_fn=lambda caps: caps.stats.total,
@@ -114,6 +151,11 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
key="total_stats_cleanings",
translation_key="total_stats_cleanings",
state_class=SensorStateClass.TOTAL_INCREASING,
device_type_overrides={
DeviceType.MOWER: EcovacsSensorDeviceTypeOverride(
translation_key="total_stats_cleanings_mower",
)
},
),
EcovacsSensorEntityDescription[BatteryEvent](
capability_fn=lambda caps: caps.battery,
@@ -274,18 +316,12 @@ class EcovacsSensor(
**kwargs: Any,
) -> None:
"""Initialize entity."""
super().__init__(device, capability, entity_description, **kwargs)
if (
entity_description.native_unit_of_measurement_fn
and (
native_unit_of_measurement
:= entity_description.native_unit_of_measurement_fn(
device.capabilities.device_type
)
)
is not None
):
self._attr_native_unit_of_measurement = native_unit_of_measurement
super().__init__(
device,
capability,
entity_description.get_for(device.capabilities.device_type),
**kwargs,
)
@override
async def async_added_to_hass(self) -> None:
@@ -268,17 +268,32 @@
"stats_area": {
"name": "Area cleaned"
},
"stats_area_mower": {
"name": "Area mowed"
},
"stats_time": {
"name": "Cleaning duration"
},
"stats_time_mower": {
"name": "Mowing duration"
},
"total_stats_area": {
"name": "Total area cleaned"
},
"total_stats_area_mower": {
"name": "Total area mowed"
},
"total_stats_cleanings": {
"name": "Total cleanings"
},
"total_stats_cleanings_mower": {
"name": "Total mowings"
},
"total_stats_time": {
"name": "Total cleaning duration"
},
"total_stats_time_mower": {
"name": "Total mowing duration"
}
},
"switch": {
@@ -213,7 +213,7 @@
'sensor.e1234567890000000003_filter_lifespan',
])
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_area_cleaned:entity-registry]
# name: test_sensors[5xu9h3][sensor.goat_g1_area_mowed:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -227,7 +227,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.goat_g1_area_cleaned',
'entity_id': 'sensor.goat_g1_area_mowed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -235,7 +235,7 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Area cleaned',
'object_id_base': 'Area mowed',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
@@ -246,25 +246,25 @@
}),
'original_device_class': <SensorDeviceClass.AREA: 'area'>,
'original_icon': None,
'original_name': 'Area cleaned',
'original_name': 'Area mowed',
'platform': 'ecovacs',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'stats_area',
'translation_key': 'stats_area_mower',
'unique_id': '8516fbb1-17f1-4194-0000000_stats_area',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_area_cleaned:state]
# name: test_sensors[5xu9h3][sensor.goat_g1_area_mowed:state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'area',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Goat G1 Area cleaned',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Goat G1 Area mowed',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfArea.SQUARE_METERS: 'm²'>,
}),
'context': <ANY>,
'entity_id': 'sensor.goat_g1_area_cleaned',
'entity_id': 'sensor.goat_g1_area_mowed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -374,64 +374,6 @@
'state': 'unknown',
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_cleaning_duration:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.goat_g1_cleaning_duration',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Cleaning duration',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Cleaning duration',
'platform': 'ecovacs',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'stats_time',
'unique_id': '8516fbb1-17f1-4194-0000000_stats_time',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_cleaning_duration:state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'duration',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Goat G1 Cleaning duration',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.goat_g1_cleaning_duration',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '5.0',
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_error:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -584,7 +526,65 @@
'state': 'unknown',
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_total_area_cleaned:entity-registry]
# name: test_sensors[5xu9h3][sensor.goat_g1_mowing_duration:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.goat_g1_mowing_duration',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Mowing duration',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Mowing duration',
'platform': 'ecovacs',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'stats_time_mower',
'unique_id': '8516fbb1-17f1-4194-0000000_stats_time',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_mowing_duration:state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'duration',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Goat G1 Mowing duration',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.goat_g1_mowing_duration',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '5.0',
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_total_area_mowed:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -600,7 +600,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.goat_g1_total_area_cleaned',
'entity_id': 'sensor.goat_g1_total_area_mowed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -608,7 +608,7 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Total area cleaned',
'object_id_base': 'Total area mowed',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
@@ -616,33 +616,33 @@
}),
'original_device_class': <SensorDeviceClass.AREA: 'area'>,
'original_icon': None,
'original_name': 'Total area cleaned',
'original_name': 'Total area mowed',
'platform': 'ecovacs',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'total_stats_area',
'translation_key': 'total_stats_area_mower',
'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_area',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_total_area_cleaned:state]
# name: test_sensors[5xu9h3][sensor.goat_g1_total_area_mowed:state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'area',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Goat G1 Total area cleaned',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Goat G1 Total area mowed',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfArea.SQUARE_METERS: 'm²'>,
}),
'context': <ANY>,
'entity_id': 'sensor.goat_g1_total_area_cleaned',
'entity_id': 'sensor.goat_g1_total_area_mowed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '60',
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_total_cleaning_duration:entity-registry]
# name: test_sensors[5xu9h3][sensor.goat_g1_total_mowing_duration:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -658,7 +658,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.goat_g1_total_cleaning_duration',
'entity_id': 'sensor.goat_g1_total_mowing_duration',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -666,7 +666,7 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Total cleaning duration',
'object_id_base': 'Total mowing duration',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
@@ -677,33 +677,33 @@
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Total cleaning duration',
'original_name': 'Total mowing duration',
'platform': 'ecovacs',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'total_stats_time',
'translation_key': 'total_stats_time_mower',
'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_time',
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_total_cleaning_duration:state]
# name: test_sensors[5xu9h3][sensor.goat_g1_total_mowing_duration:state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'duration',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Goat G1 Total cleaning duration',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Goat G1 Total mowing duration',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.goat_g1_total_cleaning_duration',
'entity_id': 'sensor.goat_g1_total_mowing_duration',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '40.0',
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_total_cleanings:entity-registry]
# name: test_sensors[5xu9h3][sensor.goat_g1_total_mowings:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -719,7 +719,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.goat_g1_total_cleanings',
'entity_id': 'sensor.goat_g1_total_mowings',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -727,29 +727,29 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Total cleanings',
'object_id_base': 'Total mowings',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Total cleanings',
'original_name': 'Total mowings',
'platform': 'ecovacs',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'total_stats_cleanings',
'translation_key': 'total_stats_cleanings_mower',
'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_cleanings',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_total_cleanings:state]
# name: test_sensors[5xu9h3][sensor.goat_g1_total_mowings:state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Goat G1 Total cleanings',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Goat G1 Total mowings',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'context': <ANY>,
'entity_id': 'sensor.goat_g1_total_cleanings',
'entity_id': 'sensor.goat_g1_total_mowings',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
+5 -5
View File
@@ -77,11 +77,11 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus):
(
"5xu9h3",
[
"sensor.goat_g1_area_cleaned",
"sensor.goat_g1_cleaning_duration",
"sensor.goat_g1_total_area_cleaned",
"sensor.goat_g1_total_cleaning_duration",
"sensor.goat_g1_total_cleanings",
"sensor.goat_g1_area_mowed",
"sensor.goat_g1_mowing_duration",
"sensor.goat_g1_total_area_mowed",
"sensor.goat_g1_total_mowing_duration",
"sensor.goat_g1_total_mowings",
"sensor.goat_g1_battery",
"sensor.goat_g1_ip_address",
"sensor.goat_g1_wi_fi_rssi",