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
@@ -21,6 +21,55 @@ async def setup_entry(hass, entry):
|
||||
return hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
|
||||
async def open_settings(hass, entry):
|
||||
"""Configure opens a menu; Settings is the first choice."""
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["menu_options"] == ["settings", "reset_model"]
|
||||
return await hass.config_entries.options.async_configure(result["flow_id"], {"next_step_id": "settings"})
|
||||
|
||||
|
||||
async def open_reset(hass, entry):
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
return await hass.config_entries.options.async_configure(result["flow_id"], {"next_step_id": "reset_model"})
|
||||
|
||||
|
||||
async def test_reset_learned_model_keeps_configuration_and_observations(hass, entry, inputs, hass_storage):
|
||||
coordinator = await setup_entry(hass, entry)
|
||||
model = coordinator.learners["sensor.room"]
|
||||
model.count, model.on_count, model.off_count = 250, 125, 125
|
||||
payload = coordinator._payload("sensor.outside")
|
||||
payload["retired_learners"] = {"sensor.gone": {**model.as_dict()}}
|
||||
payload["ventilation"] = {"events": [{"zone": "sensor.room", "opened": "2026-10-27T10:00:00+00:00"}]}
|
||||
payload["heat_sources"] = {"samples": {"ups": [[1.0, 200.0]]}}
|
||||
await coordinator._store.async_save(payload)
|
||||
assert await hass.config_entries.async_reload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.data[DOMAIN][entry.entry_id].data["status"] == "ready"
|
||||
before = (dict(entry.data), dict(entry.options), dict(entry.subentries))
|
||||
# Not confirmed: nothing happens.
|
||||
result = await open_reset(hass, entry)
|
||||
assert (result["type"], result["step_id"]) == (FlowResultType.FORM, "reset_model")
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"], {"confirm_reset": False})
|
||||
assert (result["type"], result["reason"]) == (FlowResultType.ABORT, "model_not_reset")
|
||||
assert hass.data[DOMAIN][entry.entry_id].learners["sensor.room"].count == 250
|
||||
# Confirmed: a fresh model, the same configuration.
|
||||
result = await open_reset(hass, entry)
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"], {"confirm_reset": True})
|
||||
assert (result["type"], result["reason"]) == (FlowResultType.ABORT, "model_reset")
|
||||
await hass.async_block_till_done()
|
||||
fresh = hass.data[DOMAIN][entry.entry_id]
|
||||
assert fresh is not coordinator and fresh.learners["sensor.room"].count == 0
|
||||
assert fresh.data["status"] == "learning" and not fresh._retired
|
||||
assert (dict(entry.data), dict(entry.options), dict(entry.subentries)) == before
|
||||
stored = hass_storage[f"{DOMAIN}.{entry.entry_id}"]["data"]
|
||||
assert not {"learners", "retired_learners", "evaluation", "operational_evaluation", "fingerprints"} & set(stored)
|
||||
assert stored["ventilation"]["events"][0]["opened"] == "2026-10-27T10:00:00+00:00"
|
||||
assert stored["heat_sources"] == {"samples": {"ups": [[1.0, 200.0]]}}
|
||||
assert stored["model_changes"][-1]["reasons"] == ["learned model reset by the user"]
|
||||
assert fresh.data["model_changes"][-1]["recalibrated"] == ["all"]
|
||||
|
||||
|
||||
async def test_config_flow_creates_one_zone_per_sensor(hass, inputs):
|
||||
hass.states.async_set("sensor.bedroom", "19", {"unit_of_measurement": "°C", "friendly_name": "Bedroom"})
|
||||
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_USER})
|
||||
@@ -51,7 +100,7 @@ async def test_config_flow_creates_one_zone_per_sensor(hass, inputs):
|
||||
|
||||
async def test_options_flow_keeps_settings_without_rooms(hass, entry, inputs):
|
||||
await setup_entry(hass, entry)
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
result = await open_settings(hass, entry)
|
||||
assert "room_entities" not in {str(key) for key in result["data_schema"].schema}
|
||||
main = result["data_schema"]({**MAIN, "room_max_age_minutes": 45, "room_stale_use_last_value": True})
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"], main)
|
||||
@@ -65,14 +114,14 @@ async def test_options_flow_keeps_settings_without_rooms(hass, entry, inputs):
|
||||
assert coordinator.config["room_max_age_minutes"] == 45
|
||||
assert coordinator.config["room_stale_use_last_value"] is True
|
||||
assert coordinator.rooms == ["sensor.room"] # zones are untouched by Configure
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
result = await open_settings(hass, entry)
|
||||
assert result["data_schema"](MAIN)["room_stale_use_last_value"] is True
|
||||
hass.config_entries.options.async_abort(result["flow_id"])
|
||||
|
||||
|
||||
async def test_weather_source_may_not_be_a_zone_sensor(hass, entry, inputs):
|
||||
await setup_entry(hass, entry)
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
result = await open_settings(hass, entry)
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"], result["data_schema"](MAIN))
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], result["data_schema"]({"outside_temperature_sources": ["sensor.room"]}))
|
||||
@@ -172,7 +221,7 @@ async def test_options_flow_clears_optional_weather_sources(hass, inputs):
|
||||
"weights": {"sensor.room": 1.0}})
|
||||
await setup_entry(hass, entry)
|
||||
flow = hass.config_entries.options
|
||||
result = await flow.async_init(entry.entry_id)
|
||||
result = await open_settings(hass, entry)
|
||||
result = await flow.async_configure(result["flow_id"], result["data_schema"](MAIN))
|
||||
markers = {str(key): key for key in result["data_schema"].schema}
|
||||
assert markers["outside_wind_sources"].description == {"suggested_value": ["sensor.wind"]}
|
||||
@@ -245,7 +294,7 @@ async def test_window_subentry_flow_and_ventilation_options(hass, entry, inputs)
|
||||
("sensor.gust", "9", "m/s")):
|
||||
hass.states.async_set(sensor, value, {"unit_of_measurement": unit})
|
||||
options = hass.config_entries.options
|
||||
result = await options.async_init(entry.entry_id)
|
||||
result = await open_settings(hass, entry)
|
||||
result = await options.async_configure(result["flow_id"], result["data_schema"](
|
||||
{**MAIN, "ventilation_gust": True, "ventilation_cross": True}))
|
||||
result = await options.async_configure(result["flow_id"], result["data_schema"]({
|
||||
|
||||
Reference in new issue
Block a user