Choosing room and weather freshness limits required knowing how long each sensor normally goes without reporting, which HA does not show. Add max_age_seconds_24h to every source in input_quality: the largest report age seen at polls over the last 24 hours (hourly buckets, reset on reload, underestimates by up to one poll interval). input_quality and weather_comparison change every poll, so the Recorder stored a new attribute row every five minutes. Mark them unrecorded; they remain visible on the Status entity. Model coefficients and other attributes are still recorded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
94 lines
3.6 KiB
Python
94 lines
3.6 KiB
Python
"""Diagnostics and predicted temperatures exposed as HA sensors."""
|
|
from __future__ import annotations
|
|
|
|
from homeassistant.components.sensor import SensorEntity, SensorDeviceClass
|
|
from homeassistant.const import UnitOfTemperature
|
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
|
|
from .const import DOMAIN
|
|
|
|
|
|
async def async_setup_entry(hass, entry, async_add_entities) -> None:
|
|
coordinator = hass.data[DOMAIN][entry.entry_id]
|
|
entities = [MPCStatus(coordinator, entry.entry_id), MPCWeightedTemp(coordinator, entry.entry_id), MPCOutdoorTemperature(coordinator, entry.entry_id)]
|
|
entities += [MPCRoomPrediction(coordinator, entry.entry_id, room) for room in coordinator.rooms]
|
|
async_add_entities(entities)
|
|
|
|
|
|
class MPCStatus(CoordinatorEntity, SensorEntity):
|
|
_attr_has_entity_name = True
|
|
_attr_name = "Heating MPC Status"
|
|
_attr_icon = "mdi:brain"
|
|
# Per-source report ages change every poll; recording them adds a new
|
|
# attribute row to the database every five minutes. Current values stay visible.
|
|
_unrecorded_attributes = frozenset({"input_quality", "weather_comparison"})
|
|
|
|
def __init__(self, coordinator, entry_id):
|
|
super().__init__(coordinator)
|
|
self._attr_unique_id = f"{entry_id}_status"
|
|
|
|
@property
|
|
def native_value(self):
|
|
return self.coordinator.data.get("status")
|
|
|
|
@property
|
|
def extra_state_attributes(self):
|
|
data = self.coordinator.data
|
|
return {key: value for key, value in data.items() if key not in ("status", "prediction_60m", "prediction_end")}
|
|
|
|
|
|
class MPCWeightedTemp(CoordinatorEntity, SensorEntity):
|
|
_attr_has_entity_name = True
|
|
_attr_name = "Heating MPC Weighted Temperature"
|
|
_attr_device_class = SensorDeviceClass.TEMPERATURE
|
|
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
|
|
|
def __init__(self, coordinator, entry_id):
|
|
super().__init__(coordinator)
|
|
self._attr_unique_id = f"{entry_id}_weighted_temp"
|
|
|
|
@property
|
|
def native_value(self):
|
|
return self.coordinator.data.get("weighted_temperature")
|
|
|
|
|
|
class MPCRoomPrediction(CoordinatorEntity, SensorEntity):
|
|
_attr_has_entity_name = True
|
|
_attr_device_class = SensorDeviceClass.TEMPERATURE
|
|
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
|
_attr_icon = "mdi:chart-line"
|
|
|
|
def __init__(self, coordinator, entry_id, room):
|
|
super().__init__(coordinator)
|
|
self.room = room
|
|
self._attr_unique_id = f"{entry_id}_{room}_predicted_60min"
|
|
self._attr_name = f"MPC {room} 60min Prediction"
|
|
|
|
@property
|
|
def native_value(self):
|
|
return self.coordinator.data.get("prediction_60m", {}).get(self.room)
|
|
|
|
class MPCOutdoorTemperature(CoordinatorEntity, SensorEntity):
|
|
"""Display chosen outdoor temperature and all currently valid alternatives."""
|
|
_attr_has_entity_name = True
|
|
_attr_name = "MPC Outdoor Temperature"
|
|
_attr_device_class = SensorDeviceClass.TEMPERATURE
|
|
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
|
_attr_icon = "mdi:thermometer"
|
|
|
|
def __init__(self, coordinator, entry_id):
|
|
super().__init__(coordinator)
|
|
self._attr_unique_id = f"{entry_id}_outside_temperature"
|
|
|
|
@property
|
|
def native_value(self):
|
|
return self.coordinator.data.get("weather", {}).get("temperature")
|
|
|
|
@property
|
|
def extra_state_attributes(self):
|
|
data = self.coordinator.data
|
|
return {
|
|
"active_source": data.get("weather_active_sources", {}).get("temperature"),
|
|
"alternatives": data.get("weather_comparison", {}).get("temperature", {}),
|
|
}
|