From 3ff3cb975b6122a1c58d01eda0efd75f453e2858 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Tue, 20 May 2025 15:47:45 -0400 Subject: [PATCH] Add date sensors to Rehlko (#145314) * feat: add datetime sensors * fix: constants * fix: constants * fix: move tz conversion to api * fix: update typing --- homeassistant/components/rehlko/__init__.py | 3 +- homeassistant/components/rehlko/const.py | 1 + homeassistant/components/rehlko/entity.py | 10 +- homeassistant/components/rehlko/sensor.py | 64 ++++- homeassistant/components/rehlko/strings.json | 15 ++ .../components/rehlko/fixtures/generator.json | 6 +- .../rehlko/snapshots/test_sensor.ambr | 240 ++++++++++++++++++ 7 files changed, 324 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/rehlko/__init__.py b/homeassistant/components/rehlko/__init__.py index bda2704a206..3f255f23085 100644 --- a/homeassistant/components/rehlko/__init__.py +++ b/homeassistant/components/rehlko/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import dt as dt_util from .const import ( CONF_REFRESH_TOKEN, @@ -28,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bool: """Set up Rehlko from a config entry.""" websession = async_get_clientsession(hass) - rehlko = AioKem(session=websession) + rehlko = AioKem(session=websession, home_timezone=dt_util.get_default_time_zone()) # If requests take more than 20 seconds; timeout and let the setup retry. rehlko.set_timeout(20) diff --git a/homeassistant/components/rehlko/const.py b/homeassistant/components/rehlko/const.py index f63c0872d46..6dced0ccda6 100644 --- a/homeassistant/components/rehlko/const.py +++ b/homeassistant/components/rehlko/const.py @@ -18,6 +18,7 @@ DEVICE_DATA_IS_CONNECTED = "isConnected" KOHLER = "Kohler" GENERATOR_DATA_DEVICE = "device" +GENERATOR_DATA_EXERCISE = "exercise" CONNECTION_EXCEPTIONS = ( TimeoutError, diff --git a/homeassistant/components/rehlko/entity.py b/homeassistant/components/rehlko/entity.py index 94d384e1949..274562e6a41 100644 --- a/homeassistant/components/rehlko/entity.py +++ b/homeassistant/components/rehlko/entity.py @@ -43,7 +43,7 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]): device_id: int, device_data: dict, description: EntityDescription, - use_device_key: bool = False, + document_key: str | None = None, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -61,7 +61,7 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]): manufacturer=KOHLER, connections=_get_device_connections(device_data[DEVICE_DATA_MAC_ADDRESS]), ) - self._use_device_key = use_device_key + self._document_key = document_key @property def _device_data(self) -> dict[str, Any]: @@ -71,8 +71,10 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]): @property def _rehlko_value(self) -> str: """Return the sensor value.""" - if self._use_device_key: - return self._device_data[self.entity_description.key] + if self._document_key: + return self.coordinator.data[self._document_key][ + self.entity_description.key + ] return self.coordinator.data[self.entity_description.key] @property diff --git a/homeassistant/components/rehlko/sensor.py b/homeassistant/components/rehlko/sensor.py index 9186f0e0c9f..6ff45b1a464 100644 --- a/homeassistant/components/rehlko/sensor.py +++ b/homeassistant/components/rehlko/sensor.py @@ -2,7 +2,9 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime from homeassistant.components.sensor import ( SensorDeviceClass, @@ -25,7 +27,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import DEVICE_DATA_DEVICES, DEVICE_DATA_ID +from .const import ( + DEVICE_DATA_DEVICES, + DEVICE_DATA_ID, + GENERATOR_DATA_DEVICE, + GENERATOR_DATA_EXERCISE, +) from .coordinator import RehlkoConfigEntry from .entity import RehlkoEntity @@ -37,7 +44,8 @@ PARALLEL_UPDATES = 0 class RehlkoSensorEntityDescription(SensorEntityDescription): """Class describing Rehlko sensor entities.""" - use_device_key: bool = False + document_key: str | None = None + value_fn: Callable[[str], datetime | None] | None = None SENSORS: tuple[RehlkoSensorEntityDescription, ...] = ( @@ -116,7 +124,7 @@ SENSORS: tuple[RehlkoSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - use_device_key=True, + document_key=GENERATOR_DATA_DEVICE, ), RehlkoSensorEntityDescription( key="runtimeSinceLastMaintenanceHours", @@ -132,7 +140,7 @@ SENSORS: tuple[RehlkoSensorEntityDescription, ...] = ( translation_key="device_ip_address", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - use_device_key=True, + document_key=GENERATOR_DATA_DEVICE, ), RehlkoSensorEntityDescription( key="serverIpAddress", @@ -171,7 +179,7 @@ SENSORS: tuple[RehlkoSensorEntityDescription, ...] = ( RehlkoSensorEntityDescription( key="status", translation_key="generator_status", - use_device_key=True, + document_key=GENERATOR_DATA_DEVICE, ), RehlkoSensorEntityDescription( key="engineState", @@ -181,6 +189,44 @@ SENSORS: tuple[RehlkoSensorEntityDescription, ...] = ( key="powerSource", translation_key="power_source", ), + RehlkoSensorEntityDescription( + key="lastRanTimestamp", + translation_key="last_run", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=datetime.fromisoformat, + ), + RehlkoSensorEntityDescription( + key="lastMaintenanceTimestamp", + translation_key="last_maintainance", + device_class=SensorDeviceClass.TIMESTAMP, + document_key=GENERATOR_DATA_DEVICE, + value_fn=datetime.fromisoformat, + entity_registry_enabled_default=False, + ), + RehlkoSensorEntityDescription( + key="nextMaintenanceTimestamp", + translation_key="next_maintainance", + device_class=SensorDeviceClass.TIMESTAMP, + document_key=GENERATOR_DATA_DEVICE, + value_fn=datetime.fromisoformat, + entity_registry_enabled_default=False, + ), + RehlkoSensorEntityDescription( + key="lastStartTimestamp", + translation_key="last_exercise", + device_class=SensorDeviceClass.TIMESTAMP, + document_key=GENERATOR_DATA_EXERCISE, + value_fn=datetime.fromisoformat, + entity_registry_enabled_default=False, + ), + RehlkoSensorEntityDescription( + key="nextStartTimestamp", + translation_key="next_exercise", + device_class=SensorDeviceClass.TIMESTAMP, + document_key=GENERATOR_DATA_EXERCISE, + value_fn=datetime.fromisoformat, + entity_registry_enabled_default=False, + ), ) @@ -199,7 +245,7 @@ async def async_setup_entry( device_data[DEVICE_DATA_ID], device_data, sensor_description, - sensor_description.use_device_key, + sensor_description.document_key, ) for home_data in homes for device_data in home_data[DEVICE_DATA_DEVICES] @@ -210,7 +256,11 @@ async def async_setup_entry( class RehlkoSensorEntity(RehlkoEntity, SensorEntity): """Representation of a Rehlko sensor.""" + entity_description: RehlkoSensorEntityDescription + @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the sensor state.""" + if self.entity_description.value_fn: + return self.entity_description.value_fn(self._rehlko_value) return self._rehlko_value diff --git a/homeassistant/components/rehlko/strings.json b/homeassistant/components/rehlko/strings.json index 6b842173558..d98ae04d5c8 100644 --- a/homeassistant/components/rehlko/strings.json +++ b/homeassistant/components/rehlko/strings.json @@ -91,6 +91,21 @@ }, "generator_status": { "name": "Generator status" + }, + "last_run": { + "name": "Last run" + }, + "last_maintainance": { + "name": "Last maintainance" + }, + "next_maintainance": { + "name": "Next maintainance" + }, + "next_exercise": { + "name": "Next exercise" + }, + "last_exercise": { + "name": "Last exercise" } } }, diff --git a/tests/components/rehlko/fixtures/generator.json b/tests/components/rehlko/fixtures/generator.json index fa1d4d0b45b..5741b470bc6 100644 --- a/tests/components/rehlko/fixtures/generator.json +++ b/tests/components/rehlko/fixtures/generator.json @@ -54,8 +54,8 @@ "alertCount": 0, "model": "Model20KW", "modelDisplayName": "20 KW", - "lastMaintenanceTimestamp": "2025-04-10T09:12:59", - "nextMaintenanceTimestamp": "2026-04-10T09:12:59", + "lastMaintenanceTimestamp": "2025-04-10T09:12:59-04:00", + "nextMaintenanceTimestamp": "2026-04-10T09:12:59-04:00", "maintenancePeriodDays": 365, "hasServiceAgreement": null, "totalRuntimeHours": 120.2 @@ -74,7 +74,7 @@ }, "exercise": { "frequency": "Weekly", - "nextStartTimestamp": "2025-04-19T10:00:00", + "nextStartTimestamp": "2025-04-19T10:00:00-04:00", "mode": "Unloaded", "runningMode": null, "durationMinutes": 20, diff --git a/tests/components/rehlko/snapshots/test_sensor.ambr b/tests/components/rehlko/snapshots/test_sensor.ambr index 3973996ba80..3f0334ec7b8 100644 --- a/tests/components/rehlko/snapshots/test_sensor.ambr +++ b/tests/components/rehlko/snapshots/test_sensor.ambr @@ -609,6 +609,150 @@ 'state': 'ReadyToRun', }) # --- +# name: test_sensors[sensor.generator_1_last_exercise-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.generator_1_last_exercise', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last exercise', + 'platform': 'rehlko', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_exercise', + 'unique_id': 'myemail@email.com_12345_lastStartTimestamp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.generator_1_last_exercise-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Generator 1 Last exercise', + }), + 'context': , + 'entity_id': 'sensor.generator_1_last_exercise', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-04-12T14:00:00+00:00', + }) +# --- +# name: test_sensors[sensor.generator_1_last_maintainance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.generator_1_last_maintainance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last maintainance', + 'platform': 'rehlko', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_maintainance', + 'unique_id': 'myemail@email.com_12345_lastMaintenanceTimestamp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.generator_1_last_maintainance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Generator 1 Last maintainance', + }), + 'context': , + 'entity_id': 'sensor.generator_1_last_maintainance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-04-10T13:12:59+00:00', + }) +# --- +# name: test_sensors[sensor.generator_1_last_run-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.generator_1_last_run', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last run', + 'platform': 'rehlko', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_run', + 'unique_id': 'myemail@email.com_12345_lastRanTimestamp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.generator_1_last_run-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Generator 1 Last run', + }), + 'context': , + 'entity_id': 'sensor.generator_1_last_run', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-04-12T14:00:00+00:00', + }) +# --- # name: test_sensors[sensor.generator_1_lube_oil_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -661,6 +805,102 @@ 'state': '6.0', }) # --- +# name: test_sensors[sensor.generator_1_next_exercise-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.generator_1_next_exercise', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Next exercise', + 'platform': 'rehlko', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'next_exercise', + 'unique_id': 'myemail@email.com_12345_nextStartTimestamp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.generator_1_next_exercise-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Generator 1 Next exercise', + }), + 'context': , + 'entity_id': 'sensor.generator_1_next_exercise', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-04-19T14:00:00+00:00', + }) +# --- +# name: test_sensors[sensor.generator_1_next_maintainance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.generator_1_next_maintainance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Next maintainance', + 'platform': 'rehlko', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'next_maintainance', + 'unique_id': 'myemail@email.com_12345_nextMaintenanceTimestamp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.generator_1_next_maintainance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Generator 1 Next maintainance', + }), + 'context': , + 'entity_id': 'sensor.generator_1_next_maintainance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2026-04-10T13:12:59+00:00', + }) +# --- # name: test_sensors[sensor.generator_1_power_source-entry] EntityRegistryEntrySnapshot({ 'aliases': set({