Add Reset learned model to Configure
Configure now opens a menu: Settings (the previous form) or Reset learned model. After an explicit confirmation the entry is unloaded, the learned parts of the stored model (zone, extended and ventilation coefficients, models kept for removed zones, Model Error and Forecast Error statistics, fingerprints) are removed under the storage lock, and the entry is set up again. Configuration, heat-source baselines and the window event log are kept, and model_changes records the reset. Unloading first means no update still running can write the old model back. English and Romanian strings; the gust hint now refers to Model Error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
28ff953ddd
commit
49565d610a
8 files changed
+174
-12
No files matched your search
@@ -23,6 +23,7 @@ from .heat_sources import (
|
||||
SOURCE_KEY, SOURCE_NAME, SOURCE_RESPONSE, SOURCE_SENSOR, SOURCE_ZONE, SUBENTRY_HEAT_SOURCE,
|
||||
HeatSource, build_sources, validate_source,
|
||||
)
|
||||
from .coordinator import async_reset_learned_model
|
||||
from .weather_sources import sources_for
|
||||
from .windows import (
|
||||
DEFAULT_AZIMUTH, OPENING_RANGE, SUBENTRY_WINDOW, WINDOW_AZIMUTH, WINDOW_ENABLED, WINDOW_INVERTED,
|
||||
@@ -205,6 +206,9 @@ class HeatingMPCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
SUBENTRY_WINDOW: WindowSubentryFlow}
|
||||
|
||||
|
||||
CONFIRM_RESET = "confirm_reset"
|
||||
|
||||
|
||||
class HeatingMPCOptionsFlow(config_entries.OptionsFlow):
|
||||
"""Edit sensors, ranked alternative weather providers and MPC parameters."""
|
||||
|
||||
@@ -215,13 +219,26 @@ class HeatingMPCOptionsFlow(config_entries.OptionsFlow):
|
||||
return [sensor for zone in entry_zones(self.config_entry) for sensor in zone.sensors]
|
||||
|
||||
async def async_step_init(self, user_input: dict[str, Any] | None = None):
|
||||
return self.async_show_menu(step_id="init", menu_options=["settings", "reset_model"])
|
||||
|
||||
async def async_step_settings(self, user_input: dict[str, Any] | None = None):
|
||||
config = {**self.config_entry.data, **self.config_entry.options}
|
||||
if user_input is not None:
|
||||
self._draft = user_input
|
||||
return await self.async_step_weather()
|
||||
return self.async_show_form(step_id="init", data_schema=_main_schema(
|
||||
return self.async_show_form(step_id="settings", data_schema=_main_schema(
|
||||
config, detailed=True))
|
||||
|
||||
async def async_step_reset_model(self, user_input: dict[str, Any] | None = None):
|
||||
"""Forget the learned model after an explicit confirmation; settings stay."""
|
||||
if user_input is not None:
|
||||
if user_input.get(CONFIRM_RESET) is not True:
|
||||
return self.async_abort(reason="model_not_reset")
|
||||
await async_reset_learned_model(self.hass, self.config_entry)
|
||||
return self.async_abort(reason="model_reset")
|
||||
return self.async_show_form(step_id="reset_model", data_schema=vol.Schema(
|
||||
{vol.Required(CONFIRM_RESET, default=False): selector.BooleanSelector()}))
|
||||
|
||||
async def async_step_weather(self, user_input: dict[str, Any] | None = None):
|
||||
config = {**self.config_entry.data, **self.config_entry.options, **self._draft}
|
||||
if user_input is not None:
|
||||
|
||||
@@ -65,6 +65,34 @@ def model_removed(hass: HomeAssistant, entry_id: str) -> bool:
|
||||
return entry_id in hass.data.get(f"{DOMAIN}_removed", ())
|
||||
|
||||
|
||||
# Stored keys holding what was learned. The rest is observation, not model: the
|
||||
# heat sources' baseline samples, the window history and the heater's identity.
|
||||
LEARNED_KEYS = ("learners", "retired_learners", "zone_models", "ventilation_models", "evaluation",
|
||||
"operational_evaluation", "fingerprint_version", "fingerprints", "on_samples", "off_samples")
|
||||
|
||||
|
||||
async def async_reset_learned_model(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Forget everything learned for this entry; the configuration is not touched.
|
||||
|
||||
The entry is unloaded first, so no running update can save the old model
|
||||
afterwards, then set up again from the stripped data. Accuracy statistics go
|
||||
too: they describe the model being discarded.
|
||||
"""
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
store = Store(hass, 1, f"{DOMAIN}.{entry.entry_id}")
|
||||
async with storage_lock(hass, entry.entry_id):
|
||||
stored = await store.async_load()
|
||||
if isinstance(stored, dict):
|
||||
kept = {key: value for key, value in stored.items() if key not in LEARNED_KEYS}
|
||||
changes = kept.get("model_changes") if isinstance(kept.get("model_changes"), list) else []
|
||||
kept["model_changes"] = [*changes, {
|
||||
"time": datetime.now(timezone.utc).isoformat(timespec="seconds"), "zone": "all",
|
||||
"recalibrated": ["all"], "reasons": ["learned model reset by the user"]}][-MAX_MODEL_CHANGES:]
|
||||
await store.async_save(kept)
|
||||
_LOGGER.info("Heating MPC: learned model reset by the user")
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
|
||||
|
||||
def entry_zone_list(entry: ConfigEntry, config: dict[str, Any]) -> list[Zone]:
|
||||
"""Zones from the entry's zone subentries; one zone per legacy room sensor otherwise."""
|
||||
subentries = getattr(entry, "subentries", None) or {}
|
||||
|
||||
@@ -56,6 +56,13 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Heating MPC",
|
||||
"menu_options": {
|
||||
"settings": "Settings",
|
||||
"reset_model": "Reset learned model"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Heating MPC options",
|
||||
"description": "Change room sensors and controller settings. This integration remains shadow-only.",
|
||||
"data": {
|
||||
@@ -83,10 +90,17 @@
|
||||
"window_open_minutes": "Forecasts assume an open window closes after this many more minutes, then the room recovers.",
|
||||
"recovery_minutes": "Time constant of the air warming back towards the walls' temperature.",
|
||||
"ventilation_wind": "Needs wind sources; direction also needs wind direction sources.",
|
||||
"ventilation_gust": "Adds a term for gusts above the mean wind. Keep it only if Forecast Error improves.",
|
||||
"ventilation_gust": "Adds a term for gusts above the mean wind. Keep it only if Model Error improves.",
|
||||
"ventilation_cross": "Adds a term for windows open in several zones at once. Needs windows in two or more zones."
|
||||
}
|
||||
},
|
||||
"reset_model": {
|
||||
"title": "Reset learned model",
|
||||
"description": "Forgets everything the model has learned: every zone's thermal coefficients (also those kept for removed zones), their confidence and sample counts, and the Model Error and Forecast Error statistics.\n\nKept: zones, sensors and weights, heat sources, windows, weather sources and all settings, plus the heat sources' baselines and the window event log.\n\nThe integration restarts and learns from the beginning: the first sample after about 30 minutes, forecasts after a day or more. This cannot be undone. The boiler and the thermostat are not affected.",
|
||||
"data": {
|
||||
"confirm_reset": "Yes, forget the learned model"
|
||||
}
|
||||
},
|
||||
"weather": {
|
||||
"title": "Weather source priority",
|
||||
"description": "Choose as many sensors per metric as needed and arrange them in priority order. If the preferred source is unavailable, the next one is used automatically. Outdoor temperature is mandatory; wind and cloud coverage are diagnostic-only in this version.",
|
||||
@@ -113,6 +127,10 @@
|
||||
},
|
||||
"error": {
|
||||
"invalid_weather": "Select at least one outdoor temperature sensor, distinct from room sensors."
|
||||
},
|
||||
"abort": {
|
||||
"model_reset": "The learned model was reset. Learning starts again from the beginning.",
|
||||
"model_not_reset": "Nothing was changed: the reset was not confirmed."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -56,6 +56,13 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Heating MPC",
|
||||
"menu_options": {
|
||||
"settings": "Settings",
|
||||
"reset_model": "Reset learned model"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Heating MPC options",
|
||||
"description": "Change room sensors and controller settings. This integration remains shadow-only.",
|
||||
"data": {
|
||||
@@ -83,10 +90,17 @@
|
||||
"window_open_minutes": "Forecasts assume an open window closes after this many more minutes, then the room recovers.",
|
||||
"recovery_minutes": "Time constant of the air warming back towards the walls' temperature.",
|
||||
"ventilation_wind": "Needs wind sources; direction also needs wind direction sources.",
|
||||
"ventilation_gust": "Adds a term for gusts above the mean wind. Keep it only if Forecast Error improves.",
|
||||
"ventilation_gust": "Adds a term for gusts above the mean wind. Keep it only if Model Error improves.",
|
||||
"ventilation_cross": "Adds a term for windows open in several zones at once. Needs windows in two or more zones."
|
||||
}
|
||||
},
|
||||
"reset_model": {
|
||||
"title": "Reset learned model",
|
||||
"description": "Forgets everything the model has learned: every zone's thermal coefficients (also those kept for removed zones), their confidence and sample counts, and the Model Error and Forecast Error statistics.\n\nKept: zones, sensors and weights, heat sources, windows, weather sources and all settings, plus the heat sources' baselines and the window event log.\n\nThe integration restarts and learns from the beginning: the first sample after about 30 minutes, forecasts after a day or more. This cannot be undone. The boiler and the thermostat are not affected.",
|
||||
"data": {
|
||||
"confirm_reset": "Yes, forget the learned model"
|
||||
}
|
||||
},
|
||||
"weather": {
|
||||
"title": "Weather source priority",
|
||||
"description": "Choose as many sensors per metric as needed and arrange them in priority order. If the preferred source is unavailable, the next one is used automatically. Outdoor temperature is mandatory; wind and cloud coverage are diagnostic-only in this version.",
|
||||
@@ -113,6 +127,10 @@
|
||||
},
|
||||
"error": {
|
||||
"invalid_weather": "Select at least one outdoor temperature sensor, distinct from room sensors."
|
||||
},
|
||||
"abort": {
|
||||
"model_reset": "The learned model was reset. Learning starts again from the beginning.",
|
||||
"model_not_reset": "Nothing was changed: the reset was not confirmed."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -56,6 +56,13 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Heating MPC",
|
||||
"menu_options": {
|
||||
"settings": "Setări",
|
||||
"reset_model": "Resetează modelul învățat"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurare Heating MPC",
|
||||
"description": "Modifică senzorii, ponderile și optimizatorul. Rămâne activ doar modul de observație.",
|
||||
"data": {
|
||||
@@ -83,10 +90,17 @@
|
||||
"window_open_minutes": "Prognozele presupun că o fereastră deschisă se închide după atâtea minute, apoi camera revine.",
|
||||
"recovery_minutes": "Constanta de timp a aerului care se încălzește din nou spre temperatura pereților.",
|
||||
"ventilation_wind": "Necesită surse de vânt; direcția necesită și surse pentru direcția vântului.",
|
||||
"ventilation_gust": "Adaugă un termen pentru rafalele peste vântul mediu. Păstrează-l doar dacă eroarea de prognoză scade.",
|
||||
"ventilation_gust": "Adaugă un termen pentru rafalele peste vântul mediu. Păstrează-l doar dacă Model Error scade.",
|
||||
"ventilation_cross": "Adaugă un termen pentru ferestre deschise în mai multe zone deodată. Necesită ferestre în cel puțin două zone."
|
||||
}
|
||||
},
|
||||
"reset_model": {
|
||||
"title": "Resetează modelul învățat",
|
||||
"description": "Șterge tot ce a învățat modelul: coeficienții termici ai fiecărei zone (inclusiv cei păstrați pentru zonele eliminate), încrederea și numărul de eșantioane, precum și statisticile Model Error și Forecast Error.\n\nSe păstrează: zonele, senzorii și ponderile, sursele de căldură, ferestrele, sursele meteo și toate setările, plus nivelurile de bază ale surselor de căldură și jurnalul ferestrelor.\n\nIntegrarea repornește și învață de la început: primul eșantion după aproximativ 30 de minute, prognozele după o zi sau mai mult. Operația nu poate fi anulată. Centrala și termostatul nu sunt afectate.",
|
||||
"data": {
|
||||
"confirm_reset": "Da, șterge modelul învățat"
|
||||
}
|
||||
},
|
||||
"weather": {
|
||||
"title": "Prioritatea surselor meteo",
|
||||
"description": "Alege câți senzori dorești pentru fiecare variabilă și ordonează-i după prioritate. Când prima sursă este indisponibilă, integrarea încearcă automat următoarea. Temperatura exterioară este obligatorie; vântul și nebulozitatea sunt acum doar diagnostice.",
|
||||
@@ -113,6 +127,10 @@
|
||||
},
|
||||
"error": {
|
||||
"invalid_weather": "Alege cel puțin un senzor de temperatură exterioară, diferit de senzorii camerelor."
|
||||
},
|
||||
"abort": {
|
||||
"model_reset": "Modelul învățat a fost resetat. Învățarea reîncepe de la zero.",
|
||||
"model_not_reset": "Nu s-a modificat nimic: resetarea nu a fost confirmată."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
Reference in new issue
Block a user