New apps Added
This commit is contained in:
@@ -0,0 +1,639 @@
|
||||
"""Sensor platform for Intelligent Heating Pilot."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ATTR_ANTICIPATED_START_TIME,
|
||||
ATTR_LEARNED_HEATING_SLOPE,
|
||||
ATTR_NEXT_SCHEDULE_TIME,
|
||||
ATTR_NEXT_TARGET_TEMP,
|
||||
CONF_NAME,
|
||||
DOMAIN,
|
||||
EVENT_DEAD_TIME_UPDATED,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Intelligent Heating Pilot sensors."""
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
name = config_entry.data.get(CONF_NAME, "Intelligent Heating Pilot")
|
||||
|
||||
sensors = [
|
||||
IntelligentHeatingPilotAnticipationTimeSensor(coordinator, config_entry, name),
|
||||
IntelligentHeatingPilotNextScheduleSensor(coordinator, config_entry, name),
|
||||
# HMS-only display companions
|
||||
IntelligentHeatingPilotAnticipationTimeHmsSensor(coordinator, config_entry, name),
|
||||
IntelligentHeatingPilotNextScheduleHmsSensor(coordinator, config_entry, name),
|
||||
# Metrics - LHS sensors (Global + Contextual)
|
||||
IntelligentHeatingPilotGlobalLearnedSlopeSensor(coordinator, config_entry, name),
|
||||
IntelligentHeatingPilotContextualLearnedSlopeSensor(coordinator, config_entry, name),
|
||||
IntelligentHeatingPilotPredictionConfidenceSensor(
|
||||
coordinator, config_entry, name
|
||||
), # Phase 4: New
|
||||
# Dead time learning sensor
|
||||
IntelligentHeatingPilotDeadTimeSensor(coordinator, config_entry, name),
|
||||
]
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
|
||||
|
||||
class IntelligentHeatingPilotSensorBase(SensorEntity):
|
||||
"""Base class for Intelligent Heating Pilot sensors."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: Any, config_entry: ConfigEntry, name: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.coordinator = coordinator
|
||||
self._config_entry = config_entry
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, config_entry.entry_id)},
|
||||
"name": name,
|
||||
"manufacturer": "Intelligent Heating Pilot",
|
||||
"model": "Intelligent Preheating with ML",
|
||||
}
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
def handle_anticipation_event(event):
|
||||
"""Handle anticipation calculated event."""
|
||||
data = event.data or {}
|
||||
# Filter events to only those coming from our own config entry
|
||||
if data.get("entry_id") != self._config_entry.entry_id:
|
||||
return
|
||||
old_value = self.native_value
|
||||
self._handle_anticipation_result(data)
|
||||
# Only write HA state when the sensor value actually changed to
|
||||
# avoid unnecessary state-machine writes and log noise.
|
||||
if self.native_value != old_value:
|
||||
self.async_write_ha_state()
|
||||
|
||||
self.async_on_remove(
|
||||
self.hass.bus.async_listen(
|
||||
f"{DOMAIN}_anticipation_calculated", handle_anticipation_event
|
||||
)
|
||||
)
|
||||
|
||||
def _handle_anticipation_result(self, data: dict) -> None:
|
||||
"""Handle new anticipation result. Override in subclasses."""
|
||||
pass
|
||||
|
||||
|
||||
class IntelligentHeatingPilotAnticipationTimeSensor(IntelligentHeatingPilotSensorBase):
|
||||
"""Sensor for anticipated start time."""
|
||||
|
||||
_attr_name = "Anticipated Start Time"
|
||||
_attr_icon = "mdi:clock-start"
|
||||
_attr_device_class = SensorDeviceClass.TIMESTAMP
|
||||
|
||||
def __init__(self, coordinator: Any, config_entry: ConfigEntry, name: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, config_entry, name)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_anticipated_start_time"
|
||||
self._anticipated_start: datetime | None = None
|
||||
self._attributes: dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
def native_value(self) -> datetime | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self._anticipated_start
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict:
|
||||
"""Return additional attributes."""
|
||||
return self._attributes
|
||||
|
||||
def _handle_anticipation_result(self, data: dict) -> None:
|
||||
"""Handle new anticipation result."""
|
||||
# Clear sensor values when scheduler is disabled or no timeslot is available
|
||||
# This sets the sensor to 'unknown' state and clears all attributes
|
||||
anticipated_start = data.get(ATTR_ANTICIPATED_START_TIME)
|
||||
if anticipated_start is None:
|
||||
self._anticipated_start = None
|
||||
self._attributes = {}
|
||||
_LOGGER.info("Anticipated start time cleared (scheduler disabled or no timeslot)")
|
||||
return
|
||||
|
||||
if anticipated_start:
|
||||
# Event carries ISO string; accept datetime too
|
||||
if isinstance(anticipated_start, str):
|
||||
# Parse with HA helper to preserve timezone correctly
|
||||
parsed = dt_util.parse_datetime(anticipated_start)
|
||||
if parsed is None:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(anticipated_start)
|
||||
except ValueError:
|
||||
parsed = None
|
||||
self._anticipated_start = parsed
|
||||
else:
|
||||
self._anticipated_start = anticipated_start
|
||||
# Store attributes - keep next_schedule_time as ISO string for proper serialization
|
||||
next_sched = data.get(ATTR_NEXT_SCHEDULE_TIME)
|
||||
if isinstance(next_sched, str):
|
||||
next_sched_attr = next_sched # Already ISO string
|
||||
elif isinstance(next_sched, datetime):
|
||||
next_sched_attr = next_sched.isoformat()
|
||||
else:
|
||||
next_sched_attr = None
|
||||
|
||||
self._attributes = {
|
||||
ATTR_NEXT_SCHEDULE_TIME: next_sched_attr,
|
||||
ATTR_NEXT_TARGET_TEMP: data.get(ATTR_NEXT_TARGET_TEMP),
|
||||
"anticipation_minutes": data.get("anticipation_minutes"),
|
||||
"current_temp": data.get("current_temp"),
|
||||
"scheduler_entity": data.get("scheduler_entity"),
|
||||
ATTR_LEARNED_HEATING_SLOPE: data.get(ATTR_LEARNED_HEATING_SLOPE),
|
||||
"confidence_level": data.get("confidence_level"), # Phase 4: New from domain
|
||||
}
|
||||
_LOGGER.info(
|
||||
"Anticipated start time updated: %s (confidence: %.2f)",
|
||||
self._anticipated_start,
|
||||
data.get("confidence_level", 0.0),
|
||||
)
|
||||
|
||||
|
||||
class IntelligentHeatingPilotAnticipationTimeHmsSensor(IntelligentHeatingPilotSensorBase):
|
||||
"""Companion sensor showing only HH:MM:SS for anticipated start time."""
|
||||
|
||||
_attr_name = "Anticipated Start Time (HMS)"
|
||||
_attr_icon = "mdi:clock-outline"
|
||||
|
||||
def __init__(self, coordinator: Any, config_entry: ConfigEntry, name: str) -> None:
|
||||
super().__init__(coordinator, config_entry, name)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_anticipated_start_time_hms"
|
||||
self._time_str: str | None = None
|
||||
self._attributes: dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
return self._time_str
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict:
|
||||
return self._attributes
|
||||
|
||||
def _handle_anticipation_result(self, data: dict) -> None:
|
||||
# Check if anticipated start time is available
|
||||
value = data.get(ATTR_ANTICIPATED_START_TIME)
|
||||
if value is None:
|
||||
self._time_str = None
|
||||
self._attributes = {}
|
||||
return
|
||||
|
||||
dt_val: datetime | None = None
|
||||
if isinstance(value, str):
|
||||
dt_val = dt_util.parse_datetime(value) or None
|
||||
if dt_val is None:
|
||||
try:
|
||||
dt_val = datetime.fromisoformat(value)
|
||||
except ValueError:
|
||||
dt_val = None
|
||||
elif isinstance(value, datetime):
|
||||
dt_val = value
|
||||
if dt_val is not None:
|
||||
# Ensure local timezone and format HH:MM:SS
|
||||
local_dt = dt_util.as_local(dt_val)
|
||||
self._time_str = local_dt.strftime("%H:%M:%S")
|
||||
# Provide raw timestamp as attribute for completeness
|
||||
self._attributes = {
|
||||
"timestamp": local_dt.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
class IntelligentHeatingPilotGlobalLearnedSlopeSensor(IntelligentHeatingPilotSensorBase):
|
||||
"""Sensor for global learned heating slope (LHS)."""
|
||||
|
||||
_attr_name = "Global Learned Heating Slope"
|
||||
_attr_native_unit_of_measurement = "°C/h"
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_icon = "mdi:chart-line"
|
||||
|
||||
def __init__(self, coordinator: Any, config_entry: ConfigEntry, name: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, config_entry, name)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_global_learned_heating_slope"
|
||||
# Tracks the last value written to HA state for threshold comparison.
|
||||
self._last_lhs_displayed: float | None = None
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the global LHS from coordinator cache."""
|
||||
value = self.coordinator.get_learned_heating_slope()
|
||||
# Round to 2 decimal places for cleaner display
|
||||
return round(value, 2) if value is not None else None
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict:
|
||||
"""Return additional attributes."""
|
||||
return {
|
||||
"description": "Average heating slope across all extracted cycles",
|
||||
}
|
||||
|
||||
def _handle_anticipation_result(self, data: dict) -> None:
|
||||
"""Refresh state only when global LHS changed by >= 0.1 °C/h.
|
||||
|
||||
The coordinator is updated before this event fires, so we compare the
|
||||
current native_value against the last value written to HA state.
|
||||
Availability transitions (None ↔ value) always trigger a state write.
|
||||
"""
|
||||
current = self.native_value
|
||||
prev = self._last_lhs_displayed
|
||||
if (prev is None) != (current is None) or (
|
||||
prev is not None and current is not None and abs(current - prev) >= 0.1
|
||||
):
|
||||
self._last_lhs_displayed = current
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class IntelligentHeatingPilotContextualLearnedSlopeSensor(IntelligentHeatingPilotSensorBase):
|
||||
"""Sensor for contextual learned heating slope (for current hour)."""
|
||||
|
||||
_attr_name = "Contextual Learned Heating Slope"
|
||||
_attr_native_unit_of_measurement = "°C/h"
|
||||
|
||||
_attr_icon = "mdi:chart-line"
|
||||
|
||||
def __init__(self, coordinator: Any, config_entry: ConfigEntry, name: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, config_entry, name)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_contextual_learned_heating_slope"
|
||||
self._next_schedule_time: datetime | None = None
|
||||
# Tracks the last value written to HA state for threshold comparison.
|
||||
self._last_contextual_lhs_displayed: float | None = None
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the contextual LHS for next scheduled event hour."""
|
||||
# If no next schedule time, cannot provide contextual LHS
|
||||
if not self._next_schedule_time:
|
||||
return None
|
||||
|
||||
try:
|
||||
schedule_hour = self._next_schedule_time.hour
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
# Try to get contextual LHS from coordinator cache for scheduled hour
|
||||
contextual_lhs = self.coordinator.get_contextual_learned_heating_slope(schedule_hour)
|
||||
|
||||
if contextual_lhs is None or contextual_lhs == 2.0: # Default value means no data
|
||||
return None
|
||||
|
||||
# Round to 2 decimal places for cleaner display and return as float
|
||||
return float(round(contextual_lhs, 2))
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict:
|
||||
"""Return additional attributes."""
|
||||
if not self._next_schedule_time:
|
||||
return {
|
||||
"description": "Average heating slope for scheduled event hour (no event scheduled)",
|
||||
"scheduled_hour": "–",
|
||||
}
|
||||
|
||||
try:
|
||||
scheduled_hour = self._next_schedule_time.hour
|
||||
except AttributeError:
|
||||
return {
|
||||
"description": "Average heating slope for scheduled event hour (parse error)",
|
||||
"scheduled_hour": "–",
|
||||
}
|
||||
|
||||
return {
|
||||
"description": "Average heating slope for scheduled event hour",
|
||||
"scheduled_hour": f"{scheduled_hour:02d}:00",
|
||||
}
|
||||
|
||||
def _handle_anticipation_result(self, data: dict) -> None:
|
||||
"""Refresh state only when contextual LHS changed by >= 0.1 °C/h.
|
||||
|
||||
Captures the current schedule hour BEFORE updating _next_schedule_time so
|
||||
we can detect a context change (different scheduled hour) and always write
|
||||
state when the LHS context changes, regardless of the value delta.
|
||||
Availability transitions (None ↔ value) always trigger a state write.
|
||||
"""
|
||||
old_schedule_hour = self._next_schedule_time.hour if self._next_schedule_time else None
|
||||
|
||||
# Update next_schedule_time from event
|
||||
next_schedule = data.get(ATTR_NEXT_SCHEDULE_TIME)
|
||||
if next_schedule is None:
|
||||
self._next_schedule_time = None
|
||||
else:
|
||||
# Event carries ISO string; accept datetime too
|
||||
if isinstance(next_schedule, str):
|
||||
parsed = dt_util.parse_datetime(next_schedule)
|
||||
if parsed is None:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(next_schedule)
|
||||
except ValueError:
|
||||
parsed = None
|
||||
self._next_schedule_time = parsed
|
||||
else:
|
||||
self._next_schedule_time = next_schedule
|
||||
|
||||
new_schedule_hour = self._next_schedule_time.hour if self._next_schedule_time else None
|
||||
schedule_context_changed = old_schedule_hour != new_schedule_hour
|
||||
|
||||
new_value = self.native_value
|
||||
prev = self._last_contextual_lhs_displayed
|
||||
if (
|
||||
schedule_context_changed
|
||||
or (prev is None) != (new_value is None)
|
||||
or (prev is not None and new_value is not None and abs(new_value - prev) >= 0.1)
|
||||
):
|
||||
self._last_contextual_lhs_displayed = new_value
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class IntelligentHeatingPilotNextScheduleSensor(IntelligentHeatingPilotSensorBase):
|
||||
"""Sensor for next schedule time."""
|
||||
|
||||
_attr_name = "Next Schedule Time"
|
||||
_attr_icon = "mdi:calendar-clock"
|
||||
_attr_device_class = SensorDeviceClass.TIMESTAMP
|
||||
|
||||
def __init__(self, coordinator: Any, config_entry: ConfigEntry, name: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, config_entry, name)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_next_schedule_time"
|
||||
self._next_schedule: datetime | None = None
|
||||
self._attributes: dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
def native_value(self) -> datetime | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self._next_schedule
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict:
|
||||
"""Return additional attributes."""
|
||||
return self._attributes
|
||||
|
||||
def _handle_anticipation_result(self, data: dict) -> None:
|
||||
"""Handle new anticipation result."""
|
||||
# Check if scheduler is disabled or no timeslot is available
|
||||
next_schedule = data.get(ATTR_NEXT_SCHEDULE_TIME)
|
||||
if next_schedule is None:
|
||||
self._next_schedule = None
|
||||
self._attributes = {}
|
||||
_LOGGER.info("Next schedule time cleared (scheduler disabled or no timeslot)")
|
||||
return
|
||||
|
||||
if next_schedule:
|
||||
# Event carries ISO string; accept datetime too
|
||||
if isinstance(next_schedule, str):
|
||||
# Parse with HA helper to preserve timezone correctly
|
||||
parsed = dt_util.parse_datetime(next_schedule)
|
||||
if parsed is None:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(next_schedule)
|
||||
except ValueError:
|
||||
parsed = None
|
||||
self._next_schedule = parsed
|
||||
else:
|
||||
self._next_schedule = next_schedule
|
||||
self._attributes = {
|
||||
ATTR_NEXT_TARGET_TEMP: data.get(ATTR_NEXT_TARGET_TEMP),
|
||||
"scheduler_entity": data.get("scheduler_entity"),
|
||||
}
|
||||
_LOGGER.debug("Next schedule time updated: %s", self._next_schedule)
|
||||
|
||||
|
||||
class IntelligentHeatingPilotNextScheduleHmsSensor(IntelligentHeatingPilotSensorBase):
|
||||
"""Companion sensor showing only HH:MM:SS for next schedule time."""
|
||||
|
||||
_attr_name = "Next Schedule Time (HMS)"
|
||||
_attr_icon = "mdi:clock-time-three-outline"
|
||||
|
||||
def __init__(self, coordinator: Any, config_entry: ConfigEntry, name: str) -> None:
|
||||
super().__init__(coordinator, config_entry, name)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_next_schedule_time_hms"
|
||||
self._time_str: str | None = None
|
||||
self._attributes: dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
return self._time_str
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict:
|
||||
return self._attributes
|
||||
|
||||
def _handle_anticipation_result(self, data: dict) -> None:
|
||||
# Clear when no next schedule time is available
|
||||
value = data.get(ATTR_NEXT_SCHEDULE_TIME)
|
||||
if value is None:
|
||||
self._time_str = None
|
||||
self._attributes = {}
|
||||
return
|
||||
|
||||
dt_val: datetime | None = None
|
||||
if isinstance(value, str):
|
||||
dt_val = dt_util.parse_datetime(value) or None
|
||||
if dt_val is None:
|
||||
try:
|
||||
dt_val = datetime.fromisoformat(value)
|
||||
except ValueError:
|
||||
dt_val = None
|
||||
elif isinstance(value, datetime):
|
||||
dt_val = value
|
||||
if dt_val is not None:
|
||||
local_dt = dt_util.as_local(dt_val)
|
||||
self._time_str = local_dt.strftime("%H:%M:%S")
|
||||
self._attributes = {
|
||||
"timestamp": local_dt.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
class IntelligentHeatingPilotPredictionConfidenceSensor(IntelligentHeatingPilotSensorBase):
|
||||
"""Sensor for prediction confidence level.
|
||||
|
||||
Phase 4: New sensor to display domain prediction confidence (0.0-1.0).
|
||||
This reflects the quality of the prediction based on learned data and
|
||||
available environmental sensors.
|
||||
"""
|
||||
|
||||
_attr_name = "Prediction Confidence"
|
||||
_attr_icon = "mdi:percent"
|
||||
_attr_native_unit_of_measurement = "%"
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, coordinator: Any, config_entry: ConfigEntry, name: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, config_entry, name)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_prediction_confidence"
|
||||
self._confidence: float | None = None
|
||||
self._attributes: dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor as percentage (0-100)."""
|
||||
if self._confidence is not None:
|
||||
return round(self._confidence * 100, 1)
|
||||
return None
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict:
|
||||
"""Return additional attributes."""
|
||||
return self._attributes
|
||||
|
||||
def _handle_anticipation_result(self, data: dict) -> None:
|
||||
"""Handle new anticipation result."""
|
||||
# Clear when no confidence level is available
|
||||
confidence = data.get("confidence_level")
|
||||
if confidence is None:
|
||||
self._confidence = None
|
||||
self._attributes = {}
|
||||
_LOGGER.info("Prediction confidence cleared (scheduler disabled or no timeslot)")
|
||||
return
|
||||
|
||||
confidence_value = float(confidence)
|
||||
if confidence_value > 1.0:
|
||||
confidence_value = confidence_value / 100.0
|
||||
self._confidence = confidence_value
|
||||
self._attributes = {
|
||||
ATTR_LEARNED_HEATING_SLOPE: data.get(ATTR_LEARNED_HEATING_SLOPE),
|
||||
"anticipation_minutes": data.get("anticipation_minutes"),
|
||||
"environmental_data_available": {
|
||||
"humidity": data.get("humidity") is not None,
|
||||
"cloud_coverage": data.get("cloud_coverage") is not None,
|
||||
},
|
||||
}
|
||||
_LOGGER.info("Prediction confidence updated: %.1f%%", self._confidence * 100)
|
||||
|
||||
|
||||
class IntelligentHeatingPilotDeadTimeSensor(SensorEntity):
|
||||
"""Sensor for displaying learned dead time value.
|
||||
|
||||
Shows the dead time calculated from heating cycles.
|
||||
Dead time is the delay between heating start and first measurable temperature rise (in minutes).
|
||||
"""
|
||||
|
||||
_attr_name = "Dead Time"
|
||||
_attr_icon = "mdi:timer-sand"
|
||||
_attr_device_class = SensorDeviceClass.DURATION
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_native_unit_of_measurement = "min"
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: Any,
|
||||
config_entry: ConfigEntry,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._coordinator = coordinator
|
||||
self._config_entry = config_entry
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_dead_time"
|
||||
|
||||
# Device info
|
||||
self._attr_device_info = {
|
||||
"identifiers": {("intelligent_heating_pilot", config_entry.entry_id)},
|
||||
"name": f"Intelligent Heating Pilot {name}",
|
||||
"manufacturer": "Intelligent Heating Pilot",
|
||||
"model": "IHP",
|
||||
}
|
||||
|
||||
self._dead_time: float | None = None
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the current dead time value in minutes."""
|
||||
return self._dead_time
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if sensor is available."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return additional attributes."""
|
||||
return {
|
||||
"auto_learning": self._coordinator.is_auto_learning_enabled(),
|
||||
}
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to Home Assistant."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
await self._async_refresh_dead_time()
|
||||
|
||||
@callback
|
||||
def handle_dead_time_updated(event) -> None:
|
||||
"""Handle dead time update events."""
|
||||
data = event.data or {}
|
||||
if data.get("entry_id") != self._config_entry.entry_id:
|
||||
return
|
||||
if self.hass is None:
|
||||
return
|
||||
self.hass.async_create_task(self._async_refresh_dead_time())
|
||||
|
||||
if self.hass is not None:
|
||||
self.async_on_remove(
|
||||
self.hass.bus.async_listen(EVENT_DEAD_TIME_UPDATED, handle_dead_time_updated)
|
||||
)
|
||||
|
||||
async def _async_refresh_dead_time(self) -> None:
|
||||
"""Refresh effective dead time from coordinator."""
|
||||
dead_time = await self._coordinator.get_effective_dead_time()
|
||||
self._dead_time = dead_time if dead_time is not None else None
|
||||
if self.hass is not None:
|
||||
self.async_write_ha_state()
|
||||
Reference in New Issue
Block a user