|
6 | 6 | from typing import TYPE_CHECKING, Any, ClassVar |
7 | 7 |
|
8 | 8 | from homeassistant.components.sensor import ( |
| 9 | + RestoreSensor, |
9 | 10 | SensorDeviceClass, |
10 | 11 | SensorEntity, |
11 | 12 | SensorEntityDescription, |
|
15 | 16 | PERCENTAGE, |
16 | 17 | REVOLUTIONS_PER_MINUTE, |
17 | 18 | EntityCategory, |
| 19 | + UnitOfEnergy, |
18 | 20 | UnitOfPower, |
19 | 21 | UnitOfTemperature, |
20 | 22 | UnitOfTime, |
21 | 23 | ) |
| 24 | +from homeassistant.util import dt as dt_util |
22 | 25 |
|
23 | 26 | from .const import MODEL_SPECS |
24 | 27 | from .entity import SystemairEntity |
|
30 | 33 | ) |
31 | 34 |
|
32 | 35 | if TYPE_CHECKING: |
| 36 | + from datetime import datetime, timedelta |
| 37 | + |
33 | 38 | from homeassistant.core import HomeAssistant |
34 | 39 | from homeassistant.helpers.entity_platform import AddEntitiesCallback |
35 | 40 |
|
36 | 41 | from .coordinator import SystemairDataUpdateCoordinator |
37 | 42 | from .data import SystemairConfigEntry |
38 | 43 |
|
| 44 | + |
39 | 45 | YEAR_2000_THRESHOLD = 100 |
40 | 46 |
|
41 | 47 | ALARM_ID_TO_NAME_MAP = { |
@@ -110,6 +116,40 @@ class SystemairPowerSensorEntityDescription(SensorEntityDescription): |
110 | 116 | """Describes a Systemair power sensor entity.""" |
111 | 117 |
|
112 | 118 |
|
| 119 | +@dataclass(kw_only=True, frozen=True) |
| 120 | +class SystemairEnergySensorEntityDescription(SensorEntityDescription): |
| 121 | + """Describes a Systemair energy sensor entity.""" |
| 122 | + |
| 123 | + power_sensor_key: str |
| 124 | + |
| 125 | + |
| 126 | +ENERGY_SENSORS: tuple[SystemairEnergySensorEntityDescription, ...] = ( |
| 127 | + SystemairEnergySensorEntityDescription( |
| 128 | + key="supply_fan_energy", |
| 129 | + translation_key="supply_fan_energy", |
| 130 | + device_class=SensorDeviceClass.ENERGY, |
| 131 | + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, |
| 132 | + state_class=SensorStateClass.TOTAL_INCREASING, |
| 133 | + power_sensor_key="supply_fan_power", |
| 134 | + ), |
| 135 | + SystemairEnergySensorEntityDescription( |
| 136 | + key="extract_fan_energy", |
| 137 | + translation_key="extract_fan_energy", |
| 138 | + device_class=SensorDeviceClass.ENERGY, |
| 139 | + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, |
| 140 | + state_class=SensorStateClass.TOTAL_INCREASING, |
| 141 | + power_sensor_key="extract_fan_power", |
| 142 | + ), |
| 143 | + SystemairEnergySensorEntityDescription( |
| 144 | + key="total_energy", |
| 145 | + translation_key="total_energy", |
| 146 | + device_class=SensorDeviceClass.ENERGY, |
| 147 | + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, |
| 148 | + state_class=SensorStateClass.TOTAL_INCREASING, |
| 149 | + power_sensor_key="total_power", |
| 150 | + ), |
| 151 | +) |
| 152 | + |
113 | 153 | POWER_SENSORS: tuple[SystemairPowerSensorEntityDescription, ...] = ( |
114 | 154 | SystemairPowerSensorEntityDescription( |
115 | 155 | key="supply_fan_power", |
@@ -268,14 +308,25 @@ async def async_setup_entry( |
268 | 308 | """Set up the sensor platform.""" |
269 | 309 | coordinator = entry.runtime_data.coordinator |
270 | 310 |
|
| 311 | + power_sensors_map = {desc.key: SystemairPowerSensor(coordinator=coordinator, entity_description=desc) for desc in POWER_SENSORS} |
| 312 | + |
271 | 313 | sensors = [SystemairSensor(coordinator=coordinator, entity_description=desc) for desc in ENTITY_DESCRIPTIONS] |
272 | | - power_sensors = [SystemairPowerSensor(coordinator=coordinator, entity_description=desc) for desc in POWER_SENSORS] |
| 314 | + power_sensors = list(power_sensors_map.values()) |
273 | 315 |
|
274 | | - async_add_entities(sensors + power_sensors) |
| 316 | + energy_sensors = [ |
| 317 | + SystemairEnergySensor( |
| 318 | + coordinator=coordinator, |
| 319 | + entity_description=desc, |
| 320 | + power_sensor=power_sensors_map[desc.power_sensor_key], |
| 321 | + ) |
| 322 | + for desc in ENERGY_SENSORS |
| 323 | + ] |
| 324 | + |
| 325 | + async_add_entities(sensors + power_sensors + energy_sensors) |
275 | 326 |
|
276 | 327 |
|
277 | 328 | class SystemairSensor(SystemairEntity, SensorEntity): |
278 | | - """Systemair Sensor class.""" |
| 329 | + """Systemair Sensor class for all sensors.""" |
279 | 330 |
|
280 | 331 | _attr_has_entity_name = True |
281 | 332 | entity_description: SystemairSensorEntityDescription |
@@ -426,3 +477,62 @@ def native_value(self) -> float | None: |
426 | 477 | } |
427 | 478 |
|
428 | 479 | return power_map.get(self.entity_description.key) |
| 480 | + |
| 481 | + |
| 482 | +class SystemairEnergySensor(SystemairEntity, RestoreSensor): |
| 483 | + """Systemair Energy Sensor class.""" |
| 484 | + |
| 485 | + _attr_has_entity_name = True |
| 486 | + entity_description: SystemairEnergySensorEntityDescription |
| 487 | + |
| 488 | + def __init__( |
| 489 | + self, |
| 490 | + coordinator: SystemairDataUpdateCoordinator, |
| 491 | + entity_description: SystemairEnergySensorEntityDescription, |
| 492 | + power_sensor: SystemairPowerSensor, |
| 493 | + ) -> None: |
| 494 | + """Initialize the energy sensor.""" |
| 495 | + super().__init__(coordinator) |
| 496 | + self.entity_description = entity_description |
| 497 | + self._power_sensor = power_sensor |
| 498 | + self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{entity_description.key}" |
| 499 | + self._last_update: datetime | None = None |
| 500 | + |
| 501 | + async def async_added_to_hass(self) -> None: |
| 502 | + """Handle entity which provides long-term statistics.""" |
| 503 | + await super().async_added_to_hass() |
| 504 | + |
| 505 | + last_sensor_data = await self.async_get_last_sensor_data() |
| 506 | + if last_sensor_data: |
| 507 | + self._attr_native_value = last_sensor_data.native_value |
| 508 | + |
| 509 | + if ( |
| 510 | + (last_state := await self.async_get_last_state()) |
| 511 | + and "last_update" in last_state.attributes |
| 512 | + and last_state.attributes["last_update"] is not None |
| 513 | + ): |
| 514 | + self._last_update = dt_util.parse_datetime(last_state.attributes["last_update"]) |
| 515 | + |
| 516 | + @property |
| 517 | + def extra_state_attributes(self) -> dict[str, Any]: |
| 518 | + """Return the state attributes.""" |
| 519 | + return {"last_update": self._last_update.isoformat() if self._last_update else None} |
| 520 | + |
| 521 | + def _handle_coordinator_update(self) -> None: |
| 522 | + """Handle updated data from the coordinator.""" |
| 523 | + now = dt_util.utcnow() |
| 524 | + power_w = self._power_sensor.native_value |
| 525 | + |
| 526 | + if power_w is None or self._last_update is None: |
| 527 | + self._last_update = now |
| 528 | + self.async_write_ha_state() |
| 529 | + return |
| 530 | + |
| 531 | + time_delta: timedelta = now - self._last_update |
| 532 | + energy_increment_kwh = (power_w / 1000) * (time_delta.total_seconds() / 3600) |
| 533 | + |
| 534 | + current_value = self.native_value or 0 |
| 535 | + self._attr_native_value = round(current_value + energy_increment_kwh, 4) |
| 536 | + |
| 537 | + self._last_update = now |
| 538 | + self.async_write_ha_state() |
0 commit comments