Separate model accuracy from forecast accuracy
The Forecast Error sensors replayed the model with the relay, electrical heat and window history that actually followed, so they measured the model, not a forecast anyone could have made. They are renamed Model Error (unique IDs unchanged) and gain RMSE, effective sample count and a quality label; their state stays unknown until there are at least 5 roughly independent spans. A new Forecast Error sensor scores forecasts as published, with the plan and assumptions of the moment they were made. A forecast is scored only when the relay followed its plan; otherwise it is counted as counterfactual, not as error. Statistics are saved with the model and dropped with a zone's recalibration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d73e32a4b9
commit
26c415fa6c
6 files changed
+485
-36
No files matched your search
@@ -39,7 +39,7 @@ async def test_setup_reload_unload(hass, entry, inputs):
|
||||
assert coordinator.config_entry is entry
|
||||
registry = er.async_get(hass)
|
||||
entities = er.async_entries_for_config_entry(registry, entry.entry_id)
|
||||
assert len(entities) == 11 # 7 core + 4 forecast-error sensors
|
||||
assert len(entities) == 12 # 7 core + 4 model-error sensors + 1 forecast-error sensor
|
||||
ids = {e.unique_id: e.entity_id for e in entities}
|
||||
assert coordinator.data["status"] == "learning"
|
||||
status = hass.states.get(ids[f"{entry.entry_id}_status"])
|
||||
@@ -74,7 +74,7 @@ async def test_replacing_a_zone_removes_only_its_forecast(hass, entry, inputs):
|
||||
assert registry.async_get(old) is None
|
||||
new = registry.async_get_entity_id("sensor", DOMAIN, f"{entry.entry_id}_zone_new_predicted_60min")
|
||||
assert hass.states.get(new).attributes["friendly_name"] == "MPC New 60min Prediction"
|
||||
assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 11
|
||||
assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 12
|
||||
|
||||
|
||||
async def test_storage_restore_and_remove(hass, entry, inputs):
|
||||
@@ -255,13 +255,36 @@ async def test_forecast_error_sensors_score_scheduled_polls(hass, entry, inputs,
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(ids[15])
|
||||
assert coordinator.data["forecast_accuracy"][15]["samples"] == 1
|
||||
assert float(state.state) == coordinator.data["forecast_accuracy"][15]["mae"] > 0
|
||||
accuracy = coordinator.data["forecast_accuracy"][15]
|
||||
assert accuracy["samples"] == 1
|
||||
# One replayed forecast is no accuracy figure: the state stays unknown, the numbers
|
||||
# are in the attributes with their quality.
|
||||
assert state.state == "unknown"
|
||||
assert state.attributes["mae"] == accuracy["mae"] > 0
|
||||
assert state.attributes["quality"] == "insufficient"
|
||||
assert state.attributes["samples"] == 1
|
||||
assert state.attributes["zones"]["room"]["samples"] == 1 # by zone name
|
||||
assert state.attributes["unit_of_measurement"] == "°C"
|
||||
assert state.attributes["friendly_name"] == "Heating MPC Model Error 15 min"
|
||||
assert "zones" in state.state_info["unrecorded_attributes"]
|
||||
assert hass.states.get(ids[30]).state == "unknown"
|
||||
for step in range(15): # enough 15-minute spans for an indicative figure
|
||||
future += timedelta(minutes=5, seconds=1)
|
||||
freezer.move_to(future)
|
||||
# Fresh readings, or the sensors turn stale after 30 minutes.
|
||||
hass.states.async_set("sensor.room", str(20 - 0.01 * step), {"unit_of_measurement": "°C"})
|
||||
hass.states.async_set("sensor.outside", "5", {"unit_of_measurement": "°C"}, force_update=True)
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
accuracy = coordinator.data["forecast_accuracy"][15]
|
||||
assert accuracy["quality"] == "indicative", accuracy
|
||||
assert float(hass.states.get(ids[15]).state) == accuracy["mae"]
|
||||
# Published forecasts are scored separately; nothing was published while learning.
|
||||
operational = registry.async_get_entity_id("sensor", DOMAIN, f"{entry.entry_id}_operational_forecast_error")
|
||||
state = hass.states.get(operational)
|
||||
assert state.state == "unknown"
|
||||
assert state.attributes["friendly_name"] == "Heating MPC Forecast Error"
|
||||
assert "horizons" in state.state_info["unrecorded_attributes"]
|
||||
|
||||
|
||||
async def test_learning_progress_and_last_sample_sensors(hass, entry, inputs, freezer):
|
||||
|
||||
Reference in new issue
Block a user