Test power spikes, calm wind and restart of the new model layers
A single 5-minute +700 W spike moves the room's thermal response by at most 0.2 kW and leaves the baseline at the persistent load. In calm air the wind terms vanish while natural ventilation remains. Extended and ventilation coefficients and heat-source baselines survive a restart unchanged (no hour of baseline re-learning), and enabling an extra ventilation term later keeps everything learned for the others. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1577a0c78
commit
e62798dc4f
3 files changed
+45
No files matched your search
@@ -1033,3 +1033,36 @@ class BoundaryTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
for zone, other in (('living', 'bed'), ('bed', 'living')):
|
for zone, other in (('living', 'bed'), ('bed', 'living')):
|
||||||
with self.subTest(zone=zone):
|
with self.subTest(zone=zone):
|
||||||
self.assertLess(c.extended[zone].coefficient(f'couple:{other}'), 0.002)
|
self.assertLess(c.extended[zone].coefficient(f'couple:{other}'), 0.002)
|
||||||
|
|
||||||
|
async def test_new_model_layers_survive_a_restart(self):
|
||||||
|
self.c = self.bedroom_with_window()
|
||||||
|
self.c.entry.subentries['h'] = types.SimpleNamespace(subentry_id='h', subentry_type='heat_source', data={
|
||||||
|
'key': 'ups', 'name': 'UPS', 'sensor': 'sensor.ups_power', 'zone': 'bed'})
|
||||||
|
self.set_state('sensor.ups_power', '200', unit_of_measurement='W')
|
||||||
|
self.c = coordinator.HeatingMPCCoordinator(self.hass, self.entry)
|
||||||
|
for k in range(20):
|
||||||
|
self.fresh('sensor.room', 'sensor.outside', 'sensor.wind', 'sensor.wind_dir', 'sensor.ups_power')
|
||||||
|
if k == 18:
|
||||||
|
self.open_window('on', 240)
|
||||||
|
await self.sample()
|
||||||
|
extended, vent = self.c.extended['bed'], self.c.ventilation['bed']
|
||||||
|
self.assertGreater(extended.count, 0)
|
||||||
|
self.assertGreater(vent.count, 0)
|
||||||
|
payload = self.c._store.async_save.call_args.args[0]
|
||||||
|
restored = coordinator.HeatingMPCCoordinator(self.hass, self.entry)
|
||||||
|
restored._store.async_load.return_value = payload
|
||||||
|
await restored.async_restore_model()
|
||||||
|
self.assertEqual((restored.extended['bed'].theta, restored.extended['bed'].count), (extended.theta, extended.count))
|
||||||
|
self.assertEqual((restored.ventilation['bed'].theta, restored.ventilation['bed'].count), (vent.theta, vent.count))
|
||||||
|
self.assertEqual(restored._heat_sources._baselines['ups'].value(), 200) # no hour of re-learning
|
||||||
|
# Adding a feature keeps what was learned for the others.
|
||||||
|
self.entry.data['ventilation_cross'] = True
|
||||||
|
self.entry.subentries['z2'] = types.SimpleNamespace(subentry_id='z2', subentry_type='zone', data={
|
||||||
|
'key': 'living', 'name': 'Living', 'sensors': ['sensor.living']})
|
||||||
|
self.entry.subentries['w2'] = types.SimpleNamespace(subentry_id='w2', subentry_type='window', data={
|
||||||
|
'key': 'living_window', 'name': 'Living window', 'sensor': 'binary_sensor.living_window', 'zone': 'living'})
|
||||||
|
grown = coordinator.HeatingMPCCoordinator(self.hass, self.entry)
|
||||||
|
grown._store.async_load.return_value = payload
|
||||||
|
await grown.async_restore_model()
|
||||||
|
self.assertEqual(grown.ventilation['bed'].features[-1], 'vent_cross')
|
||||||
|
self.assertEqual(grown.ventilation['bed'].theta[:-1], vent.theta)
|
||||||
@@ -93,6 +93,14 @@ class TrackerTests(unittest.TestCase):
|
|||||||
self.assertEqual(tracker.zone_input('office'), states[-1]['ups']['response_kw'])
|
self.assertEqual(tracker.zone_input('office'), states[-1]['ups']['response_kw'])
|
||||||
self.assertEqual(tracker.zone_input('living'), 0.0)
|
self.assertEqual(tracker.zone_input('living'), 0.0)
|
||||||
|
|
||||||
|
def test_short_spike_barely_moves_response_or_baseline(self):
|
||||||
|
tracker = hs.HeatSourceTracker(sources(UPS), {'baselines': {'ups': [200.0] * 24}})
|
||||||
|
states = run(tracker, [{'ups': 200.0}] * 3 + [{'ups': 900.0}] + [{'ups': 200.0}] * 4)
|
||||||
|
peak = max(s['ups']['response_kw'] for s in states)
|
||||||
|
self.assertAlmostEqual(peak, 0.7 * (1 - exp(-1 / 3))) # one 5-minute interval of +700 W
|
||||||
|
self.assertLess(peak, 0.2)
|
||||||
|
self.assertEqual(states[-1]['ups']['baseline_w'], 200)
|
||||||
|
|
||||||
def test_unknown_power_resets_until_settled_again(self):
|
def test_unknown_power_resets_until_settled_again(self):
|
||||||
tracker = hs.HeatSourceTracker(sources(UPS), {'baselines': {'ups': [200.0] * 24}})
|
tracker = hs.HeatSourceTracker(sources(UPS), {'baselines': {'ups': [200.0] * 24}})
|
||||||
run(tracker, [{'ups': 200.0}] * 7)
|
run(tracker, [{'ups': 200.0}] * 7)
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ class GeometryTests(unittest.TestCase):
|
|||||||
[-9.0, -36.0, 21.6, -45.0, -9.0, 0.0]):
|
[-9.0, -36.0, 21.6, -45.0, -9.0, 0.0]):
|
||||||
self.assertAlmostEqual(value, expected)
|
self.assertAlmostEqual(value, expected)
|
||||||
self.assertIsNone(wm.ventilation_vector(['vent_wind'], 20, 2, {**inputs, 'wind': None}))
|
self.assertIsNone(wm.ventilation_vector(['vent_wind'], 20, 2, {**inputs, 'wind': None}))
|
||||||
|
# Calm air: wind terms vanish, natural ventilation stays.
|
||||||
|
calm = wm.ventilation_vector(features, 20.0, 2.0, {**inputs, 'wind': 0.0, 'gust': 0.0})
|
||||||
|
self.assertEqual([round(v, 9) for v in calm[1:4]], [0.0, 0.0, 0.0])
|
||||||
|
self.assertEqual(calm[0], -9.0)
|
||||||
self.assertEqual(wm.ventilation_vector(['vent_natural'], 20, 2, {**inputs, 'wind': None}), [-9.0])
|
self.assertEqual(wm.ventilation_vector(['vent_natural'], 20, 2, {**inputs, 'wind': None}), [-9.0])
|
||||||
self.assertIsNone(wm.ventilation_vector(['recovery'], 20, 2, {**inputs, 'recovery': 0.5}))
|
self.assertIsNone(wm.ventilation_vector(['recovery'], 20, 2, {**inputs, 'recovery': 0.5}))
|
||||||
self.assertEqual(wm.ventilation_vector(['recovery'], 19, 2, {**inputs, 'recovery': 0.5, 't_ref': 21}),
|
self.assertEqual(wm.ventilation_vector(['recovery'], 19, 2, {**inputs, 'recovery': 0.5, 't_ref': 21}),
|
||||||
|
|||||||
Reference in new issue
Block a user