Keep the entry loaded when the last zone is deleted

Deleting the only zone in the UI reloaded the entry with no zones, and
min() over an empty set of models raised during setup, so the entry
failed to load. Counts and learning progress now default to 0, and the
status reads "no_zones" until a zone is added again; adding one resumes
normally. HA Core regression test included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
buzeraandClaude Opus 5.5 committed 2026-10-27 03:02:03 +02:00
1 parent e62798dc4f
commit dc92e2bac5
2 files changed
+25 -7

No files matched your search

+10 -7
View File
@@ -166,8 +166,9 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._update_counts()
def _update_counts(self) -> None:
self._on_samples = min(m.on_count for m in self.learners.values())
self._off_samples = min(m.off_count for m in self.learners.values())
# Without zones (all deleted in the UI) there is nothing to count.
self._on_samples = min((m.on_count for m in self.learners.values()), default=0)
self._off_samples = min((m.off_count for m in self.learners.values()), default=0)
def _sensor_quality(self, entity_id: str, now: datetime, limit: float) -> dict:
state = self.hass.states.get(entity_id)
@@ -241,9 +242,9 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
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)
((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()), default=0.0), 1)
def _extended_diagnostics(self, room: str) -> dict[str, Any]:
model = self.extended.get(room)
@@ -432,7 +433,7 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"temperatures": {r: round(t, 3) for r, t in temps.items() if t is not None},
"zone_names": {key: zone.name for key, zone in self.zones.items()},
"on_samples": self._on_samples, "off_samples": self._off_samples,
"samples": min(learner.count for learner in self.learners.values()),
"samples": min((learner.count for learner in self.learners.values()), default=0),
"mode": climate.state if climate else "unavailable",
"weather": {k: v for k, v in weather.items() if v is not None},
"weather_active_sources": {k: v for k, v in active_sources.items() if v},
@@ -486,6 +487,8 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._evaluator.reset_window()
status["forecast_accuracy"] = self._evaluator.summary(self.rooms)
self._count_result(status, "waiting_for_inputs")
if not self.zones:
status["status"] = "no_zones"
return status
# A zone learns only across a poll pair where it was usable at both ends with
@@ -587,7 +590,7 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"reference_heat": heater_on,
"on_samples": self._on_samples,
"off_samples": self._off_samples,
"samples": min(learner.count for learner in self.learners.values()),
"samples": min((learner.count for learner in self.learners.values()), default=0),
"models": {
room: {"name": self.zones[room].name,
"loss": round(model.theta[0], 6),