Coverage for tests/tests/test_apis_heat.py: 0%
130 statements
« prev ^ index » next coverage.py v7.11.3, created at 2026-01-05 21:46 +0100
« prev ^ index » next coverage.py v7.11.3, created at 2026-01-05 21:46 +0100
1#!/usr/bin/env python3
2"""RAMSES RF - Test the Command.put_*, Command.set_* APIs."""
4from collections.abc import Callable, Iterable
5from datetime import datetime as dt
7from ramses_rf.const import SZ_DOMAIN_ID
8from ramses_rf.helpers import shrink
9from ramses_tx.address import HGI_DEV_ADDR
10from ramses_tx.command import Command
11from ramses_tx.const import SZ_TIMESTAMP
12from ramses_tx.helpers import parse_fault_log_entry
13from ramses_tx.message import Message
14from ramses_tx.packet import Packet
17# NOTE: not used for 0418
18def _test_api_good(
19 api: Callable, packets: Iterable[str]
20) -> None: # NOTE: incl. addr_set check
21 """Test a verb|code pair that has a Command constructor."""
23 for pkt_line in packets:
24 pkt = _create_pkt_from_frame(pkt_line.split("#")[0].rstrip())
25 msg = Message(pkt)
27 cmd = _test_api_from_msg(api, msg)
28 assert cmd.payload == msg._pkt.payload # aka pkt.payload
30 if isinstance(packets, dict) and (payload := packets[pkt_line]):
31 assert shrink(msg.payload, keep_falsys=True) == eval(payload)
34def _test_api_fail(
35 api: Callable, packets: Iterable[str]
36) -> None: # NOTE: incl. addr_set check
37 """Test a verb|code pair that has a Command constructor."""
39 for pkt_line in packets:
40 pkt = _create_pkt_from_frame(pkt_line.split("#")[0].rstrip())
41 msg = Message(pkt)
43 try:
44 cmd = _test_api_from_msg(api, msg)
45 except (AssertionError, TypeError, ValueError):
46 cmd = None
47 else:
48 assert cmd and cmd.payload == msg._pkt.payload # aka pkt.payload
50 if isinstance(packets, dict) and (payload := packets[pkt_line]):
51 assert shrink(msg.payload, keep_falsys=True) == eval(payload)
54def _create_pkt_from_frame(pkt_line: str) -> Packet:
55 """Create a pkt from a pkt_line and assert their frames match."""
57 pkt = Packet.from_port(dt.now(), pkt_line)
58 assert str(pkt) == pkt_line[4:]
59 return pkt
62def _test_api_from_msg(api: Callable, msg: Message) -> Command:
63 """Create a cmd from a msg and assert their meta-data (doesn"t assert payload.)."""
65 cmd: Command = api(
66 msg.dst.id, **{k: v for k, v in msg.payload.items() if k[:1] != "_"}
67 )
69 if msg.src.id == HGI_DEV_ADDR.id:
70 assert cmd == msg._pkt # assert str(cmd) == str(pkt)
71 assert cmd.dst.id == msg._pkt.dst.id
72 assert cmd.verb == msg._pkt.verb
73 assert cmd.code == msg._pkt.code
74 # assert cmd.payload == pkt.payload
76 return cmd
79SET_0004_FAIL = (
80 "... W --- 18:000730 01:145038 --:------ 0004 022 05000000000000000000000000000000000000000000", # name is None
81 "... W --- 18:000730 01:145038 --:------ 0004 022 05005468697320497320412056657279204C6F6E6720", # trailing space
82)
83SET_0004_GOOD = (
84 "... W --- 18:000730 01:145038 --:------ 0004 022 00004D617374657220426564726F6F6D000000000000",
85 "... W --- 18:000730 01:145038 --:------ 0004 022 05005468697320497320412056657279204C6F6E6767",
86)
89def test_set_0004() -> None:
90 _test_api_good(Command.set_zone_name, SET_0004_GOOD)
91 _test_api_fail(Command.set_zone_name, SET_0004_FAIL)
94SET_000A_GOOD = (
95 "... W --- 18:000730 01:145038 --:------ 000A 006 010001F40DAC",
96 "... W --- 18:000730 01:145038 --:------ 000A 006 031001F409C4",
97 "... W --- 18:000730 01:145038 --:------ 000A 006 050201F40898",
98)
101def test_set_000a() -> None:
102 _test_api_good(Command.set_zone_config, SET_000A_GOOD)
105GET_0404_GOOD = {
106 "... RQ --- 18:000730 01:076010 --:------ 0404 007 00230008000100": "{'zone_idx': 'HW', 'frag_number': 1, 'total_frags': None}",
107 "... RQ --- 18:000730 01:076010 --:------ 0404 007 02200008000100": "{'zone_idx': '02', 'frag_number': 1, 'total_frags': None}",
108 "... RQ --- 18:000730 01:076010 --:------ 0404 007 02200008000204": "{'zone_idx': '02', 'frag_number': 2, 'total_frags': 4}",
109 "... RQ --- 18:000730 01:076010 --:------ 0404 007 02200008000304": "{'zone_idx': '02', 'frag_number': 3, 'total_frags': 4}",
110 "... RQ --- 18:000730 01:076010 --:------ 0404 007 02200008000404": "{'zone_idx': '02', 'frag_number': 4, 'total_frags': 4}",
111}
114def test_get_0404() -> None:
115 _test_api_good(Command.get_schedule_fragment, GET_0404_GOOD)
118GET_0418_GOOD = { # NOTE: this constructor is used only for testing
119 "... I --- 01:145038 --:------ 01:145038 0418 022 000000B0000000000000000000007FFFFF7000000000": "{'log_idx': '00', 'log_entry': None}",
120 "... I --- 01:145038 --:------ 01:145038 0418 022 000000B0060804000000B897A0697FFFFF70001003B6": "{'log_idx': '00', 'log_entry': ('23-11-17T20:03:18', 'fault', 'comms_fault', 'actuator', '08', '04:000950', 'B0', '0000', 'FFFF7000')}",
121}
124# NOTE: does not use _test_api_good() as main payload is a tuple, and not a dict
125def test_put_0418() -> None:
126 for pkt_line in GET_0418_GOOD:
127 pkt = _create_pkt_from_frame(pkt_line.split("#")[0].rstrip())
128 log_pkt = parse_fault_log_entry(pkt.payload)
130 if SZ_TIMESTAMP not in log_pkt: # ignore null log entries
131 continue
133 cmd = Command._put_system_log_entry(pkt.src.id, **log_pkt) # type: ignore[call-arg]
134 log_cmd = parse_fault_log_entry(cmd.payload)
136 assert log_pkt == log_cmd
139SET_1030_GOOD = { # NOTE: no W|1030 seen in the wild
140 "... W --- 18:000730 01:145038 --:------ 1030 016 01C80137C9010FCA0196CB010FCC0101": "{'zone_idx': '01', 'max_flow_setpoint': 55, 'min_flow_setpoint': 15, 'valve_run_time': 150, 'pump_run_time': 15, 'boolean_cc': 1}",
141}
144def test_set_1030() -> None:
145 _test_api_good(Command.set_mix_valve_params, SET_1030_GOOD)
148SET_10A0_GOOD = { # NOTE: no W|10A0 seen in the wild
149 "000 W --- 01:123456 07:031785 --:------ 10A0 006 000F6E050064": "{'dhw_idx': '00', 'setpoint': 39.5, 'overrun': 5, 'differential': 1.0}",
150 "000 W --- 01:123456 07:031785 --:------ 10A0 006 000F6E0003E8": "{'dhw_idx': '00', 'setpoint': 39.5, 'overrun': 0, 'differential': 10.0}",
151 "000 W --- 01:123456 07:031785 --:------ 10A0 006 0015180301F4": "{'dhw_idx': '00', 'setpoint': 54.0, 'overrun': 3, 'differential': 5.0}",
152 "000 W --- 01:123456 07:031785 --:------ 10A0 006 0013240003E8": "{'dhw_idx': '00', 'setpoint': 49.0, 'overrun': 0, 'differential': 10.0}",
153 #
154 "001 W --- 01:123456 07:031785 --:------ 10A0 006 010F6E050064": "{'dhw_idx': '01', 'setpoint': 39.5, 'overrun': 5, 'differential': 1.0}",
155 "001 W --- 01:123456 07:031785 --:------ 10A0 006 010F6E0003E8": "{'dhw_idx': '01', 'setpoint': 39.5, 'overrun': 0, 'differential': 10.0}",
156 "001 W --- 01:123456 07:031785 --:------ 10A0 006 0115180301F4": "{'dhw_idx': '01', 'setpoint': 54.0, 'overrun': 3, 'differential': 5.0}",
157 "001 W --- 01:123456 07:031785 --:------ 10A0 006 0113240003E8": "{'dhw_idx': '01', 'setpoint': 49.0, 'overrun': 0, 'differential': 10.0}",
158}
161def test_set_10a0() -> None:
162 _test_api_good(Command.set_dhw_params, SET_10A0_GOOD)
165SET_1100_FAIL = (
166 "... W --- 01:145038 13:163733 --:------ 1100 008 000C1400007FFF01", # no domain_id
167)
168SET_1100_GOOD = {
169 "... W --- 01:145038 13:035462 --:------ 1100 008 00240414007FFF01": "{'domain_id': '00', 'cycle_rate': 9, 'min_on_time': 1.0, 'min_off_time': 5.0, 'proportional_band_width': None}",
170 "... W --- 01:145038 13:163733 --:------ 1100 008 000C14000000C801": "{'domain_id': '00', 'cycle_rate': 3, 'min_on_time': 5.0, 'min_off_time': 0.0, 'proportional_band_width': 2.0}",
171 "... W --- 01:145038 13:163733 --:------ 1100 008 00180400007FFF01": "{'domain_id': '00', 'cycle_rate': 6, 'min_on_time': 1.0, 'min_off_time': 0.0, 'proportional_band_width': None}",
172 "... W --- 01:145038 13:035462 --:------ 1100 008 FC042814007FFF01": "{'domain_id': 'FC', 'cycle_rate': 1, 'min_on_time': 10.0, 'min_off_time': 5.0, 'proportional_band_width': None}",
173 "... W --- 01:145038 13:035462 --:------ 1100 008 FC082814007FFF01": "{'domain_id': 'FC', 'cycle_rate': 2, 'min_on_time': 10.0, 'min_off_time': 5.0, 'proportional_band_width': None}",
174 "... W --- 01:145038 13:035462 --:------ 1100 008 FC243C14007FFF01": "{'domain_id': 'FC', 'cycle_rate': 9, 'min_on_time': 15.0, 'min_off_time': 5.0, 'proportional_band_width': None}",
175 "... W --- 01:145038 13:035462 --:------ 1100 008 FC240414007FFF01": "{'domain_id': 'FC', 'cycle_rate': 9, 'min_on_time': 1.0, 'min_off_time': 5.0, 'proportional_band_width': None}",
176 "... W --- 01:145038 13:035462 --:------ 1100 008 FC240428007FFF01": "{'domain_id': 'FC', 'cycle_rate': 9, 'min_on_time': 1.0, 'min_off_time': 10.0, 'proportional_band_width': None}",
177 "... W --- 01:145038 13:035462 --:------ 1100 008 FC083C14007FFF01": "{'domain_id': 'FC', 'cycle_rate': 2, 'min_on_time': 15.0, 'min_off_time': 5.0, 'proportional_band_width': None}",
178 "... W --- 01:145038 13:035462 --:------ 1100 008 FC083C00007FFF01": "{'domain_id': 'FC', 'cycle_rate': 2, 'min_on_time': 15.0, 'min_off_time': 0.0, 'proportional_band_width': None}",
179}
182def test_set_1100() -> None: # NOTE: bespoke: see params
183 packets = SET_1100_GOOD
185 for pkt_line in packets:
186 pkt = _create_pkt_from_frame(pkt_line)
187 msg = Message(pkt)
189 msg.payload[SZ_DOMAIN_ID] = msg.payload.get(SZ_DOMAIN_ID, "00")
191 cmd = _test_api_from_msg(Command.set_tpi_params, msg)
192 assert cmd.payload == msg._pkt.payload
194 if isinstance(packets, dict) and (payload := packets[pkt_line]):
195 assert shrink(msg.payload, keep_falsys=True) == eval(payload)
198PUT_1260_GOOD = { # TODO: RPs being converted to Is
199 "... I --- 07:017494 --:------ 07:017494 1260 003 00111E": "{'temperature': 43.82}",
200 "... I --- 07:017494 --:------ 07:017494 1260 003 007FFF": "{'temperature': None}",
201 # "... I --- 07:123456 --:------ 07:123456 1260 003 010E74": "{'temperature': 37.0, 'dhw_idx': '01'}", # contrived
202 # "... I --- 07:123456 --:------ 07:123456 1260 003 017FFF": "{'temperature': None}", # contrived
203 # "... RP --- 01:123456 18:123456 --:------ 1260 003 00116A": "{'temperature': 44.58}",
204 # "... RP --- 01:078710 18:002563 --:------ 1260 003 00116A": "{'temperature': 44.58, 'dhw_idx': '00'}",
205 # "... RP --- 01:078710 18:002563 --:------ 1260 003 01116A": "{'temperature': 44.58, 'dhw_idx': '01'}", # contrived
206 # "... RP --- 10:124973 18:132629 --:------ 1260 003 000E74": "{'temperature': 37.0}",
207}
210def test_set_1260() -> None:
211 _test_api_good(Command.put_dhw_temp, PUT_1260_GOOD)
214SET_1F41_GOOD = {
215 # 00 W --- 18:000730 01:050858 --:------ 1F41 006 000000FFFFFF ": "{'dhw_idx': '00', 'mode': 'follow_schedule'}",
216 # 00 W --- 18:000730 01:050858 --:------ 1F41 006 000100FFFFFF ": "{'dhw_idx': '00', 'mode': 'follow_schedule'}",
217 "000 W --- 18:000730 01:050858 --:------ 1F41 006 00FF00FFFFFF ": "{'dhw_idx': '00', 'mode': 'follow_schedule'}",
218 "000 W --- 18:000730 01:050858 --:------ 1F41 006 000102FFFFFF ": "{'dhw_idx': '00', 'mode': 'permanent_override', 'active': 1}",
219 "000 W --- 18:000730 01:050858 --:------ 1F41 012 000004FFFFFF0509160607E5": "{'dhw_idx': '00', 'mode': 'temporary_override', 'active': 0, 'until': '2021-06-22T09:05:00'}",
220 "000 W --- 18:000730 01:050858 --:------ 1F41 012 000104FFFFFF2F0E0D0B07E5": "{'dhw_idx': '00', 'mode': 'temporary_override', 'active': 1, 'until': '2021-11-13T14:47:00'}",
221 #
222 # 01 W --- 18:000730 01:050858 --:------ 1F41 006 010000FFFFFF ": "{'dhw_idx': '01', 'mode': 'follow_schedule'}",
223 # 01 W --- 18:000730 01:050858 --:------ 1F41 006 010100FFFFFF ": "{'dhw_idx': '01', 'mode': 'follow_schedule'}",
224 "001 W --- 18:000730 01:050858 --:------ 1F41 006 01FF00FFFFFF ": "{'dhw_idx': '01', 'mode': 'follow_schedule'}",
225 "001 W --- 18:000730 01:050858 --:------ 1F41 006 010102FFFFFF ": "{'dhw_idx': '01', 'mode': 'permanent_override', 'active': 1}",
226 "001 W --- 18:000730 01:050858 --:------ 1F41 012 010004FFFFFF0509160607E5": "{'dhw_idx': '01', 'mode': 'temporary_override', 'active': 0, 'until': '2021-06-22T09:05:00'}",
227 "001 W --- 18:000730 01:050858 --:------ 1F41 012 010104FFFFFF2F0E0D0B07E5": "{'dhw_idx': '01', 'mode': 'temporary_override', 'active': 1, 'until': '2021-11-13T14:47:00'}",
228} # TODO: add other modes
229SET_1F41_FAIL = (
230 "000 W --- 18:000730 01:050858 --:------ 1F41 006 020000FFFFFF", # dhw_idx = 02
231 "000 W --- 18:000730 01:050858 --:------ 1F41 006 000005FFFFFF", # zone_mode = 05
232 "000 W --- 18:000730 01:050858 --:------ 1F41 006 000005FFFFFF", # zone_mode = 05
233)
236def test_set_1f41() -> None:
237 _test_api_good(Command.set_dhw_mode, SET_1F41_GOOD)
240SET_2309_FAIL = (
241 "... W --- 18:000730 01:145038 --:------ 2309 003 017FFF", # temp is None - should be good?
242)
243SET_2309_GOOD = (
244 "... W --- 18:000730 01:145038 --:------ 2309 003 00047E",
245 "... W --- 18:000730 01:145038 --:------ 2309 003 0101F4",
246)
249def test_set_2309() -> None:
250 _test_api_good(Command.set_zone_setpoint, SET_2309_GOOD)
253SET_2349_GOOD = (
254 "... W --- 18:005567 01:223036 --:------ 2349 007 037FFF00FFFFFF",
255 "... W --- 22:015492 01:076010 --:------ 2349 007 0101F400FFFFFF",
256 "... W --- 18:000730 01:145038 --:------ 2349 007 06028A01FFFFFF",
257 "... W --- 22:081652 01:063844 --:------ 2349 007 0106400300003C",
258 "... W --- 18:000730 01:050858 --:------ 2349 013 06096004FFFFFF240A050107E6",
259 "... W --- 18:000730 01:050858 --:------ 2349 013 02096004FFFFFF1B0D050107E6",
260)
263def test_set_2349() -> None:
264 _test_api_good(Command.set_zone_mode, SET_2349_GOOD)
267SET_2E04_GOOD = {
268 "... W --- 30:258720 01:073976 --:------ 2E04 008 00FFFFFFFFFFFF00": "{'system_mode': 'auto'}",
269 "... W --- 30:258720 01:073976 --:------ 2E04 008 01FFFFFFFFFFFF00": "{'system_mode': 'heat_off'}",
270 "... W --- 30:258720 01:073976 --:------ 2E04 008 06FFFFFFFFFFFF00": "{'system_mode': 'auto_with_reset'}",
271 #
272 "... W --- 30:258720 01:073976 --:------ 2E04 008 03FFFFFFFFFFFF00": "{'system_mode': 'away', 'until': None}",
273 "... W --- 30:258720 01:073976 --:------ 2E04 008 0300001D0A07E301": "{'system_mode': 'away', 'until': '2019-10-29T00:00:00'}",
274 "... W --- 30:258720 01:073976 --:------ 2E04 008 07FFFFFFFFFFFF00": "{'system_mode': 'custom', 'until': None}",
275 "... W --- 30:258720 01:073976 --:------ 2E04 008 0700001D0A07E301": "{'system_mode': 'custom', 'until': '2019-10-29T00:00:00'}",
276 "... W --- 30:258720 01:073976 --:------ 2E04 008 02FFFFFFFFFFFF00": "{'system_mode': 'eco_boost', 'until': None}",
277 "... W --- 30:258720 01:073976 --:------ 2E04 008 020B011A0607E401": "{'system_mode': 'eco_boost', 'until': '2020-06-26T01:11:00'}",
278 "... W --- 30:258720 01:073976 --:------ 2E04 008 04FFFFFFFFFFFF00": "{'system_mode': 'day_off', 'until': None}",
279 "... W --- 30:258720 01:073976 --:------ 2E04 008 0400001D0A07E301": "{'system_mode': 'day_off', 'until': '2019-10-29T00:00:00'}",
280 "... W --- 30:258720 01:073976 --:------ 2E04 008 05FFFFFFFFFFFF00": "{'system_mode': 'day_off_eco', 'until': None}",
281 "... W --- 30:258720 01:073976 --:------ 2E04 008 0500001D0A07E301": "{'system_mode': 'day_off_eco', 'until': '2019-10-29T00:00:00'}",
282 "... W --- 30:258720 01:073976 --:------ 2E04 008 0521011A0607E401": "{'system_mode': 'day_off_eco', 'until': '2020-06-26T01:33:00'}", # a contrived time, usu. 00:00
283}
286def test_set_2e04() -> None:
287 _test_api_good(Command.set_system_mode, SET_2E04_GOOD)
290PUT_30C9_FAIL = (
291 "... I --- 13:074756 --:------ 13:074756 30C9 003 007FFF",
292 "... I --- 01:197498 --:------ 01:197498 30C9 024 01086D02087003086604070A0508DF06083307083008085C",
293)
294PUT_30C9_GOOD = (
295 "... I --- 04:068997 --:------ 04:068997 30C9 003 007FFF",
296 "... I --- 04:068997 --:------ 04:068997 30C9 003 000838",
297 "... I --- 03:123456 --:------ 03:123456 30C9 003 0007C1",
298 "... I --- 03:123456 --:------ 03:123456 30C9 003 007FFF",
299 "... I --- 13:074756 --:------ 03:074756 30C9 003 00086D",
300)
303def test_put_30c9() -> None:
304 _test_api_good(Command.put_sensor_temp, PUT_30C9_GOOD)
307SET_313F_GOOD = (
308 "... W --- 30:258720 01:073976 --:------ 313F 009 006000320C040207E6",
309 "... W --- 30:258720 01:073976 --:------ 313F 009 0060011E09010707E6",
310 "... W --- 30:258720 01:073976 --:------ 313F 009 006002210D080C07E5",
311 "... W --- 30:042165 01:076010 --:------ 313F 009 006003090A0D0207E6",
312 "... W --- 30:042165 01:076010 --:------ 313F 009 0060041210040207E6",
313)
316def test_set_313f() -> None: # NOTE: bespoke: payload
317 for pkt_line in SET_313F_GOOD:
318 pkt = Packet.from_port(dt.now(), pkt_line)
319 assert str(pkt)[:4] == pkt_line[4:8]
320 assert str(pkt)[6:] == pkt_line[10:]
322 msg = Message(pkt)
324 cmd = _test_api_from_msg(Command.set_system_time, msg)
325 assert cmd.payload[:4] == msg._pkt.payload[:4]
326 assert cmd.payload[6:] == msg._pkt.payload[6:]
329PUT_3EF0_FAIL = ("... I --- 13:123456 --:------ 13:123456 3EF0 003 00AAFF",)
330PUT_3EF0_GOOD = (
331 "... I --- 13:123456 --:------ 13:123456 3EF0 003 0000FF",
332 "... I --- 13:123456 --:------ 13:123456 3EF0 003 00C8FF",
333)
336def test_put_3ef0() -> None:
337 _test_api_good(Command.put_actuator_state, PUT_3EF0_GOOD)
340PUT_3EF1_GOOD = ( # TODO: needs checking
341 "... RP --- 13:123456 01:123456 --:------ 3EF1 007 000126012600FF",
342 "... RP --- 13:123456 18:123456 --:------ 3EF1 007 007FFF003C0010", # NOTE: should be: RP|10|3EF1
343)
346def test_put_3ef1() -> None: # NOTE: bespoke: params, ?payload
347 for pkt_line in PUT_3EF1_GOOD:
348 pkt = _create_pkt_from_frame(pkt_line)
349 msg = Message(pkt)
351 kwargs = msg.payload
352 modulation_level = kwargs.pop("modulation_level")
353 actuator_countdown = kwargs.pop("actuator_countdown")
355 cmd = Command.put_actuator_cycle(
356 msg.src.id,
357 msg.dst.id,
358 modulation_level,
359 actuator_countdown,
360 **{k: v for k, v in kwargs.items() if k[:1] != "_"},
361 )
363 if msg.src.id != HGI_DEV_ADDR.id:
364 assert cmd.src.id == pkt.src.id
365 assert cmd.dst.id == pkt.dst.id
366 assert cmd.verb == pkt.verb
367 assert cmd.code == pkt.code
369 assert cmd.payload[:-2] == pkt.payload[:-2]