Coverage for tests/tests/test_apis_hvac.py: 0%
54 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
5from datetime import datetime as dt
6from typing import Any
8from ramses_tx.command import CODE_API_MAP, Command
9from ramses_tx.message import Message
10from ramses_tx.packet import Packet
13def _test_api(api: Callable, packets: dict[str]) -> None: # NOTE: incl. addr_set check
14 """Test a verb|code pair that has a Command constructor, src and dst."""
16 for pkt_line, kwargs in packets.items():
17 pkt = _create_pkt_from_frame(pkt_line)
19 msg = Message(pkt)
21 _test_api_from_kwargs(api, pkt, **kwargs)
22 _test_api_from_msg(api, msg)
25def _test_api_one(
26 api: Callable, packets: dict[str]
27) -> None: # NOTE: incl. addr_set check
28 """Test a verb|code pair that has a Command constructor and src, but no dst."""
30 for pkt_line, kwargs in packets.items():
31 pkt = _create_pkt_from_frame(pkt_line)
33 msg = Message(pkt)
35 _test_api_one_from_kwargs(api, pkt, **kwargs)
36 _test_api_one_from_msg(api, msg)
39def _create_pkt_from_frame(pkt_line: str) -> Packet:
40 """Create a pkt from a pkt_line and assert their frames match."""
42 pkt = Packet.from_port(dt.now(), pkt_line)
43 assert str(pkt) == pkt_line[4:]
44 return pkt
47def _test_api_from_msg(api: Callable, msg: Message) -> Command:
48 """Create a cmd from a msg with a src_id, and assert they're equal
49 (*also* asserts payload)."""
51 cmd: Command = api(
52 msg.dst.id,
53 src_id=msg.src.id,
54 **{k: v for k, v in msg.payload.items() if k[:1] != "_"},
55 )
57 assert cmd == msg._pkt # must have exact same addr set
59 return cmd
62def _test_api_one_from_msg(api: Callable, msg: Message) -> Command:
63 """Create a cmd from a msg and assert they're equal (*also* asserts payload)."""
65 cmd: Command = api(
66 msg.dst.id,
67 **{k: v for k, v in msg.payload.items()}, # if k[:1] != "_"},
68 # requirement turned off as it skips required item like _unknown_fan_info_flags
69 )
71 assert cmd == msg._pkt # must have exact same addr set
73 return cmd
76def _test_api_from_kwargs(api: Callable, pkt: Packet, **kwargs: Any) -> None:
77 """
78 Test comparing a created packet to an expected result.
80 :param api: Command lookup by Verb|Code
81 :param pkt: expected result to match
82 :param kwargs: arguments for the Command
83 """
84 cmd = api(HRU, src_id=REM, **kwargs)
86 assert str(cmd) == str(pkt)
89def _test_api_one_from_kwargs(api: Callable, pkt: Packet, **kwargs: Any) -> None:
90 cmd = api(HRU, **kwargs)
92 assert str(cmd) == str(pkt)
95def test_set() -> None:
96 for test_pkts in (SET_22F1_KWARGS, SET_22F7_KWARGS):
97 pkt = list(test_pkts)[0]
98 api = CODE_API_MAP[f"{pkt[4:6]}|{pkt[41:45]}"]
99 _test_api(api, test_pkts)
102HRU = "32:155617" # also used as a FAN
103REM = "37:171871"
104NUL = "--:------"
106SET_22F1_KWARGS = {
107 f"000 I --- {REM} {HRU} {NUL} 22F1 002 0000": {"fan_mode": None},
108 #
109 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0000": {"fan_mode": 0},
110 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0001": {"fan_mode": 1},
111 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0002": {"fan_mode": 2},
112 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0003": {"fan_mode": 3},
113 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0004": {"fan_mode": 4},
114 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0005": {"fan_mode": 5},
115 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0006": {"fan_mode": 6},
116 f"001 I --- {REM} {HRU} {NUL} 22F1 002 0007": {"fan_mode": 7},
117 #
118 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0000": {"fan_mode": "00"},
119 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0001": {"fan_mode": "01"},
120 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0002": {"fan_mode": "02"},
121 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0003": {"fan_mode": "03"},
122 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0004": {"fan_mode": "04"},
123 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0005": {"fan_mode": "05"},
124 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0006": {"fan_mode": "06"},
125 f"002 I --- {REM} {HRU} {NUL} 22F1 002 0007": {"fan_mode": "07"},
126 #
127 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0000": {"fan_mode": "away"},
128 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0001": {"fan_mode": "low"},
129 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0002": {"fan_mode": "medium"},
130 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0003": {"fan_mode": "high"},
131 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0004": {"fan_mode": "auto"},
132 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0005": {"fan_mode": "auto_alt"},
133 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0006": {"fan_mode": "boost"},
134 f"003 I --- {REM} {HRU} {NUL} 22F1 002 0007": {"fan_mode": "off"},
135}
138SET_22F7_KWARGS = {
139 f"000 W --- {REM} {HRU} {NUL} 22F7 002 00FF": {}, # shouldn't be OK
140 #
141 f"001 W --- {REM} {HRU} {NUL} 22F7 002 00FF": {
142 "bypass_position": None
143 }, # is auto?
144 f"001 W --- {REM} {HRU} {NUL} 22F7 002 0000": {"bypass_position": 0.0},
145 # 001 W --- {REM} {HRU} {NUL} 22F7 002 0064": {"bypass_position": 0.5},
146 f"001 W --- {REM} {HRU} {NUL} 22F7 002 00C8": {"bypass_position": 1.0},
147 f"002 W --- {REM} {HRU} {NUL} 22F7 002 00FF": {
148 "bypass_mode": "auto"
149 }, # is auto, or None?
150 f"002 W --- {REM} {HRU} {NUL} 22F7 002 0000": {"bypass_mode": "off"},
151 f"002 W --- {REM} {HRU} {NUL} 22F7 002 00C8": {"bypass_mode": "on"},
152}
155# new tests
156def test_get() -> None:
157 for test_pkts in (GET_12A0_KWARGS, GET_1298_KWARGS, GET_31DA_KWARGS):
158 pkt = list(test_pkts)[0]
159 api = CODE_API_MAP[f"{pkt[4:6]}|{pkt[41:45]}"]
160 _test_api_one(api, test_pkts)
163GET_12A0_KWARGS = {
164 f"000 I --- {HRU} {NUL} {HRU} 12A0 002 00EF": {
165 "indoor_humidity": None
166 }, # shouldn't be OK
167 #
168 f"082 I --- {HRU} {NUL} {HRU} 12A0 002 0037": {"indoor_humidity": 0.55},
169}
171GET_1298_KWARGS = {
172 f"064 I --- {HRU} {NUL} {HRU} 1298 003 000322": {"co2_level": 802},
173}
175GET_31DA_KWARGS = {
176 # this is a composite payload, containing many keys
177 # 31DA packet from values in ramses_tx/command.py#get_hvac_fan_31da
178 f"... I --- {HRU} {NUL} {HRU} 31DA 029 00EF007FFF343308980898088A0882F800001514140000EFEF05F50613": {
179 "hvac_id": "00",
180 "bypass_position": 0.000,
181 "air_quality": None,
182 "co2_level": None,
183 "indoor_humidity": 0.52,
184 "outdoor_humidity": 0.51,
185 "exhaust_temp": 22.0,
186 "supply_temp": 22.0,
187 "indoor_temp": 21.86,
188 "outdoor_temp": 21.78,
189 "speed_capabilities": ["off", "low_med_high", "timer", "boost", "auto"],
190 "fan_info": "away",
191 "_unknown_fan_info_flags": [0, 0, 0],
192 "exhaust_fan_speed": 0.1,
193 "supply_fan_speed": 0.1,
194 "remaining_mins": 0,
195 "post_heat": None,
196 "pre_heat": None,
197 "supply_flow": 15.25,
198 "exhaust_flow": 15.55,
199 },
200 f"... I --- {HRU} {NUL} {HRU} 31DA 029 00C84004B2EFEF7FFF7FFF7FFF7FFFF808EF831F000000EFEF7FFF7FFF": {
201 "hvac_id": "00",
202 "co2_level": 1202,
203 "air_quality": 1.0,
204 "air_quality_basis": "rel_humidity",
205 "indoor_humidity": None,
206 "outdoor_humidity": None,
207 "exhaust_temp": None,
208 "supply_temp": None,
209 "indoor_temp": None,
210 "outdoor_temp": None,
211 "speed_capabilities": [
212 "off",
213 "low_med_high",
214 "timer",
215 "boost",
216 "auto",
217 "auto_night",
218 ],
219 "bypass_position": None,
220 "fan_info": "speed 3, high",
221 "_unknown_fan_info_flags": [1, 0, 0],
222 "exhaust_fan_speed": 0.155,
223 "supply_fan_speed": 0.0,
224 "remaining_mins": 0,
225 "post_heat": None,
226 "pre_heat": None,
227 "supply_flow": None,
228 "exhaust_flow": None,
229 },
230 f"... I --- {HRU} {NUL} {HRU} 31DA 029 00EF007FFF30EF7FFF7FFF7FFF7FFFF808EF83C7000000EFEF7FFF7FFF": {
231 "hvac_id": "00",
232 "speed_capabilities": [
233 "off",
234 "low_med_high",
235 "timer",
236 "boost",
237 "auto",
238 "auto_night",
239 ],
240 "fan_info": "speed 3, high",
241 "_unknown_fan_info_flags": [1, 0, 0],
242 "air_quality": None,
243 "co2_level": None,
244 "indoor_humidity": 0.48,
245 "outdoor_humidity": None,
246 "exhaust_temp": None,
247 "supply_temp": None,
248 "indoor_temp": None,
249 "outdoor_temp": None,
250 "bypass_position": None,
251 "exhaust_fan_speed": 0.995,
252 "supply_fan_speed": 0.0,
253 "remaining_mins": 0,
254 "post_heat": None,
255 "pre_heat": None,
256 "supply_flow": None,
257 "exhaust_flow": None,
258 },
259 f"... I --- {HRU} {NUL} {HRU} 31DA 029 00EF007FFFEFEF7FFF7FFF7FFF7FFFF000EF0162000000EFEF7FFF7FFF": {
260 "hvac_id": "00",
261 "outdoor_humidity": None,
262 "outdoor_temp": None,
263 "air_quality": None,
264 "co2_level": None,
265 "indoor_humidity": None,
266 "exhaust_temp": None,
267 "supply_temp": None,
268 "indoor_temp": None,
269 "speed_capabilities": ["off", "low_med_high", "timer", "boost"],
270 "bypass_position": None,
271 "fan_info": "speed 1, low",
272 "_unknown_fan_info_flags": [0, 0, 0],
273 "exhaust_fan_speed": 0.49,
274 "supply_fan_speed": 0.0,
275 "remaining_mins": 0,
276 "post_heat": None,
277 "pre_heat": None,
278 "supply_flow": None,
279 "exhaust_flow": None,
280 },
281 f"... I --- {HRU} {NUL} {HRU} 31DA 029 21EF00020136EF7FFF7FFF7FFF7FFF0002EF18FFFF000000EF7FFF7FFF": {
282 "hvac_id": "21",
283 "speed_capabilities": ["post_heater"],
284 "fan_info": "auto",
285 "_unknown_fan_info_flags": [0, 0, 0],
286 "air_quality": None,
287 "co2_level": 513,
288 "indoor_humidity": 0.54,
289 "outdoor_humidity": None,
290 "exhaust_temp": None,
291 "supply_temp": None,
292 "indoor_temp": None,
293 "outdoor_temp": None,
294 "bypass_position": None,
295 "exhaust_fan_speed": None,
296 "supply_fan_speed": None,
297 "remaining_mins": 0,
298 "post_heat": 0.0,
299 "pre_heat": None,
300 "supply_flow": None,
301 "exhaust_flow": None,
302 },
303 # messages with 30 byte payload
304 f"... I --- {HRU} {NUL} {HRU} 31DA 030 00EF007FFF2CEF7FFF7FFF7FFF7FFFF800EF0128000000EFEF7FFF7FFF00": {
305 "hvac_id": "00",
306 "exhaust_temp": None,
307 "air_quality": None,
308 "co2_level": None,
309 "indoor_humidity": 0.44,
310 "outdoor_humidity": None,
311 "supply_temp": None,
312 "indoor_temp": None,
313 "outdoor_temp": None,
314 "speed_capabilities": ["off", "low_med_high", "timer", "boost", "auto"],
315 "bypass_position": None,
316 "fan_info": "speed 1, low",
317 "_unknown_fan_info_flags": [0, 0, 0],
318 "exhaust_fan_speed": 0.2,
319 "supply_fan_speed": 0.0,
320 "remaining_mins": 0,
321 "post_heat": None,
322 "pre_heat": None,
323 "supply_flow": None,
324 "exhaust_flow": None,
325 "_extra": "00",
326 },
327 f"... I --- {HRU} {NUL} {HRU} 31DA 030 21EF007FFF41EF080607D709480737F002AA0234400000005C7FFF7FFF00": {
328 "hvac_id": "21",
329 "exhaust_fan_speed": 0.26,
330 "supply_fan_speed": 0.32,
331 "supply_flow": None,
332 "exhaust_flow": None,
333 "air_quality": None,
334 "co2_level": None,
335 "indoor_humidity": 0.65,
336 "outdoor_humidity": None,
337 "exhaust_temp": 20.54,
338 "supply_temp": 20.08,
339 "indoor_temp": 23.76,
340 "outdoor_temp": 18.47,
341 "speed_capabilities": ["off", "low_med_high", "timer", "boost", "post_heater"],
342 "bypass_position": 0.85,
343 "fan_info": "speed 2, medium",
344 "_unknown_fan_info_flags": [0, 0, 0],
345 "remaining_mins": 0,
346 "post_heat": 0.0,
347 "pre_heat": 0.46,
348 "_extra": "00",
349 },
350}
353# TODO Add tests to get states from 31DA
354# (verifies SQLite refactoring)
355# set up HVAC system first from messages
356#
357# Example: current_temperature(self) in ramses_cc.climate.py
358# simulates requesting Climate self._device.indoor_temp from a system