diff --git a/custom_components/heating_mpc/coordinator.py b/custom_components/heating_mpc/coordinator.py index 2f6b6d5..3447922 100644 --- a/custom_components/heating_mpc/coordinator.py +++ b/custom_components/heating_mpc/coordinator.py @@ -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), diff --git a/tests/ha/test_lifecycle.py b/tests/ha/test_lifecycle.py index 3262ce4..28a05a7 100644 --- a/tests/ha/test_lifecycle.py +++ b/tests/ha/test_lifecycle.py @@ -290,3 +290,18 @@ async def test_learning_progress_and_last_sample_sensors(hass, entry, inputs, fr last = hass.states.get(last_id) assert last.state == "waiting_for_inputs" assert last.attributes["counts_since_start"] == {"first_sample": 1, "learned": 1, "waiting_for_inputs": 1} + + +async def test_deleting_the_last_zone_keeps_the_entry_loaded(hass, entry, inputs, caplog): + await setup(hass, entry) + assert hass.config_entries.async_remove_subentry(entry, zone_subentry(entry, "sensor.room").subentry_id) + await hass.async_block_till_done() + assert entry.state.name == "LOADED" + coordinator = hass.data[DOMAIN][entry.entry_id] + assert (coordinator.rooms, coordinator.data["status"]) == ([], "no_zones") + assert coordinator.data["learning_progress"] == 0 + assert "Error setting up entry" not in caplog.text + # Adding a zone back resumes normally. + add_zone(hass, entry, "sensor.room", ["sensor.room"]) + await hass.async_block_till_done() + assert hass.data[DOMAIN][entry.entry_id].data["status"] == "learning"