From 3cda93d00113219dc506138c612075ac1c869e61 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Sun, 6 Oct 2024 16:10:26 +0200 Subject: [PATCH] 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 --------- Co-authored-by: Joost Lekkerkerker --- .../components/husqvarna_automower/entity.py | 8 +- .../components/husqvarna_automower/icons.json | 12 ++ .../components/husqvarna_automower/sensor.py | 98 ++++++++- .../husqvarna_automower/strings.json | 12 ++ .../husqvarna_automower/fixtures/mower.json | 4 +- .../snapshots/test_diagnostics.ambr | 4 +- .../snapshots/test_sensor.ambr | 194 ++++++++++++++++++ .../husqvarna_automower/test_init.py | 2 +- 8 files changed, 318 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/entity.py b/homeassistant/components/husqvarna_automower/entity.py index fd9e7578fb2..ea3fff079eb 100644 --- a/homeassistant/components/husqvarna_automower/entity.py +++ b/homeassistant/components/husqvarna_automower/entity.py @@ -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.""" diff --git a/homeassistant/components/husqvarna_automower/icons.json b/homeassistant/components/husqvarna_automower/icons.json index 8511a63fbec..14ac5ce4068 100644 --- a/homeassistant/components/husqvarna_automower/icons.json +++ b/homeassistant/components/husqvarna_automower/icons.json @@ -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" } } }, diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index ed80366c648..b9a6fb16486 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -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 + ) diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index baeba4684ac..05a18bcb19f 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -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": { diff --git a/tests/components/husqvarna_automower/fixtures/mower.json b/tests/components/husqvarna_automower/fixtures/mower.json index a2bab4b2f43..8ab2f96e42f 100644 --- a/tests/components/husqvarna_automower/fixtures/mower.json +++ b/tests/components/husqvarna_automower/fixtures/mower.json @@ -105,9 +105,7 @@ "workAreaId": 654321, "name": "Back lawn", "cuttingHeight": 25, - "enabled": true, - "progress": 30, - "lastTimeCompleted": 1722449269 + "enabled": true }, { "workAreaId": 0, diff --git a/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr index f0036e653a8..ab9e81985c9 100644 --- a/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr @@ -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, }), }), }) diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index c090b835ae3..dfc1d41775f 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -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': , + 'device_class': None, + 'device_id': , + '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': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'entity_id': 'sensor.test_mower_1_front_lawn_last_time_completed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + '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': , + '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': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_mower_1_front_lawn_progress', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'device_class': None, + 'device_id': , + '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': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'entity_id': 'sensor.test_mower_1_my_lawn_last_time_completed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + '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': , + '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': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_mower_1_my_lawn_progress', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- # name: test_sensor_snapshot[sensor.test_mower_1_next_start-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/husqvarna_automower/test_init.py b/tests/components/husqvarna_automower/test_init.py index bdbb13ff37e..b7cc6f883f4 100644 --- a/tests/components/husqvarna_automower/test_init.py +++ b/tests/components/husqvarna_automower/test_init.py @@ -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))