Add work area sensors to Husqvarna Automower (#126931)

* Add work area sensors to Husqvarna Automower

* add exists function

* fix tests

* add icons

* docstring

* Update homeassistant/components/husqvarna_automower/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Thomas55555 2024-10-06 16:10:26 +02:00 committed by GitHub
parent e705ca83b2
commit 3cda93d001
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 318 additions and 16 deletions

View file

@ -155,8 +155,8 @@ class AutomowerControlEntity(AutomowerAvailableEntity):
return super().available and _check_error_free(self.mower_attributes)
class WorkAreaControlEntity(AutomowerControlEntity):
"""Base entity work work areas with control function."""
class WorkAreaAvailableEntity(AutomowerAvailableEntity):
"""Base entity for work work areas."""
def __init__(
self,
@ -184,3 +184,7 @@ class WorkAreaControlEntity(AutomowerControlEntity):
def available(self) -> bool:
"""Return True if the work area is available and the mower has no errors."""
return super().available and self.work_area_id in self.work_areas
class WorkAreaControlEntity(WorkAreaAvailableEntity, AutomowerControlEntity):
"""Base entity work work areas with control function."""

View file

@ -27,6 +27,12 @@
"error": {
"default": "mdi:alert-circle-outline"
},
"my_lawn_last_time_completed": {
"default": "mdi:clock-outline"
},
"my_lawn_progress": {
"default": "mdi:collage"
},
"number_of_charging_cycles": {
"default": "mdi:battery-sync-outline"
},
@ -35,6 +41,12 @@
},
"restricted_reason": {
"default": "mdi:tooltip-question"
},
"work_area_last_time_completed": {
"default": "mdi:clock-outline"
},
"work_area_progress": {
"default": "mdi:collage"
}
}
},

View file

@ -12,6 +12,7 @@ from aioautomower.model import (
MowerModes,
MowerStates,
RestrictedReasons,
WorkArea,
)
from aioautomower.utils import naive_to_aware
@ -29,7 +30,11 @@ from homeassistant.util import dt as dt_util
from . import AutomowerConfigEntry
from .coordinator import AutomowerDataUpdateCoordinator
from .entity import AutomowerBaseEntity
from .entity import (
AutomowerBaseEntity,
WorkAreaAvailableEntity,
_work_area_translation_key,
)
_LOGGER = logging.getLogger(__name__)
@ -261,7 +266,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription):
value_fn: Callable[[MowerAttributes], StateType | datetime]
SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
AutomowerSensorEntityDescription(
key="battery_percent",
state_class=SensorStateClass.MEASUREMENT,
@ -396,6 +401,37 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
)
@dataclass(frozen=True, kw_only=True)
class WorkAreaSensorEntityDescription(SensorEntityDescription):
"""Describes the work area sensor entities."""
exists_fn: Callable[[WorkArea], bool] = lambda _: True
value_fn: Callable[[WorkArea], StateType | datetime]
translation_key_fn: Callable[[int, str], str]
WORK_AREA_SENSOR_TYPES: tuple[WorkAreaSensorEntityDescription, ...] = (
WorkAreaSensorEntityDescription(
key="progress",
translation_key_fn=_work_area_translation_key,
exists_fn=lambda data: data.progress is not None,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: data.progress,
),
WorkAreaSensorEntityDescription(
key="last_time_completed",
translation_key_fn=_work_area_translation_key,
exists_fn=lambda data: data.last_time_completed_naive is not None,
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: naive_to_aware(
data.last_time_completed_naive,
ZoneInfo(str(dt_util.DEFAULT_TIME_ZONE)),
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AutomowerConfigEntry,
@ -403,12 +439,25 @@ async def async_setup_entry(
) -> None:
"""Set up sensor platform."""
coordinator = entry.runtime_data
async_add_entities(
AutomowerSensorEntity(mower_id, coordinator, description)
for mower_id in coordinator.data
for description in SENSOR_TYPES
if description.exists_fn(coordinator.data[mower_id])
)
entities: list[SensorEntity] = []
for mower_id in coordinator.data:
if coordinator.data[mower_id].capabilities.work_areas:
_work_areas = coordinator.data[mower_id].work_areas
if _work_areas is not None:
entities.extend(
WorkAreaSensorEntity(
mower_id, coordinator, description, work_area_id
)
for description in WORK_AREA_SENSOR_TYPES
for work_area_id in _work_areas
if description.exists_fn(_work_areas[work_area_id])
)
entities.extend(
AutomowerSensorEntity(mower_id, coordinator, description)
for description in MOWER_SENSOR_TYPES
if description.exists_fn(coordinator.data[mower_id])
)
async_add_entities(entities)
class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity):
@ -442,3 +491,36 @@ class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity):
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the state attributes."""
return self.entity_description.extra_state_attributes_fn(self.mower_attributes)
class WorkAreaSensorEntity(WorkAreaAvailableEntity, SensorEntity):
"""Defining the Work area sensors with WorkAreaSensorEntityDescription."""
entity_description: WorkAreaSensorEntityDescription
def __init__(
self,
mower_id: str,
coordinator: AutomowerDataUpdateCoordinator,
description: WorkAreaSensorEntityDescription,
work_area_id: int,
) -> None:
"""Set up AutomowerSensors."""
super().__init__(mower_id, coordinator, work_area_id)
self.entity_description = description
self._attr_unique_id = f"{mower_id}_{work_area_id}_{description.key}"
self._attr_translation_placeholders = {
"work_area": self.work_area_attributes.name
}
@property
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.work_area_attributes)
@property
def translation_key(self) -> str:
"""Return the translation key of the work area."""
return self.entity_description.translation_key_fn(
self.work_area_id, self.entity_description.key
)

View file

@ -204,6 +204,12 @@
"zone_generator_problem": "Zone generator problem"
}
},
"my_lawn_last_time_completed": {
"name": "My lawn last time completed"
},
"my_lawn_progress": {
"name": "My lawn progress"
},
"number_of_charging_cycles": {
"name": "Number of charging cycles"
},
@ -266,6 +272,12 @@
"name": "Work area ID assignment"
}
}
},
"work_area_last_time_completed": {
"name": "{work_area} last time completed"
},
"work_area_progress": {
"name": "{work_area} progress"
}
},
"switch": {

View file

@ -105,9 +105,7 @@
"workAreaId": 654321,
"name": "Back lawn",
"cuttingHeight": 25,
"enabled": true,
"progress": 30,
"lastTimeCompleted": 1722449269
"enabled": true
},
{
"workAreaId": 0,

View file

@ -152,9 +152,9 @@
'654321': dict({
'cutting_height': 25,
'enabled': True,
'last_time_completed_naive': '2024-07-31T18:07:49',
'last_time_completed_naive': None,
'name': 'Back lawn',
'progress': 30,
'progress': None,
}),
}),
})

View file

@ -448,6 +448,103 @@
'state': 'no_error',
})
# ---
# name: test_sensor_snapshot[sensor.test_mower_1_front_lawn_last_time_completed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_mower_1_front_lawn_last_time_completed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Front lawn last time completed',
'platform': 'husqvarna_automower',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'work_area_last_time_completed',
'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_123456_last_time_completed',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_snapshot[sensor.test_mower_1_front_lawn_last_time_completed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'Test Mower 1 Front lawn last time completed',
}),
'context': <ANY>,
'entity_id': 'sensor.test_mower_1_front_lawn_last_time_completed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-08-12T05:54:29+00:00',
})
# ---
# name: test_sensor_snapshot[sensor.test_mower_1_front_lawn_progress-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_mower_1_front_lawn_progress',
'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': 'Front lawn progress',
'platform': 'husqvarna_automower',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'work_area_progress',
'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_123456_progress',
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_snapshot[sensor.test_mower_1_front_lawn_progress-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Mower 1 Front lawn progress',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.test_mower_1_front_lawn_progress',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '40',
})
# ---
# name: test_sensor_snapshot[sensor.test_mower_1_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -510,6 +607,103 @@
'state': 'main_area',
})
# ---
# name: test_sensor_snapshot[sensor.test_mower_1_my_lawn_last_time_completed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_mower_1_my_lawn_last_time_completed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'My lawn last time completed',
'platform': 'husqvarna_automower',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'my_lawn_last_time_completed',
'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_0_last_time_completed',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_snapshot[sensor.test_mower_1_my_lawn_last_time_completed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'Test Mower 1 My lawn last time completed',
}),
'context': <ANY>,
'entity_id': 'sensor.test_mower_1_my_lawn_last_time_completed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-08-12T03:07:49+00:00',
})
# ---
# name: test_sensor_snapshot[sensor.test_mower_1_my_lawn_progress-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_mower_1_my_lawn_progress',
'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': 'My lawn progress',
'platform': 'husqvarna_automower',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'my_lawn_progress',
'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_0_progress',
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_snapshot[sensor.test_mower_1_my_lawn_progress-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Mower 1 My lawn progress',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.test_mower_1_my_lawn_progress',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '20',
})
# ---
# name: test_sensor_snapshot[sensor.test_mower_1_next_start-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View file

@ -221,7 +221,7 @@ async def test_coordinator_automatic_registry_cleanup(
assert (
len(er.async_entries_for_config_entry(entity_registry, entry.entry_id))
== current_entites - 33
== current_entites - 37
)
assert (
len(dr.async_entries_for_config_entry(device_registry, entry.entry_id))