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) return super().available and _check_error_free(self.mower_attributes)
class WorkAreaControlEntity(AutomowerControlEntity): class WorkAreaAvailableEntity(AutomowerAvailableEntity):
"""Base entity work work areas with control function.""" """Base entity for work work areas."""
def __init__( def __init__(
self, self,
@ -184,3 +184,7 @@ class WorkAreaControlEntity(AutomowerControlEntity):
def available(self) -> bool: def available(self) -> bool:
"""Return True if the work area is available and the mower has no errors.""" """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 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": { "error": {
"default": "mdi:alert-circle-outline" "default": "mdi:alert-circle-outline"
}, },
"my_lawn_last_time_completed": {
"default": "mdi:clock-outline"
},
"my_lawn_progress": {
"default": "mdi:collage"
},
"number_of_charging_cycles": { "number_of_charging_cycles": {
"default": "mdi:battery-sync-outline" "default": "mdi:battery-sync-outline"
}, },
@ -35,6 +41,12 @@
}, },
"restricted_reason": { "restricted_reason": {
"default": "mdi:tooltip-question" "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, MowerModes,
MowerStates, MowerStates,
RestrictedReasons, RestrictedReasons,
WorkArea,
) )
from aioautomower.utils import naive_to_aware from aioautomower.utils import naive_to_aware
@ -29,7 +30,11 @@ from homeassistant.util import dt as dt_util
from . import AutomowerConfigEntry from . import AutomowerConfigEntry
from .coordinator import AutomowerDataUpdateCoordinator from .coordinator import AutomowerDataUpdateCoordinator
from .entity import AutomowerBaseEntity from .entity import (
AutomowerBaseEntity,
WorkAreaAvailableEntity,
_work_area_translation_key,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -261,7 +266,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription):
value_fn: Callable[[MowerAttributes], StateType | datetime] value_fn: Callable[[MowerAttributes], StateType | datetime]
SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
AutomowerSensorEntityDescription( AutomowerSensorEntityDescription(
key="battery_percent", key="battery_percent",
state_class=SensorStateClass.MEASUREMENT, 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( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AutomowerConfigEntry, entry: AutomowerConfigEntry,
@ -403,12 +439,25 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up sensor platform.""" """Set up sensor platform."""
coordinator = entry.runtime_data coordinator = entry.runtime_data
async_add_entities( entities: list[SensorEntity] = []
AutomowerSensorEntity(mower_id, coordinator, description) for mower_id in coordinator.data:
for mower_id in coordinator.data if coordinator.data[mower_id].capabilities.work_areas:
for description in SENSOR_TYPES _work_areas = coordinator.data[mower_id].work_areas
if description.exists_fn(coordinator.data[mower_id]) 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): class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity):
@ -442,3 +491,36 @@ class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity):
def extra_state_attributes(self) -> Mapping[str, Any] | None: def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the state attributes.""" """Return the state attributes."""
return self.entity_description.extra_state_attributes_fn(self.mower_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" "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": { "number_of_charging_cycles": {
"name": "Number of charging cycles" "name": "Number of charging cycles"
}, },
@ -266,6 +272,12 @@
"name": "Work area ID assignment" "name": "Work area ID assignment"
} }
} }
},
"work_area_last_time_completed": {
"name": "{work_area} last time completed"
},
"work_area_progress": {
"name": "{work_area} progress"
} }
}, },
"switch": { "switch": {

View File

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

View File

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

View File

@ -448,6 +448,103 @@
'state': 'no_error', '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] # name: test_sensor_snapshot[sensor.test_mower_1_mode-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@ -510,6 +607,103 @@
'state': 'main_area', '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] # name: test_sensor_snapshot[sensor.test_mower_1_next_start-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

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