Count rejected samples and poll outcomes; pin rollback contract

Status showed accepted sample counts but not why learning was slow.
Each room's model now counts samples it rejected (outliers, implausible
inputs), persisted as an optional rejected_count key and shown as
models.<room>.rejected_samples. Status also counts every poll outcome
since start (sample_results_since_start).

Add tests that a model in the exact shape 0.2.1 wrote restores with its
coefficients, and that today's saved model still has the keys and types
0.2.1's restore code reads, so rolling back keeps the learned model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
buzeraandClaude Opus 5.5 committed 2026-10-26 22:30:16 +02:00
1 parent ace6d0061b
commit 43915ba810
4 files changed
+69 -14

No files matched your search

@@ -45,6 +45,8 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self.learners: dict[str, Learner] = {room: Learner() for room in self.rooms}
self._retired: dict[str, dict[str, Any]] = {}
self._report_ages = ReportAgeHistory()
# How each poll was handled since start, to explain slow learning.
self._sample_results: dict[str, int] = {}
self._store = Store(hass, 1, f"{DOMAIN}.{entry.entry_id}")
self._previous: dict[str, Any] | None = None
# Radiator-lag proxy at the last poll with a valid relay state. Tracked
@@ -291,6 +293,9 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"outside_source": active_sources["temperature"],
})
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._previous = {
"time": now, "temps": vals, "outside": outside, "heater_on": heater_on,
"outside_source": active_sources["temperature"],
@@ -320,6 +325,7 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"bias": round(model.theta[2], 6),
"samples": model.count,
"on_samples": model.on_count, "off_samples": model.off_count,
"rejected_samples": model.rejected_count,
"mae_5m": round(model.mae, 4) if model.mae is not None else None}
for room, model in self.learners.items()
},