"""Configuration fingerprints: what invalidates which learned coefficient.""" import sys import types import unittest from pathlib import Path COMPONENTS = Path(__file__).resolve().parents[1] / 'custom_components' / 'heating_mpc' sys.modules.setdefault('heating_mpc', types.ModuleType('heating_mpc')) sys.modules['heating_mpc'].__path__ = [str(COMPONENTS)] from heating_mpc import compatibility as cp, heat_sources as hs, windows as wm, zones as zn # noqa: E402 def setup(zones=None, sources=None, windows=None, recovery=20): zones = zones or [{'key': 'living', 'name': 'Living', 'sensors': ['sensor.l1', 'sensor.l2'], 'sensor_weights': {'sensor.l1': 1, 'sensor.l2': 1}, 'neighbors': ['bed']}, {'key': 'bed', 'name': 'Bedroom', 'sensors': ['sensor.b']}] built = {z.key: z for z in zn.build_zones((z, None) for z in zones)} sources = hs.build_sources(((s, None) for s in (sources if sources is not None else [ {'key': 'ups', 'sensor': 'sensor.ups', 'zone': 'living'}, {'key': 'pc', 'sensor': 'sensor.pc', 'zone': 'living', 'included_in': 'ups'}])), built) windows = wm.build_windows(((w, None) for w in (windows if windows is not None else [ {'key': 'w1', 'sensor': 'binary_sensor.w1', 'zone': 'bed', 'azimuth': 345}, {'key': 'w2', 'sensor': 'binary_sensor.w2', 'zone': 'living', 'azimuth': 345}])), built) return cp.zone_fingerprints(built, sources, windows, recovery), sources def differing(a, b): return {zone: sorted(k for k in set(a[zone]) | set(b[zone]) if a[zone].get(k) != b[zone].get(k)) for zone in a if zone in b and a[zone] != b[zone]} class FingerprintTests(unittest.TestCase): def setUp(self): self.base, _ = setup() def test_deterministic_and_comfort_settings_are_not_physical(self): self.assertEqual(setup()[0], self.base) zones = [{'key': 'living', 'name': 'Renamed', 'sensors': ['sensor.l1', 'sensor.l2'], 'comfort_weight': 5, 'target_offset': 1.5, 'sensor_weights': {'sensor.l1': 2, 'sensor.l2': 2}, 'neighbors': ['bed']}, {'key': 'bed', 'name': 'Bedroom', 'sensors': ['sensor.b'], 'comfort_weight': 3}] self.assertEqual(differing(self.base, setup(zones)[0]), {}) # equal weights scaled: same average def test_measurement_changes_reset_the_whole_zone_and_its_neighbour_term(self): for sensors, weights in ((['sensor.l1', 'sensor.l2'], {'sensor.l1': 3, 'sensor.l2': 1}), # weight ratio (['sensor.l1', 'sensor.l3'], {}), # replaced (['sensor.l1'], {})): # removed with self.subTest(sensors=sensors, weights=weights): zones = [{'key': 'living', 'sensors': sensors, 'sensor_weights': weights, 'neighbors': ['bed']}, {'key': 'bed', 'sensors': ['sensor.b']}] diff = differing(self.base, setup(zones)[0]) self.assertEqual(set(diff['living']), set(self.base['living'])) self.assertEqual(diff['bed'], ['couple:living']) # the bedroom's own model is untouched def test_heat_source_changes_reset_only_the_electrical_term(self): moved = [{'key': 'ups', 'sensor': 'sensor.ups', 'zone': 'bed'}, {'key': 'pc', 'sensor': 'sensor.pc', 'zone': 'living', 'included_in': 'ups'}] self.assertEqual(differing(self.base, setup(sources=moved)[0]), {'living': ['internal'], 'bed': ['internal']}) for change in ({'sensor': 'sensor.ups2'}, {'heat_fraction': 0.5}, {'response_minutes': 30}, {'baseline_mode': 'manual', 'baseline_w': 150}): with self.subTest(change=change): sources = [{'key': 'ups', 'sensor': 'sensor.ups', 'zone': 'living', **change}, {'key': 'pc', 'sensor': 'sensor.pc', 'zone': 'living', 'included_in': 'ups'}] self.assertEqual(differing(self.base, setup(sources=sources)[0]), {'living': ['internal']}) def test_window_changes_are_selective(self): turned = [{'key': 'w1', 'sensor': 'binary_sensor.w1', 'zone': 'bed', 'azimuth': 300}, {'key': 'w2', 'sensor': 'binary_sensor.w2', 'zone': 'living', 'azimuth': 345}] self.assertEqual(differing(self.base, setup(windows=turned)[0]), {'bed': ['vent_wind_dir']}) bigger = [{'key': 'w1', 'sensor': 'binary_sensor.w1', 'zone': 'bed', 'azimuth': 345, 'opening_factor': 2}, {'key': 'w2', 'sensor': 'binary_sensor.w2', 'zone': 'living', 'azimuth': 345}] diff = differing(self.base, setup(windows=bigger)[0]) self.assertEqual(diff['bed'], ['recovery', 'vent_cross', 'vent_gust', 'vent_natural', 'vent_wind', 'vent_wind_dir']) self.assertEqual(diff['living'], ['vent_cross']) # sees the bedroom's windows self.assertEqual(differing(self.base, setup(recovery=40)[0]), {'living': ['recovery'], 'bed': ['recovery']}) def test_baseline_prints_and_change_detection(self): _, sources = setup() prints = cp.baseline_fingerprints(sources) _, without_pc = setup(sources=[{'key': 'ups', 'sensor': 'sensor.ups', 'zone': 'living'}]) self.assertNotEqual(prints['ups'], cp.baseline_fingerprints(without_pc)['ups']) self.assertEqual(cp.changed(None, self.base['living'], ['base']), []) # 0.4.0 data: assumed to match self.assertEqual(cp.changed({'base': 'x'}, self.base['living'], ['base', 'internal']), ['base']) self.assertEqual(cp.reason('couple:bed'), "a neighbouring zone's sensors changed") if __name__ == '__main__': unittest.main()