Show learning activity with Learning Progress and Last Sample sensors

During a first test the Status entity only said "learning" for the
first half hour (the radiator-lag wait after a restart), and the
attributes explaining why were hard to find. Add two sensors:

- Learning Progress (%): for the least trained room, the mean of its
  three readiness requirements (200 samples, 30 ON, 30 OFF), each capped
  at 100 %; 100 % means ready. Attributes show the counts.
- Last Sample: an enum of the last poll's learning outcome, with English
  and Romanian state names (e.g. "Waiting for radiators to settle"). Its
  history shows when learning happens.

Polls blocked by missing shared inputs now report waiting_for_inputs.
Readiness thresholds move to const.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
buzeraandClaude Opus 5.5 committed 2026-10-26 22:39:25 +02:00
1 parent 3c949333bd
commit d2bd398a0f
9 files changed
+179 -11

No files matched your search

+17 -6
View File
@@ -17,7 +17,7 @@ from .const import (
CONF_ROOM_MAX_AGE, CONF_ROOM_STALE_KEEP, CONF_WEATHER_MAX_AGE, ROOM_STALE_KEEP_MAX_MINUTES,
DOMAIN, CONF_CLIMATE, CONF_HEATER, CONF_ROOMS, CONF_WEIGHTS, WEATHER_SOURCES,
CONF_HORIZON, CONF_MIN_ON, CONF_MIN_OFF, CONF_SWITCH_COST, CONF_HEAT_COST,
CONF_BAND, DEFAULTS,
CONF_BAND, DEFAULTS, READY_OFF_SAMPLES, READY_ON_SAMPLES, READY_SAMPLES,
)
from .data_quality import ReportAgeHistory, freshness
from .evaluation import ForecastEvaluator
@@ -159,6 +159,17 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._relay_events_dropped = False
return steady
def _count_result(self, status: dict[str, Any], result: str) -> None:
"""Record how this poll was handled for learning, and the learning progress."""
status["sample_result"] = result
self._sample_results[result] = self._sample_results.get(result, 0) + 1
status["sample_results_since_start"] = dict(self._sample_results)
# Least trained room: mean of its three readiness requirements (100 = ready).
status["learning_progress"] = round(100 * min(
(min(1.0, m.count / READY_SAMPLES) + min(1.0, m.on_count / READY_ON_SAMPLES)
+ min(1.0, m.off_count / READY_OFF_SAMPLES)) / 3
for m in self.learners.values()), 1)
def _prediction_inputs(self) -> tuple:
"""Values the prediction depends on, used to detect changes during calculation."""
heater = self.config[CONF_HEATER]
@@ -260,6 +271,7 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._previous = None
self._evaluator.reset_window()
status["forecast_accuracy"] = self._evaluator.summary(self.rooms)
self._count_result(status, "waiting_for_inputs")
return status
# A room learns only across a poll pair where it was usable at both ends.
@@ -290,9 +302,7 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._update_counts()
learned = True
result = status["sample_result"]
self._sample_results[result] = self._sample_results.get(result, 0) + 1
status["sample_results_since_start"] = dict(self._sample_results)
self._count_result(status, status["sample_result"])
self._previous = {
"time": now, "temps": vals, "outside": outside, "heater_on": heater_on,
"outside_source": active_sources["temperature"],
@@ -359,8 +369,9 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# Do not act on a cold model: every room used needs ~17 hours of samples
# plus both relay conditions.
used_models = [self.learners[room] for room in vals]
if (min(m.count for m in used_models) < 200 or min(m.on_count for m in used_models) < 30
or min(m.off_count for m in used_models) < 30):
if (min(m.count for m in used_models) < READY_SAMPLES
or min(m.on_count for m in used_models) < READY_ON_SAMPLES
or min(m.off_count for m in used_models) < READY_OFF_SAMPLES):
status["status"] = "learning"
return status
try: