Add date sensors to Rehlko (#145314)

* feat: add datetime sensors

* fix: constants

* fix: constants

* fix: move tz conversion to api

* fix: update typing
This commit is contained in:
Pete Sage 2025-05-20 15:47:45 -04:00 committed by GitHub
parent 8ec5472b79
commit 3ff3cb975b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 324 additions and 15 deletions

View File

@ -10,6 +10,7 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import dt as dt_util
from .const import ( from .const import (
CONF_REFRESH_TOKEN, CONF_REFRESH_TOKEN,
@ -28,7 +29,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bool:
"""Set up Rehlko from a config entry.""" """Set up Rehlko from a config entry."""
websession = async_get_clientsession(hass) 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. # If requests take more than 20 seconds; timeout and let the setup retry.
rehlko.set_timeout(20) rehlko.set_timeout(20)

View File

@ -18,6 +18,7 @@ DEVICE_DATA_IS_CONNECTED = "isConnected"
KOHLER = "Kohler" KOHLER = "Kohler"
GENERATOR_DATA_DEVICE = "device" GENERATOR_DATA_DEVICE = "device"
GENERATOR_DATA_EXERCISE = "exercise"
CONNECTION_EXCEPTIONS = ( CONNECTION_EXCEPTIONS = (
TimeoutError, TimeoutError,

View File

@ -43,7 +43,7 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]):
device_id: int, device_id: int,
device_data: dict, device_data: dict,
description: EntityDescription, description: EntityDescription,
use_device_key: bool = False, document_key: str | None = None,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator)
@ -61,7 +61,7 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]):
manufacturer=KOHLER, manufacturer=KOHLER,
connections=_get_device_connections(device_data[DEVICE_DATA_MAC_ADDRESS]), connections=_get_device_connections(device_data[DEVICE_DATA_MAC_ADDRESS]),
) )
self._use_device_key = use_device_key self._document_key = document_key
@property @property
def _device_data(self) -> dict[str, Any]: def _device_data(self) -> dict[str, Any]:
@ -71,8 +71,10 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]):
@property @property
def _rehlko_value(self) -> str: def _rehlko_value(self) -> str:
"""Return the sensor value.""" """Return the sensor value."""
if self._use_device_key: if self._document_key:
return self._device_data[self.entity_description.key] return self.coordinator.data[self._document_key][
self.entity_description.key
]
return self.coordinator.data[self.entity_description.key] return self.coordinator.data[self.entity_description.key]
@property @property

View File

@ -2,7 +2,9 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -25,7 +27,12 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType 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 .coordinator import RehlkoConfigEntry
from .entity import RehlkoEntity from .entity import RehlkoEntity
@ -37,7 +44,8 @@ PARALLEL_UPDATES = 0
class RehlkoSensorEntityDescription(SensorEntityDescription): class RehlkoSensorEntityDescription(SensorEntityDescription):
"""Class describing Rehlko sensor entities.""" """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, ...] = ( SENSORS: tuple[RehlkoSensorEntityDescription, ...] = (
@ -116,7 +124,7 @@ SENSORS: tuple[RehlkoSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
use_device_key=True, document_key=GENERATOR_DATA_DEVICE,
), ),
RehlkoSensorEntityDescription( RehlkoSensorEntityDescription(
key="runtimeSinceLastMaintenanceHours", key="runtimeSinceLastMaintenanceHours",
@ -132,7 +140,7 @@ SENSORS: tuple[RehlkoSensorEntityDescription, ...] = (
translation_key="device_ip_address", translation_key="device_ip_address",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
use_device_key=True, document_key=GENERATOR_DATA_DEVICE,
), ),
RehlkoSensorEntityDescription( RehlkoSensorEntityDescription(
key="serverIpAddress", key="serverIpAddress",
@ -171,7 +179,7 @@ SENSORS: tuple[RehlkoSensorEntityDescription, ...] = (
RehlkoSensorEntityDescription( RehlkoSensorEntityDescription(
key="status", key="status",
translation_key="generator_status", translation_key="generator_status",
use_device_key=True, document_key=GENERATOR_DATA_DEVICE,
), ),
RehlkoSensorEntityDescription( RehlkoSensorEntityDescription(
key="engineState", key="engineState",
@ -181,6 +189,44 @@ SENSORS: tuple[RehlkoSensorEntityDescription, ...] = (
key="powerSource", key="powerSource",
translation_key="power_source", 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[DEVICE_DATA_ID],
device_data, device_data,
sensor_description, sensor_description,
sensor_description.use_device_key, sensor_description.document_key,
) )
for home_data in homes for home_data in homes
for device_data in home_data[DEVICE_DATA_DEVICES] for device_data in home_data[DEVICE_DATA_DEVICES]
@ -210,7 +256,11 @@ async def async_setup_entry(
class RehlkoSensorEntity(RehlkoEntity, SensorEntity): class RehlkoSensorEntity(RehlkoEntity, SensorEntity):
"""Representation of a Rehlko sensor.""" """Representation of a Rehlko sensor."""
entity_description: RehlkoSensorEntityDescription
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType | datetime:
"""Return the sensor state.""" """Return the sensor state."""
if self.entity_description.value_fn:
return self.entity_description.value_fn(self._rehlko_value)
return self._rehlko_value return self._rehlko_value

View File

@ -91,6 +91,21 @@
}, },
"generator_status": { "generator_status": {
"name": "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"
} }
} }
}, },

View File

@ -54,8 +54,8 @@
"alertCount": 0, "alertCount": 0,
"model": "Model20KW", "model": "Model20KW",
"modelDisplayName": "20 KW", "modelDisplayName": "20 KW",
"lastMaintenanceTimestamp": "2025-04-10T09:12:59", "lastMaintenanceTimestamp": "2025-04-10T09:12:59-04:00",
"nextMaintenanceTimestamp": "2026-04-10T09:12:59", "nextMaintenanceTimestamp": "2026-04-10T09:12:59-04:00",
"maintenancePeriodDays": 365, "maintenancePeriodDays": 365,
"hasServiceAgreement": null, "hasServiceAgreement": null,
"totalRuntimeHours": 120.2 "totalRuntimeHours": 120.2
@ -74,7 +74,7 @@
}, },
"exercise": { "exercise": {
"frequency": "Weekly", "frequency": "Weekly",
"nextStartTimestamp": "2025-04-19T10:00:00", "nextStartTimestamp": "2025-04-19T10:00:00-04:00",
"mode": "Unloaded", "mode": "Unloaded",
"runningMode": null, "runningMode": null,
"durationMinutes": 20, "durationMinutes": 20,

View File

@ -609,6 +609,150 @@
'state': 'ReadyToRun', 'state': 'ReadyToRun',
}) })
# --- # ---
# name: test_sensors[sensor.generator_1_last_exercise-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'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.generator_1_last_exercise',
'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': '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': <ANY>,
'entity_id': 'sensor.generator_1_last_exercise',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'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': <ANY>,
'entity_id': 'sensor.generator_1_last_maintainance',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'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': <ANY>,
'entity_id': 'sensor.generator_1_last_run',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2025-04-12T14:00:00+00:00',
})
# ---
# name: test_sensors[sensor.generator_1_lube_oil_temperature-entry] # name: test_sensors[sensor.generator_1_lube_oil_temperature-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@ -661,6 +805,102 @@
'state': '6.0', 'state': '6.0',
}) })
# --- # ---
# name: test_sensors[sensor.generator_1_next_exercise-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'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.generator_1_next_exercise',
'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': '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': <ANY>,
'entity_id': 'sensor.generator_1_next_exercise',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'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': <ANY>,
'entity_id': 'sensor.generator_1_next_maintainance',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2026-04-10T13:12:59+00:00',
})
# ---
# name: test_sensors[sensor.generator_1_power_source-entry] # name: test_sensors[sensor.generator_1_power_source-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({