The lag proxy was reset to the new relay command at every relay change and after any skipped interval, so it read 1.0 straight after ON and 0.0 straight after OFF. This disabled the lag model around every boiler cycle: on a synthetic lagged plant cycled by a hysteresis thermostat the learner's heat gain came out 34% low (0.079 vs 0.12) and the loss term 43% low, and the optimizer started post-transition plans from the wrong lag. Track the lag at every poll with a valid relay state, independently of room sensors. Integrate a single change between polls at its last_changed time. For ambiguous cases (round trip between polls, relay outage, restart, clock jump) restart from time-in-state with an explicit error half-width, and learn only once it is at most 0.1 (about 32 min after an HA restart). The same synthetic test now learns 0.1195/0.00389 (true 0.12/0.004). Expose heat_lag, heat_lag_uncertainty and sample_result on Status so users can see why a poll was or was not learned. The HA test fixture relay now has an hour-old timestamp; a new test covers a freshly written relay state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
43 lines
1.7 KiB
Python
43 lines
1.7 KiB
Python
"""Real Home Assistant fixtures, isolated from the offline boundary doubles."""
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from homeassistant.core import ServiceRegistry
|
|
from homeassistant.util import dt as dt_util
|
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def custom_integrations(enable_custom_integrations):
|
|
"""Let HA discover custom_components/heating_mpc in this checkout."""
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def no_actuator_calls():
|
|
"""Reject every HA service call during all tested observer lifecycle paths."""
|
|
with patch.object(ServiceRegistry, "async_call", side_effect=AssertionError(
|
|
"The shadow integration must never call HA services"
|
|
)) as calls:
|
|
yield calls
|
|
calls.assert_not_called()
|
|
|
|
|
|
@pytest.fixture
|
|
def entry():
|
|
return MockConfigEntry(
|
|
domain="heating_mpc", version=2, unique_id="home_heating",
|
|
data={"climate_entity": "climate.reference", "heater_entity": "switch.boiler",
|
|
"room_entities": ["sensor.room"],
|
|
"outside_temperature_sources": ["sensor.outside"],
|
|
"weights": {"sensor.room": 1.0}, "horizon_minutes": 60},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def inputs(hass):
|
|
hass.states.async_set("sensor.room", "20", {"unit_of_measurement": "°C"})
|
|
hass.states.async_set("sensor.outside", "5", {"unit_of_measurement": "°C"})
|
|
# The relay has held its state for an hour, so the radiator-lag estimate is settled.
|
|
hass.states.async_set("switch.boiler", "off", timestamp=dt_util.utcnow().timestamp() - 3600)
|
|
hass.states.async_set("climate.reference", "heat", {"temperature": 22, "preset_mode": "comfort"})
|