Coverage for src/ramses_tx/ramses.py: 100%
53 statements
« prev ^ index » next coverage.py v7.11.3, created at 2026-01-05 21:44 +0100
« prev ^ index » next coverage.py v7.11.3, created at 2026-01-05 21:44 +0100
1#!/usr/bin/env python3
2"""RAMSES RF - a RAMSES-II protocol decoder & analyser."""
4# TODO: code a lifespan for most packets
6from __future__ import annotations
8from datetime import timedelta as td
9from typing import Any, Final
11from .const import SZ_NAME, DevType
13from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
14 I_,
15 RP,
16 RQ,
17 W_,
18 Code,
19 VerbT,
20)
23SZ_LIFESPAN: Final = "lifespan" # WIP
26#
27########################################################################################
28# CODES_SCHEMA - HEAT (CH/DHW, Honeywell/Resideo) vs HVAC (ventilation, Itho/Orcon/etc.)
30# The master list - all known codes are here, even if there's no corresponding parser
31# Anything with a zone-idx should start: ^0[0-9A-F], ^(0[0-9A-F], or ^((0[0-9A-F]
33#
34CODES_SCHEMA: dict[Code, dict[str, Any]] = { # rf_unknown
35 Code._0001: {
36 SZ_NAME: "rf_unknown",
37 I_: r"^00FFFF02(00|FF)$", # loopback
38 RQ: r"^00([28A]0)00(0[0-9A-F])(FF|04)$", # HVAC
39 RP: r"^00([28A]0)00(0[0-9A-F])", # HVAC
40 W_: r"^(0[0-9A-F]|FC|FF)000005(01|05)$",
41 }, # TODO: there appears to be a dodgy? RQ/RP for UFC
42 Code._0002: { # WIP: outdoor_sensor - CODE_IDX_COMPLEX?
43 # is it CODE_IDX_COMPLEX:
44 # - 02...... for outside temp?
45 # - 03...... for other stuff?
46 SZ_NAME: "outdoor_sensor",
47 I_: r"^0[0-4][0-9A-F]{4}(00|01|02|05)$", # Domoticz sends ^02!!
48 RQ: r"^00$", # NOTE: sent by an RFG100
49 },
50 Code._0004: { # zone_name
51 SZ_NAME: "zone_name",
52 I_: r"^0[0-9A-F]00([0-9A-F]){40}$", # RP is same, null_rp: xxxx,7F*20
53 RQ: r"^0[0-9A-F]00$",
54 W_: r"^0[0-9A-F]00([0-9A-F]){40}$", # contrived
55 SZ_LIFESPAN: td(days=1),
56 },
57 Code._0005: { # system_zones
58 SZ_NAME: "system_zones",
59 # .I --- 34:092243 --:------ 34:092243 0005 012 000A0000-000F0000-00100000
60 I_: r"^(00[01][0-9A-F]{5}){1,3}$",
61 RQ: r"^00[01][0-9A-F]$", # f"00{zone_type}", evohome won't respond to 00
62 RP: r"^00[01][0-9A-F]{3,5}$",
63 SZ_LIFESPAN: False,
64 },
65 Code._0006: { # schedule_version # TODO: what for DHW schedule?
66 SZ_NAME: "schedule_version",
67 RQ: r"^00$",
68 RP: r"^0005[0-9A-F]{4}$",
69 },
70 Code._0008: { # relay_demand, TODO: check RP
71 SZ_NAME: "relay_demand",
72 # 000 I --- 31:012319 08:006244 --:------ 0008 013 0006958C33CA6ECD2067AA53DD
73 I_: r"^((0[0-9A-F]|F[9AC])[0-9A-F]{2}|00[0-9A-F]{24})$",
74 RQ: r"^00$",
75 RP: r"^00[0-9A-F]{2}$", # seems only 13: RP (TODO: what about 10:, 08/31:)
76 },
77 Code._0009: { # relay_failsafe (only is_controller, OTB send an 0009?)
78 SZ_NAME: "relay_failsafe",
79 # .I --- 01:145038 --:------ 01:145038 0009 006 FC01FFF901FF
80 # .I --- 01:145038 --:------ 01:145038 0009 003 0700FF
81 # .I --- 10:040239 01:223036 --:------ 0009 003 000000
82 # .I --- --:------ --:------ 12:227486 0009 003 0000FF
83 I_: r"^((0[0-9A-F]|F[9AC])0[0-1](00|FF))+$",
84 },
85 Code._000A: { # zone_params
86 SZ_NAME: "zone_params",
87 I_: r"^(0[0-9A-F][0-9A-F]{10}){1,8}$",
88 W_: r"^0[0-9A-F][0-9A-F]{10}$",
89 RQ: r"^0[0-9A-F]((00)?|([0-9A-F]{10})+)$", # is: r"^0[0-9A-F]([0-9A-F]{10})+$"
90 RP: r"^0[0-9A-F][0-9A-F]{10}$", # null_rp: xx/007FFF7FFF
91 # 17:54:13.126 063 RQ --- 34:064023 01:145038 --:------ 000A 001 03
92 # 17:54:13.141 045 RP --- 01:145038 34:064023 --:------ 000A 006 031002260B86
93 # 19:20:49.460 062 RQ --- 12:010740 01:145038 --:------ 000A 006 080001F40DAC
94 # 19:20:49.476 045 RP --- 01:145038 12:010740 --:------ 000A 006 081001F40DAC
95 SZ_LIFESPAN: td(days=1),
96 },
97 Code._000C: { # zone_devices
98 SZ_NAME: "zone_devices",
99 # RP --- 01:145038 18:013393 --:------ 000C 018 06-08-00-1099C3 06-08-00-1099C5 06-08-00-1099BF
100 # RP --- 01:145038 18:013393 --:------ 000C 016 05-08-00-109901 08-00-109902 08-00-109903
101 I_: r"^0[0-9A-F][01][0-9A-F]|7F[0-9A-F]{6}([0-9A-F]{10}|[0-9A-F]{12}){1,7}$",
102 RQ: r"^0[0-9A-F][01][0-9A-F]$", # TODO: f"{zone_idx}{device_type}"
103 SZ_LIFESPAN: False,
104 },
105 Code._000E: { # unknown_000e
106 SZ_NAME: "message_000e",
107 I_: r"^0000(14|28)$",
108 },
109 Code._0016: { # rf_check
110 SZ_NAME: "rf_check",
111 RQ: r"^0[0-9A-F]([0-9A-F]{2})?$", # TODO: officially: r"^0[0-9A-F]{3}$"
112 RP: r"^0[0-9A-F]{3}$",
113 },
114 Code._0100: { # language
115 SZ_NAME: "language",
116 RQ: r"^00([0-9A-F]{4}F{4})?$", # NOTE: RQ/04/0100 has a payload
117 RP: r"^00[0-9A-F]{4}F{4}$",
118 SZ_LIFESPAN: td(days=1), # TODO: make longer?
119 },
120 Code._0150: { # unknown_0150
121 SZ_NAME: "message_0150",
122 RQ: r"^00$",
123 RP: r"^000000$",
124 },
125 Code._01D0: { # unknown_01d0, TODO: definitely a real code, zone_idx is a guess
126 SZ_NAME: "message_01d0",
127 I_: r"^0[0-9A-F][0-9A-F]{2}$",
128 W_: r"^0[0-9A-F][0-9A-F]{2}$",
129 # .W --- 04:000722 01:158182 --:------ 01D0 002 0003 # is a guess, the
130 # .I --- 01:158182 04:000722 --:------ 01D0 002 0003 # TRV was in zone 00
131 },
132 Code._01E9: { # unknown_01e9, TODO: definitely a real code, zone_idx is a guess
133 SZ_NAME: "message_01e9",
134 I_: r"^0[0-9A-F][0-9A-F]{2}$",
135 W_: r"^0[0-9A-F][0-9A-F]{2}$",
136 # .W --- 04:000722 01:158182 --:------ 01E9 002 0003 # is a guess, the
137 # .I --- 01:158182 04:000722 --:------ 01E9 002 0000 # TRV was in zone 00
138 },
139 Code._01FF: { # unknown_01ff, TODO: definitely a real code, Itho Spider
140 SZ_NAME: "message_01ff",
141 I_: r"^(00|01)[0-9A-F]{50}$",
142 RQ: r"^(00|01)[0-9A-F]{50}$",
143 W_: r"^00[0-9A-F]{50}$",
144 },
145 Code._0404: { # zone_schedule
146 SZ_NAME: "zone_schedule",
147 I_: r"^0[0-9A-F](20|23)[0-9A-F]{2}08[0-9A-F]{6}$",
148 RQ: r"^0[0-9A-F](20|23)000800[0-9A-F]{4}$",
149 RP: r"^0[0-9A-F](20|23)0008[0-9A-F]{6}[0-9A-F]{2,82}$",
150 W_: r"^0[0-9A-F](20|23)[0-9A-F]{2}08[0-9A-F]{6}[0-9A-F]{2,82}$", # as per RP
151 SZ_LIFESPAN: None,
152 },
153 Code._0418: { # system_fault
154 SZ_NAME: "system_fault",
155 I_: r"^00(00|40|C0)[0-3][0-9A-F]B0[0-9A-F]{6}0000[0-9A-F]{12}FFFF700[012][0-9A-F]{6}$",
156 RQ: r"^0000[0-3][0-9A-F]$", # f"0000{log_idx}", no payload
157 },
158 Code._042F: { # unknown_042f, # non-evohome are len==9, seen only once?
159 # .I --- 32:168090 --:------ 32:168090 042F 009 000000100F00105050
160 # RP --- 10:048122 18:006402 --:------ 042F 009 000200001400163010
161 SZ_NAME: "message_042f",
162 I_: r"^00([0-9A-F]{2}){7,8}$",
163 RQ: r"^00$",
164 RP: r"^00([0-9A-F]{2}){7,8}$",
165 },
166 Code._0B04: { # unknown_0b04
167 # .I --- --:------ --:------ 12:207082 0B04 002 00C8
168 SZ_NAME: "message_0b04",
169 I_: r"^00(00|C8)$",
170 },
171 Code._1030: { # mixvalve_params
172 SZ_NAME: "mixvalve_params",
173 # .I --- --:------ --:------ 12:138834 1030 016 01C80137C9010FCA0196CB010FCC0101
174 I_: r"^0[0-9A-F](C[89A-C]01[0-9A-F]{2}){5}$",
175 RP: r"^00((20|21)01[0-9A-F]{2}){2}$", # rarely seen, HVAC
176 W_: r"^0[0-9A-F](C[89A-C]01[0-9A-F]{2}){5}$", # contrived
177 },
178 Code._1060: { # device_battery
179 SZ_NAME: "device_battery",
180 I_: r"^0[0-9A-F](FF|[0-9A-F]{2})0[01]$", # HCW: r"^(FF|0[0-9A-F]...
181 SZ_LIFESPAN: td(days=1),
182 },
183 Code._1081: { # max_ch_setpoint
184 SZ_NAME: "max_ch_setpoint",
185 RQ: r"^00$",
186 RP: r"^00[0-9A-F]{4}$",
187 },
188 Code._1090: { # unknown_1090
189 # 095 RP --- 23:100224 22:219457 --:------ 1090 005 00-7FFF-01F4
190 SZ_NAME: "message_1090",
191 RQ: r"^00$",
192 RP: r"^00",
193 },
194 Code._1098: { # unknown_1098
195 SZ_NAME: "message_1098",
196 RQ: r"^00$",
197 RP: r"^00",
198 },
199 Code._10A0: { # dhw_params
200 SZ_NAME: "dhw_params",
201 # RQ --- 07:045960 01:145038 --:------ 10A0 006 0013740003E4
202 # RP --- 10:048122 18:006402 --:------ 10A0 003 001B58
203 # NOTE: RFG100 uses a domain id! (00|01)
204 # 19:14:24.662 051 RQ --- 30:185469 01:037519 --:------ 10A0 001 00
205 # 19:14:31.463 053 RQ --- 30:185469 01:037519 --:------ 10A0 001 01
206 I_: r"^(00|01)[0-9A-F]{4}([0-9A-F]{6})?$", # NOTE: RQ/07/10A0 has a payload
207 RQ: r"^(00|01)([0-9A-F]{10})?$", # NOTE: RQ/07/10A0 has a payload
208 W_: r"^(00|01)[0-9A-F]{4}([0-9A-F]{6})?$", # TODO: needs checking
209 SZ_LIFESPAN: td(hours=4),
210 },
211 Code._10B0: { # unknown_10b0
212 SZ_NAME: "message_10b0",
213 RQ: r"^00$",
214 RP: r"^00[0-9A-F]{8}$",
215 },
216 Code._10D0: { # filter_change - polling interval should be 1/day
217 SZ_NAME: "filter_change",
218 I_: r"^00[0-9A-F]{6}(0000|FFFF)?$",
219 RQ: r"^00(00)?$",
220 W_: r"^00FF$",
221 },
222 Code._10E0: { # device_info
223 SZ_NAME: "device_info",
224 I_: r"^(00|FF)([0-9A-F]{30,})?$", # r"^[0-9A-F]{32,}$" might be OK
225 RQ: r"^00$", # NOTE: 63 seen (no RP), some devices will accept [0-9A-F]{2}
226 # RP: r"^[0-9A-F]{2}([0-9A-F]){30,}$", # NOTE: index same as RQ
227 SZ_LIFESPAN: False,
228 },
229 Code._10E1: { # device_id
230 SZ_NAME: "device_id",
231 RP: r"^00[0-9A-F]{6}$",
232 RQ: r"^00$",
233 SZ_LIFESPAN: False,
234 },
235 Code._10E2: { # unknown_10e2, HVAC?
236 SZ_NAME: "unknown_10e2",
237 I_: r"^00[0-9A-F]{4}$",
238 },
239 Code._1100: { # tpi_params
240 SZ_NAME: "tpi_params",
241 # I --- 01:172368 --:------ 01:172368 1100 008 FC180400007FFF00
242 # I --- 01:172368 13:040439 --:------ 1100 008 FC042814007FFF00
243 # RQ --- 01:145038 13:163733 --:------ 1100 008 00180400007FFF01 # boiler relay
244 # RP --- 13:163733 01:145038 --:------ 1100 008 00180400FF7FFF01
245 # RQ --- 01:145038 13:035462 --:------ 1100 008 FC240428007FFF01 # not bolier relay
246 # RP --- 13:035462 01:145038 --:------ 1100 008 00240428007FFF01
247 I_: r"^(00|FC)[0-9A-F]{6}(00|FF)([0-9A-F]{4}0[01])?$",
248 W_: r"^(00|FC)[0-9A-F]{6}(00|FF)([0-9A-F]{4}0[01])?$", # TODO: is there no I?
249 RQ: r"^(00|FC)([0-9A-F]{6}(00|FF)([0-9A-F]{4}0[01])?)?$", # RQ/13:/00, or RQ/01:/FC:
250 SZ_LIFESPAN: td(days=1),
251 },
252 Code._11F0: { # unknown_11f0, from heatpump relay
253 SZ_NAME: "message_11f0",
254 I_: r"^00",
255 },
256 Code._1260: { # dhw_temp
257 SZ_NAME: "dhw_temp",
258 # RQ --- 30:185469 01:037519 --:------ 1260 001 00
259 # RP --- 01:037519 30:185469 --:------ 1260 003 000837
260 # RQ --- 18:200202 10:067219 --:------ 1260 002 0000
261 # RP --- 10:067219 18:200202 --:------ 1260 003 007FFF
262 # .I --- 07:045960 --:------ 07:045960 1260 003 0007A9
263 I_: r"^(00|01)[0-9A-F]{4}$", # NOTE: RP is same
264 RQ: r"^(00|01)(00)?$", # TODO: officially: r"^(00|01)$"
265 SZ_LIFESPAN: td(hours=1),
266 },
267 Code._1280: { # outdoor_humidity
268 SZ_NAME: "outdoor_humidity",
269 I_: r"^00[0-9A-F]{2}[0-9A-F]{8}?$",
270 },
271 Code._1290: { # outdoor_temp
272 SZ_NAME: "outdoor_temp",
273 I_: r"^00[0-9A-F]{4}$", # NOTE: RP is same
274 RQ: r"^00$",
275 },
276 Code._1298: { # co2_level
277 SZ_NAME: "co2_level",
278 I_: r"^00[0-9A-F]{4}$", # NOTE: RP is same
279 RQ: r"^00$",
280 },
281 Code._12A0: { # indoor_humidity
282 SZ_NAME: "indoor_humidity",
283 I_: r"^(0[0-9A-F]{3}([0-9A-F]{8}(00)?)?)+$",
284 RP: r"^0[0-9A-F]{3}([0-9A-F]{8}(00)?)?$",
285 SZ_LIFESPAN: td(hours=1),
286 },
287 Code._12B0: { # window_state (HVAC % window open)
288 SZ_NAME: "window_state",
289 I_: r"^0[0-9A-F](0000|C800|FFFF)$", # NOTE: RP is same
290 RQ: r"^0[0-9A-F](00)?$",
291 SZ_LIFESPAN: td(hours=1),
292 },
293 Code._12C0: { # displayed_temp (HVAC room temp)
294 SZ_NAME: "displayed_temp", # displayed room temp
295 I_: r"^00[0-9A-F]{2}0[01](FF)?$",
296 },
297 Code._12C8: { # air_quality, HVAC
298 SZ_NAME: "air_quality",
299 I_: r"^00[0-9A-F]{4}$",
300 },
301 Code._12F0: { # dhw_flow_rate
302 # 2021-11-05T06:25:20.399400 065 RP --- 10:023327 18:131597 --:------ 12F0 003 000307
303 # 2021-11-05T06:25:20.669382 066 RP --- 10:023327 18:131597 --:------ 3220 005 00C01307C0
304 # 2021-11-05T06:35:20.450201 065 RP --- 10:023327 18:131597 --:------ 12F0 003 000023
305 # 2021-11-05T06:35:20.721228 066 RP --- 10:023327 18:131597 --:------ 3220 005 0040130059
306 # 2021-12-06T06:35:54.575298 073 RP --- 10:051349 18:135447 --:------ 12F0 003 00059F
307 # 2021-12-06T06:35:55.949502 071 RP --- 10:051349 18:135447 --:------ 3220 005 00C0130ECC
308 SZ_NAME: "dhw_flow_rate",
309 RQ: r"^00$",
310 RP: r"^00[0-9A-F]{4}$",
311 },
312 Code._1300: { # cv water pressure (usu. for ch)
313 SZ_NAME: "ch_pressure",
314 RQ: r"^00$",
315 RP: r"^00[0-9A-F]{4}$",
316 },
317 Code._1470: { # programme_scheme, HVAC (1470, 1F70, 22B0)
318 SZ_NAME: "programme_scheme",
319 RQ: r"^00$",
320 I_: r"^00[0-9A-F]{14}$",
321 W_: r"^00[0-9A-F]{2}0{4}800{6}$",
322 },
323 Code._1F09: { # system_sync - FF (I), 00 (RP), F8 (W, after 1FC9)
324 SZ_NAME: "system_sync",
325 I_: r"^(00|01|DB|FF)[0-9A-F]{4}$", # FF is evohome, DB is Hometronics
326 RQ: r"^00$",
327 RP: r"^00[0-9A-F]{4}$", # xx-secs
328 W_: r"^F8[0-9A-F]{4}$",
329 },
330 Code._1F41: { # dhw_mode
331 SZ_NAME: "dhw_mode",
332 I_: r"^(00|01)(00|01|FF)0[0-5]F{6}(([0-9A-F]){12})?$",
333 RQ: r"^(00|01)$", # will accept: r"^(00|01)(00)$"
334 W_: r"^(00|01)(00|01|FF)0[0-5]F{6}(([0-9A-F]){12})?$",
335 SZ_LIFESPAN: td(hours=4),
336 },
337 Code._1F70: { # programme_config, HVAC (1470, 1F70, 22B0)
338 SZ_NAME: "programme_config",
339 I_: r"^00[0-9A-F]{30}$",
340 RQ: r"^00[0-9A-F]{30}$",
341 W_: r"^00[0-9A-F]{30}$",
342 },
343 Code._1FC9: { # rf_bind
344 SZ_NAME: "rf_bind", # idx-code-dev_id
345 RQ: r"^00$",
346 RP: r"^((0[0-9A-F]|F[69ABCF]|[0-9A-F]{2})([0-9A-F]{10}))+$",
347 I_: r"^((0[0-9A-F]|F[69ABCF]|[0-9A-F]{2})([0-9A-F]{10}))+|00|21$", # NOTE: payload can be 00
348 W_: r"^((0[0-9A-F]|F[69ABCF]|[0-9A-F]{2})([0-9A-F]{10}))+$",
349 },
350 Code._1FCA: { # unknown_1fca
351 SZ_NAME: "message_1fca",
352 RQ: r"^00$",
353 RP: r"^((0[0-9A-F]|F[9ABCF]|90)([0-9A-F]{10}))+$", # xx-code-dev_id
354 I_: r"^((0[0-9A-F]|F[9ABCF])([0-9A-F]{10}))+$",
355 W_: r"^((0[0-9A-F]|F[9ABCF])([0-9A-F]{10}))+$",
356 },
357 Code._1FD0: { # unknown_1fd0
358 SZ_NAME: "message_1fd0",
359 RQ: r"^00$",
360 RP: r"^00",
361 },
362 Code._1FD4: { # opentherm_sync
363 SZ_NAME: "opentherm_sync",
364 I_: r"^00([0-9A-F]{4})$",
365 },
366 Code._2210: { # unknown_2210, HVAC,
367 SZ_NAME: "unknown_2210",
368 I_: r"^00[0-9A-F]{82}$",
369 RQ: r"^00$",
370 },
371 Code._2249: { # setpoint_now?
372 SZ_NAME: "setpoint_now", # setpt_now_next
373 I_: r"^(0[0-9A-F]{13}){1,2}$",
374 }, # TODO: This could be an array
375 Code._22C9: { # setpoint_bounds (was: ufh_setpoint)
376 SZ_NAME: "setpoint_bounds",
377 I_: r"^(0[0-9A-F][0-9A-F]{8}0[12]){1,4}(0[12]03)?$", # (0[12]03)? only if len(array) == 1
378 W_: r"^(0[0-9A-F][0-9A-F]{8}0[12])$", # never an array
379 },
380 Code._22D0: { # unknown_22d0, Spider thermostat, HVAC system switch?
381 SZ_NAME: "message_22d0",
382 I_: r"^(00|03)[0-9]{6}$",
383 W_: r"^03[0-9]{4}1E14030020$",
384 },
385 Code._22D9: { # boiler_setpoint
386 SZ_NAME: "boiler_setpoint",
387 RQ: r"^00$",
388 RP: r"^00[0-9A-F]{4}$",
389 },
390 Code._22E0: { # unknown_22e0, HVAC, NB: no I
391 SZ_NAME: "unknown_22e0",
392 RQ: r"^00$",
393 RP: r"^00[0-9A-F]{6}$",
394 },
395 Code._22E5: { # unknown_22e5, HVAC, NB: no I
396 SZ_NAME: "unknown_22e5",
397 RQ: r"^00$",
398 RP: r"^00[0-9A-F]{6}$",
399 },
400 Code._22E9: { # unknown_22e9, HVAC, NB: no I
401 SZ_NAME: "unknown_22e9",
402 RQ: r"^00$",
403 RP: r"^00[0-9A-F]{6}$",
404 },
405 Code._22F1: { # fan_mode, HVAC
406 SZ_NAME: "fan_mode",
407 RQ: r"^00$",
408 RP: r"^00[0-9A-F]{4}$",
409 I_: r"^(00|63)(0[0-9A-F]){1,2}$",
410 },
411 Code._22F2: { # unknown_22f2, HVAC, NB: no I
412 SZ_NAME: "unknown_22f2",
413 RQ: r"^00$", # (00|01)?
414 RP: r"^00[0-9A-F]{4}(01[0-9A-F]{4})?$",
415 },
416 Code._22F3: { # fan_boost, HVAC
417 SZ_NAME: "fan_boost",
418 I_: r"^(00|63)(021E)?[0-9A-F]{4}([0-9A-F]{8})?$",
419 }, # minutes only?
420 Code._22F4: { # unknown_22f4, HVAC
421 SZ_NAME: "unknown_22f4",
422 I_: r"^00[0-9A-F]{24}$",
423 RQ: r"^00$",
424 },
425 Code._22F7: { # fan_bypass_mode (% open), HVAC
426 SZ_NAME: "fan_bypass_mode",
427 I_: r"^00([0-9A-F]{2}){1,2}$", # RP is the same
428 RQ: r"^00$",
429 W_: r"^00[0-9A-F]{2}(EF)?$",
430 },
431 Code._22F8: { # fan_22f8 (moisture scenario?), HVAC
432 SZ_NAME: "fan_22f8",
433 RQ: r"^00$",
434 I_: r"^00[0-9A-F]{4}$",
435 },
436 Code._22B0: { # programme_status, HVAC (1470, 1F70, 22B0)
437 SZ_NAME: "programme_status",
438 W_: r"^00[0-9A-F]{2}$",
439 I_: r"^00[0-9A-F]{2}$",
440 },
441 Code._2309: { # setpoint
442 SZ_NAME: "setpoint",
443 I_: r"^(0[0-9A-F]{5})+$",
444 W_: r"^0[0-9A-F]{5}$",
445 # RQ --- 12:010740 01:145038 --:------ 2309 003 03073A # No RPs
446 RQ: r"^0[0-9A-F]([0-9A-F]{4})?$", # NOTE: 12 uses: r"^0[0-9A-F]$"
447 SZ_LIFESPAN: td(minutes=30),
448 },
449 Code._2349: { # zone_mode
450 SZ_NAME: "zone_mode",
451 I_: r"^0[0-9A-F]{5}0[0-4][0-9A-F]{6}([0-9A-F]{12})?$",
452 W_: r"^0[0-9A-F]{5}0[0-4][0-9A-F]{6}([0-9A-F]{12})?$",
453 # .W --- 18:141846 01:050858 --:------ 2349 013 02-0960-04-FFFFFF-0409160607E5
454 # .W --- 18:141846 01:050858 --:------ 2349 007 02-08FC-01-FFFFFF
455 RQ: r"^0[0-9A-F](00|[0-9A-F]{12})?$",
456 # RQ --- 22:070483 01:063844 --:------ 2349 007 06-0708-03-000027
457 SZ_LIFESPAN: td(hours=4),
458 },
459 Code._2389: { # unknown_2389 - CODE_IDX_COMPLEX?
460 # .I 024 03:052382 --:------ 03:052382 2389 003 02001B
461 SZ_NAME: "unknown_2389",
462 I_: r"^0[0-4][0-9A-F]{4}$",
463 },
464 Code._2400: { # unknown_2400, from OTB
465 SZ_NAME: "message_2400",
466 RQ: r"^00$",
467 RP: r"^00",
468 },
469 Code._2401: { # unknown_2401, from OTB
470 SZ_NAME: "message_2401",
471 RQ: r"^00$",
472 RP: r"^00",
473 },
474 Code._2410: { # unknown_2410, from OTB
475 SZ_NAME: "message_2410",
476 RQ: r"^00$",
477 RP: r"^00",
478 },
479 Code._2411: { # fan_params, HVAC
480 SZ_NAME: "fan_params",
481 I_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}([0-9A-F]{8}){4}[0-9A-F]{2,6}$", # Allow 2-6 byte footer
482 RQ: r"^(00|01|15|16|17|21)00[0-9A-F]{2}((00){19})?$",
483 RP: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}([0-9A-F]{8}){3}[0-9A-F]{2,6}$", # 4 blocks + footer
484 W_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}([0-9A-F]{8}){3}[0-9A-F]{2,6}$", # Same as RP
485 },
486 Code._2420: { # unknown_2420, from OTB
487 SZ_NAME: "message_2420",
488 RQ: r"^00$",
489 RP: r"^00",
490 },
491 Code._2D49: { # unknown_2d49
492 SZ_NAME: "message_2d49",
493 # 10:14:08.526 045 I --- 01:023389 --:------ 01:023389 2D49 003 010000
494 # 10:14:12.253 047 I --- 01:023389 --:------ 01:023389 2D49 003 00C800
495 # 10:14:12.272 047 I --- 01:023389 --:------ 01:023389 2D49 003 01C800
496 # 10:14:12.390 049 I --- 01:023389 --:------ 01:023389 2D49 003 880000
497 # 10:14:12.399 048 I --- 01:023389 --:------ 01:023389 2D49 003 FD0000
498 I_: r"^(0[0-9A-F]|88|F6|FD)[0-9A-F]{2}(00||FF)$",
499 }, # seen with Hometronic systems
500 Code._2E04: { # system_mode
501 SZ_NAME: "system_mode",
502 I_: r"^0[0-7][0-9A-F]{12}0[01]$",
503 RQ: r"^FF$",
504 W_: r"^0[0-7][0-9A-F]{12}0[01]$",
505 SZ_LIFESPAN: td(hours=4),
506 },
507 Code._2E10: { # presence_detect - HVAC
508 SZ_NAME: "presence_detect",
509 I_: r"^00(00|01)(00)?$",
510 },
511 Code._30C9: { # temperature
512 SZ_NAME: "temperature",
513 I_: r"^(0[0-9A-F][0-9A-F]{4})+$",
514 RQ: r"^0[0-9A-F](00)?$", # TODO: officially: r"^0[0-9A-F]$"
515 RP: r"^0[0-9A-F][0-9A-F]{4}$", # Null: r"^0[0-9A-F]7FFF$"
516 SZ_LIFESPAN: td(hours=1),
517 },
518 Code._3110: { # ufc_demand - HVAC
519 SZ_NAME: "ufc_demand",
520 I_: r"^(00|01)00[0-9A-F]{2}(00|10|20)", # (00|10|20|FF)???
521 },
522 Code._3120: { # unknown_3120 - Error Report?
523 SZ_NAME: "message_3120",
524 I_: r"^00[0-9A-F]{10}FF$", # only ever: 34:/0070B0000000FF
525 RQ: r"^00$", # 20: will RP an RQ?
526 # RP: r"^00[0-9A-F]{10}FF$", # only ever: 20:/0070B000009CFF
527 },
528 Code._313E: { # unknown_313e, HVAC, NB: no I
529 SZ_NAME: "unknown_313e",
530 RQ: r"^00$",
531 RP: r"^00[0-9A-F]{20}$",
532 },
533 Code._313F: { # datetime (time report)
534 SZ_NAME: "datetime",
535 I_: r"^00[0-9A-F]{16}$", # NOTE: RP is same
536 RQ: r"^00$",
537 W_: r"^00[0-9A-F]{16}$",
538 SZ_LIFESPAN: td(seconds=3),
539 },
540 Code._3150: { # heat_demand, also fans with preheat
541 SZ_NAME: "heat_demand",
542 I_: r"^((0[0-9A-F])[0-9A-F]{2}|FC[0-9A-F]{2})+$",
543 SZ_LIFESPAN: td(minutes=20),
544 },
545 Code._31D9: { # fan_state
546 SZ_NAME: "fan_state",
547 # I_: r"^(00|21)[0-9A-F]{32}$",
548 # I_: r"^(00|01|21)[0-9A-F]{4}((00|FE)(00|20){12}(00|08))?$",
549 I_: r"^(00|01|15|16|17|21)[0-9A-F]{4}(([0-9A-F]{2})(00|20){0,12}(00|01|04|08)?)?$", # 00-0004-FE
550 RQ: r"^(00|01|15|16|17|21)$",
551 },
552 Code._31DA: { # hvac_state (fan_state_extended)
553 SZ_NAME: "hvac_state",
554 I_: r"^(00|01|15|16|17|21)[0-9A-F]{56}(00|20)?$",
555 RQ: r"^(00|01|15|16|17|21)$",
556 # RQ --- 32:168090 30:082155 --:------ 31DA 001 21
557 },
558 Code._31E0: { # fan_demand
559 # 10:15:42.712 077 I --- 29:146052 32:023459 --:------ 31E0 003 0000C8
560 # 10:21:18.549 078 I --- 29:146052 32:023459 --:------ 31E0 003 000000
561 # 07:56:50.522 095 I --- --:------ --:------ 07:044315 31E0 004 00006E00
562 SZ_NAME: "fan_demand",
563 I_: r"^00([0-9A-F]{4}){1,3}(00|FF)?$",
564 },
565 Code._3200: { # boiler (or CV?) output temp
566 SZ_NAME: "boiler_output",
567 I_: r"^00[0-9A-F]{4}$",
568 RQ: r"^00$",
569 },
570 Code._3210: { # boiler (or CV?) return temp
571 SZ_NAME: "boiler_return",
572 RQ: r"^00$",
573 RP: r"^00[0-9A-F]{4}$",
574 },
575 Code._3220: { # opentherm_msg
576 SZ_NAME: "opentherm_msg",
577 RQ: r"^00[0-9A-F]{8}$",
578 RP: r"^00[0-9A-F]{8}$",
579 },
580 Code._3221: { # unknown_3221, from OTB
581 SZ_NAME: "message_3221",
582 RQ: r"^00$",
583 RP: r"^00",
584 },
585 Code._3222: { # unknown_3222, HVAC, NB: no I
586 SZ_NAME: "unknown_3222",
587 RQ: r"^00$",
588 RP: r"^00[0-9A-F]{4,24}$",
589 },
590 Code._3223: { # unknown_3223, from OTB
591 SZ_NAME: "message_3223",
592 RQ: r"^00$",
593 RP: r"^00",
594 },
595 Code._3B00: { # actuator_sync, NOTE: no RQ
596 SZ_NAME: "actuator_sync",
597 I_: r"^(00|FC)(00|C8)$",
598 },
599 Code._3EF0: { # actuator_state
600 SZ_NAME: "actuator_state",
601 # .I --- 13:106039 --:------ 13:106039 3EF0 003 00-C8FF
602 # .I --- 21:038634 --:------ 21:038634 3EF0 006 00-0000-0A0200 # # Itho spIDer
603 # .I --- 10:030051 --:------ 10:030051 3EF0 009 00-0010-000000-020A64
604 # .I --- 08:031043 31:077159 --:------ 3EF0 020 00-1191A72044399D2A50DE43F920478AF7185F3F # # Jasper BLOB
605 I_: r"^..((00|C8)FF|[0-9A-F]{10}|[0-9A-F]{16}|[0-9A-F]{38})$",
606 RQ: r"^00(00)?$",
607 RP: r"^00((00|C8)FF|[0-9A-F]{10}|[0-9A-F]{16})$",
608 },
609 Code._3EF1: { # actuator_cycle
610 SZ_NAME: "actuator_cycle",
611 # RQ --- 31:004811 13:077615 --:------ 3EF1 001 00
612 # RP --- 13:077615 31:004811 --:------ 3EF1 007 00024D001300FF
613 # RQ --- 22:068154 13:031208 --:------ 3EF1 002 0000
614 # RP --- 13:031208 22:068154 --:------ 3EF1 007 00024E00E000FF
615 # RQ --- 31:074182 08:026984 --:------ 3EF1 012 0005D1341DA39B8C7DAFD4C1
616 # RP --- 08:026984 31:074182 --:------ 3EF1 018 001396A7E087922FA77794280B66BE16A975
617 RQ: r"^00((00)?|[0-9A-F]{22})$", # NOTE: latter is Japser
618 RP: r"^00([0-9A-F]{12}|[0-9A-F]{34})$", # NOTE: latter is Japser
619 },
620 Code._4401: { # unknown_4401 - HVAC
621 SZ_NAME: "unknown_4401",
622 I_: r"^[0-9A-F]{40}$",
623 RP: r"^00$",
624 RQ: r"^[0-9A-F]{40}$",
625 W_: r"^[0-9A-F]{40}$",
626 },
627 Code._4E01: { # xxx (HVAC) - Itho Spider
628 SZ_NAME: "hvac_4e01",
629 I_: r"^00([0-9A-F]{4}){3,12}00$",
630 },
631 Code._4E02: { # xxx (HVAC) - Itho Spider
632 SZ_NAME: "hvac_4e02",
633 I_: r"^00([0-9A-F]{4}){3,12}(02|03|04|05)([0-9A-F]{4}){3,12}$",
634 },
635 Code._4E04: { # xxx (HVAC) - Itho Spider
636 SZ_NAME: "hvac_4e04",
637 I_: r"^00(00|01|02)[0-9A-F]{2}$",
638 W_: r"^00(00|01|02)[0-9A-F]{2}$",
639 },
640 Code._4E0D: { # xxx (HVAC) - Itho Spider
641 SZ_NAME: "hvac_4e0d",
642 I_: r"^(01|02)(00|01)$",
643 },
644 Code._4E15: { # xxx (HVAC) - Itho Spider
645 SZ_NAME: "hvac_4e15",
646 I_: r"^000[0-7]$",
647 },
648 Code._4E16: { # xxx (HVAC) - Itho Spider
649 SZ_NAME: "hvac_4e16",
650 I_: r"^00(00){6}$",
651 },
652 Code._PUZZ: {
653 SZ_NAME: "puzzle_packet",
654 I_: r"^00(([0-9A-F]){2})+$",
655 },
656}
657CODE_NAME_LOOKUP = {k: v["name"] for k, v in CODES_SCHEMA.items()}
660for code in CODES_SCHEMA.values(): # map any (missing) RPs to I_s
661 if RQ in code and RP not in code and I_ in code:
662 code[RP] = code[I_]
663#
664# .I --- 01:210309 --:------ 01:210309 0009 006 FC00FFF900FF
665CODES_WITH_ARRAYS: dict[Code, list[int | tuple[str, ...]]] = { # 000C/1FC9 are special
666 Code._0005: [4, ("34",)],
667 Code._0009: [3, ("01", "12", "22")],
668 Code._000A: [6, ("01", "12", "22")], # single element I after a W
669 Code._2309: [3, ("01", "12", "22")],
670 Code._30C9: [3, ("01", "12", "22")],
671 Code._2249: [7, ("23",)],
672 Code._22C9: [6, ("02",)],
673 Code._3150: [2, ("02",)],
674} # TODO dex: element_length, src.type(s) (and dst.type too)
675#
676RQ_IDX_COMPLEX: list[Code] = [
677 Code._0005, # context: zone_type
678 Code._000A, # optional payload
679 Code._000C, # context: index, zone_type
680 Code._0016, # optional payload
681 Code._0100, # optional payload
682 Code._0404, # context: index, fragment_idx (fragment_header)
683 Code._0418, # context: index
684 Code._10A0, # optional payload
685 Code._1100, # optional payload
686 Code._2309, # optional payload
687 Code._2349, # optional payload
688 Code._3220, # context: msg_id, and payload
689]
690RQ_NO_PAYLOAD: list[Code] = [
691 k
692 for k, v in CODES_SCHEMA.items()
693 if v.get(RQ)
694 in (r"^FF$", r"^00$", r"^00(00)?$", r"^0[0-9A-F](00)?$", r"^0[0-9A-F]00$")
695]
696RQ_NO_PAYLOAD.extend((Code._0418,))
699########################################################################################
700# IDX:_xxxxxx: index (and context)
702# all known codes should be in only one of IDX_COMPLEX, IDX_NONE, IDX_SIMPLE
704# IDX_COMPLEX - *usually has* a context, but doesn't satisfy criteria for IDX_SIMPLE:
705CODE_IDX_ARE_COMPLEX: set[Code] = {
706 Code._0005,
707 Code._000C, # idx = fx(payload[0:4])
708 # Code._0404, # use "HW" for idx if payload[4:6] == "23" # TODO: should be used
709 Code._0418, # log_idx (payload[4:6]) # null RPs are missing an idx
710 Code._1100,
711 Code._3220, # data_id (payload[4:6])
712} # TODO: 0005 to ..._NONE?
714# IDX_SIMPLE - *can have* a context, but sometimes not (usu. 00): only ever payload[:2],
715# either a zone_idx, domain_id or (UFC) circuit_idx (or array of such, i.e. seqx[:2])
717_SIMPLE_IDX = ("^0[0-9A-F]", "^(0[0-9A-F]", "^((0[0-9A-F]", "^(00|01)")
718CODE_IDX_ARE_SIMPLE: set[Code] = {
719 k
720 for k, v in CODES_SCHEMA.items()
721 for verb in (RQ, I_)
722 if k not in CODE_IDX_ARE_COMPLEX and v.get(verb, "").startswith(_SIMPLE_IDX)
723}
724CODE_IDX_ARE_SIMPLE |= {
725 Code._22D0,
726 Code._2411,
727 Code._31D9,
728 Code._31DA,
729 Code._3B00,
730 Code._4E0D,
731}
733# IDX_NONE - *never has* a context: most payloads start 00, but no context even if the
734# payload starts with something else (e.g. 2E04)
735CODE_IDX_ARE_NONE: set[Code] = {
736 k
737 for k, v in CODES_SCHEMA.items()
738 if k not in CODE_IDX_ARE_COMPLEX | CODE_IDX_ARE_SIMPLE
739 and ((RQ in v and v[RQ][:3] == "^00") or (I_ in v and v[I_][:3] == "^00"))
740}
741CODE_IDX_ARE_NONE |= {Code._22F3, Code._2389, Code._2E04, Code._4401}
743# CODE_IDX_DOMAIN - NOTE: not necc. mutex with other 3
744CODE_IDX_DOMAIN: dict[Code, str] = {
745 Code._0001: "^F[ACF])",
746 Code._0008: "^F[9AC]",
747 Code._0009: "^F[9AC]",
748 Code._1100: "^FC",
749 Code._1FC9: "^F[9ABCF]",
750 Code._3150: "^FC",
751 Code._3B00: "^FC",
752}
755#
756########################################################################################
757# CODES_BY_DEV_SLUG - HEAT (CH/DHW) vs HVAC (ventilation)
758# TODO: 34: can 3220 - split out RND from THM/STA
759_DEV_KLASSES_HEAT: dict[str, dict[Code, dict[VerbT, Any]]] = {
760 DevType.RFG: { # RFG100: RF to Internet gateway (and others)
761 Code._0002: {RQ: {}},
762 Code._0004: {I_: {}, RQ: {}},
763 Code._0005: {RQ: {}},
764 Code._0006: {RQ: {}},
765 Code._000A: {RQ: {}},
766 Code._000C: {RQ: {}},
767 Code._000E: {W_: {}},
768 Code._0016: {RP: {}},
769 Code._0404: {RQ: {}, W_: {}},
770 Code._0418: {RQ: {}},
771 Code._10A0: {RQ: {}},
772 Code._10E0: {I_: {}, RQ: {}, RP: {}},
773 Code._1260: {RQ: {}},
774 Code._1290: {I_: {}},
775 Code._1F41: {RQ: {}},
776 Code._1FC9: {RP: {}, W_: {}},
777 Code._22D9: {RQ: {}},
778 Code._2309: {I_: {}},
779 Code._2349: {RQ: {}, RP: {}, W_: {}},
780 Code._2E04: {RQ: {}, I_: {}, W_: {}},
781 Code._30C9: {RQ: {}},
782 Code._313F: {RQ: {}, RP: {}, W_: {}},
783 Code._3220: {RQ: {}},
784 Code._3EF0: {RQ: {}},
785 },
786 DevType.CTL: { # e.g. ATC928: Evohome Colour Controller
787 Code._0001: {W_: {}},
788 Code._0002: {I_: {}, RP: {}},
789 Code._0004: {I_: {}, RP: {}},
790 Code._0005: {I_: {}, RP: {}},
791 Code._0006: {RP: {}},
792 Code._0008: {I_: {}},
793 Code._0009: {I_: {}},
794 Code._000A: {I_: {}, RP: {}},
795 Code._000C: {RP: {}},
796 Code._0016: {RQ: {}, RP: {}},
797 Code._0100: {RP: {}},
798 Code._01D0: {I_: {}},
799 Code._01E9: {I_: {}},
800 Code._0404: {I_: {}, RP: {}},
801 Code._0418: {I_: {}, RP: {}},
802 Code._1030: {I_: {}},
803 Code._10A0: {I_: {}, RP: {}},
804 Code._10E0: {RP: {}},
805 Code._1100: {I_: {}, RQ: {}, RP: {}, W_: {}},
806 Code._1260: {RP: {}},
807 Code._1290: {RP: {}},
808 Code._12B0: {I_: {}, RP: {}},
809 Code._1F09: {I_: {}, RP: {}, W_: {}},
810 Code._1FC9: {I_: {}, RQ: {}, RP: {}, W_: {}},
811 Code._1F41: {I_: {}, RP: {}},
812 Code._2249: {I_: {}}, # Hometronics, not Evohome
813 Code._22D9: {RQ: {}},
814 Code._2309: {I_: {}, RP: {}},
815 Code._2349: {I_: {}, RP: {}},
816 Code._2D49: {I_: {}},
817 Code._2E04: {I_: {}, RP: {}},
818 Code._30C9: {I_: {}, RP: {}},
819 Code._313F: {I_: {}, RP: {}, W_: {}},
820 Code._3150: {I_: {}},
821 Code._3220: {RQ: {}},
822 Code._3B00: {I_: {}},
823 Code._3EF0: {RQ: {}},
824 },
825 DevType.PRG: { # e.g. HCF82/HCW82: Room Temperature Sensor
826 Code._0009: {I_: {}},
827 Code._1090: {RP: {}},
828 Code._10A0: {RP: {}},
829 Code._1100: {I_: {}},
830 Code._1F09: {I_: {}},
831 Code._2249: {I_: {}},
832 Code._2309: {I_: {}},
833 Code._30C9: {I_: {}},
834 Code._3B00: {I_: {}},
835 Code._3EF1: {RP: {}},
836 },
837 DevType.THM: { # e.g. Generic Thermostat
838 Code._0001: {W_: {}},
839 Code._0005: {I_: {}},
840 Code._0008: {I_: {}},
841 Code._0009: {I_: {}},
842 Code._000A: {I_: {}, RQ: {}, W_: {}},
843 Code._000C: {I_: {}},
844 Code._000E: {I_: {}},
845 Code._0016: {RQ: {}},
846 Code._01FF: {I_: {}, RQ: {}},
847 Code._042F: {I_: {}},
848 Code._1030: {I_: {}},
849 Code._1060: {I_: {}},
850 Code._1090: {RQ: {}},
851 Code._10E0: {I_: {}},
852 Code._1100: {I_: {}},
853 Code._12C0: {I_: {}},
854 Code._1F09: {I_: {}},
855 Code._1FC9: {I_: {}},
856 Code._22C9: {W_: {}}, # DT4R
857 Code._22D0: {W_: {}}, # Spider master THM
858 Code._2309: {I_: {}, RQ: {}, W_: {}},
859 Code._2349: {RQ: {}, W_: {}},
860 Code._30C9: {I_: {}},
861 Code._3110: {I_: {}}, # Spider THM
862 Code._3120: {I_: {}},
863 Code._313F: {
864 I_: {}
865 }, # .W --- 30:253184 34:010943 --:------ 313F 009 006000070E0...
866 Code._3220: {RP: {}}, # RND (using OT)
867 Code._3B00: {I_: {}},
868 Code._3EF0: {RQ: {}}, # when bound direct to a 13:
869 Code._3EF1: {RQ: {}}, # when bound direct to a 13:
870 },
871 DevType.UFC: { # e.g. HCE80/HCC80: Underfloor Heating Controller
872 Code._0001: {RP: {}, W_: {}}, # TODO: Ix RP
873 Code._0005: {RP: {}},
874 Code._0008: {I_: {}},
875 Code._000A: {RP: {}},
876 Code._000C: {RP: {}},
877 Code._1FC9: {I_: {}},
878 Code._1FD4: {I_: {}}, # Spider Autotemp, slave 'ticker'
879 Code._10E0: {I_: {}, RP: {}},
880 Code._22C9: {I_: {}}, # NOTE: No RP
881 Code._22D0: {I_: {}, RP: {}},
882 Code._2309: {RP: {}},
883 Code._3110: {I_: {}}, # Spider Autotemp
884 Code._3150: {I_: {}},
885 Code._4E01: {I_: {}}, # Spider Autotemp Zone controller
886 Code._4E04: {I_: {}}, # idem
887 },
888 DevType.TRV: { # e.g. HR92/HR91: Radiator Controller
889 Code._0001: {W_: {r"^0[0-9A-F]"}},
890 Code._0004: {RQ: {r"^0[0-9A-F]00$"}},
891 Code._0016: {RQ: {}, RP: {}},
892 Code._0100: {RQ: {r"^00"}},
893 Code._01D0: {W_: {}},
894 Code._01E9: {W_: {}},
895 Code._1060: {I_: {r"^0[0-9A-F]{3}0[01]$"}},
896 Code._10E0: {I_: {r"^00[0-9A-F]{30,}$"}},
897 Code._12B0: {I_: {r"^0[0-9A-F]{3}00$"}}, # sends every 1h
898 Code._1F09: {RQ: {r"^00$"}},
899 Code._1FC9: {I_: {}, W_: {}},
900 Code._2309: {I_: {r"^0[0-9A-F]{5}$"}},
901 Code._30C9: {I_: {r"^0[0-9A-F]"}},
902 Code._313F: {RQ: {r"^00$"}},
903 Code._3150: {I_: {r"^0[0-9A-F]{3}$"}},
904 },
905 DevType.DHW: { # e.g. CS92: (DHW) Cylinder Thermostat
906 Code._0016: {RQ: {}},
907 Code._1060: {I_: {}},
908 Code._10A0: {RQ: {}}, # This RQ/07/10A0 includes a payload
909 Code._1260: {I_: {}},
910 Code._1FC9: {I_: {}},
911 },
912 DevType.OTB: { # e.g. R8810/R8820: OpenTherm Bridge
913 Code._0009: {I_: {}}, # 1/24h for a R8820 (not an R8810)
914 Code._0150: {RP: {}}, # R8820A only?
915 Code._042F: {I_: {}, RP: {}},
916 Code._1081: {RP: {}}, # R8820A only?
917 Code._1098: {RP: {}}, # R8820A only?
918 Code._10A0: {RP: {}},
919 Code._10B0: {RP: {}}, # R8820A only?
920 Code._10E0: {I_: {}, RP: {}},
921 Code._10E1: {RP: {}}, # R8820A only?
922 Code._1260: {RP: {}},
923 Code._1290: {RP: {}},
924 Code._12F0: {RP: {}}, # R8820A only?
925 Code._1300: {RP: {}}, # R8820A only?
926 Code._1FC9: {I_: {}, W_: {}},
927 Code._1FD0: {RP: {}}, # R8820A only?
928 Code._1FD4: {I_: {}}, # 2/min for R8810, every ~210 sec for R8820
929 Code._22D9: {RP: {}},
930 Code._2400: {RP: {}}, # R8820A only?
931 Code._2401: {RP: {}}, # R8820A only?
932 Code._2410: {RP: {}}, # R8820A only?
933 Code._2420: {RP: {}}, # R8820A only?
934 Code._3150: {I_: {}},
935 Code._3200: {RP: {}}, # R8820A only?
936 Code._3210: {RP: {}}, # R8820A only?
937 Code._3220: {RP: {}},
938 Code._3221: {RP: {}}, # R8820A only?
939 Code._3223: {RP: {}}, # R8820A only?
940 Code._3EF0: {I_: {}, RP: {}},
941 Code._3EF1: {RP: {}},
942 }, # see: https://www.opentherm.eu/request-details/?post_ids=2944
943 DevType.BDR: { # e.g. BDR91A/BDR91T: Wireless Relay Box
944 Code._0008: {RP: {}}, # doesn't RP/0009
945 Code._0016: {RP: {}},
946 # Code._10E0: {}, # 13: will not RP/10E0 # TODO: how to indicate that fact here
947 Code._1100: {I_: {}, RP: {}},
948 Code._11F0: {I_: {}}, # BDR91T in heatpump mode
949 Code._1FC9: {RP: {}, W_: {}},
950 Code._2D49: {I_: {}}, # BDR91T in heatpump mode
951 Code._3B00: {I_: {}},
952 Code._3EF0: {I_: {}},
953 # RP: {}, # RQ --- 01:145038 13:237335 --:------ 3EF0 001 00
954 Code._3EF1: {RP: {}},
955 },
956 DevType.OUT: {
957 Code._0002: {I_: {}},
958 Code._1FC9: {I_: {}},
959 }, # i.e. HB85 (ext. temperature/luminosity(lux)), HB95 (+ wind speed)
960 #
961 DevType.JIM: { # Jasper Interface Module, 08
962 Code._0008: {RQ: {}},
963 Code._10E0: {I_: {}},
964 Code._1100: {I_: {}},
965 Code._3EF0: {I_: {}},
966 Code._3EF1: {RP: {}},
967 },
968 DevType.JST: { # Jasper Stat, 31
969 Code._0008: {I_: {}},
970 Code._10E0: {I_: {}},
971 Code._3EF1: {RQ: {}, RP: {}},
972 },
973 # DevType.RND: { # e.g. TR87RF: Single (round) Zone Thermostat
974 # Code._0005: {I_: {}},
975 # Code._0008: {I_: {}},
976 # Code._000A: {I_: {}, RQ: {}},
977 # Code._000C: {I_: {}},
978 # Code._000E: {I_: {}},
979 # Code._042F: {I_: {}},
980 # Code._1060: {I_: {}},
981 # Code._10E0: {I_: {}},
982 # Code._12C0: {I_: {}},
983 # Code._1FC9: {I_: {}},
984 # Code._1FD4: {I_: {}},
985 # Code._2309: {I_: {}, RQ: {}, W_: {}},
986 # Code._2349: {RQ: {}},
987 # Code._30C9: {I_: {}},
988 # Code._3120: {I_: {}},
989 # Code._313F: {I_: {}}, # W --- 30:253184 34:010943 --:------ 313F 009 006000070E0...
990 # Code._3EF0: {I_: {}, RQ: {}}, # when bound direct to a 13:
991 # Code._3EF1: {RQ: {}}, # when bound direct to a 13:
992 # },
993 # DevType.DTS: { # e.g. DTS92(E)
994 # Code._0001: {W_: {}},
995 # Code._0008: {I_: {}},
996 # Code._0009: {I_: {}},
997 # Code._000A: {I_: {}, RQ: {}, W_: {}},
998 # Code._0016: {RQ: {}},
999 # # "0B04": {I_: {}},
1000 # Code._1030: {I_: {}},
1001 # Code._1060: {I_: {}},
1002 # Code._1090: {RQ: {}},
1003 # Code._1100: {I_: {}},
1004 # Code._1F09: {I_: {}},
1005 # Code._1FC9: {I_: {}},
1006 # Code._2309: {I_: {}, RQ: {}, W_: {}},
1007 # Code._2349: {RQ: {}, W_: {}},
1008 # Code._30C9: {I_: {}},
1009 # Code._313F: {I_: {}},
1010 # Code._3B00: {I_: {}},
1011 # Code._3EF1: {RQ: {}},
1012 # },
1013 # DevType.HCW: { # e.g. HCF82/HCW82: Room Temperature Sensor
1014 # Code._0001: {W_: {}},
1015 # Code._0002: {I_: {}},
1016 # Code._0008: {I_: {}},
1017 # Code._0009: {I_: {}},
1018 # Code._1060: {I_: {}},
1019 # Code._1100: {I_: {}},
1020 # Code._1F09: {I_: {}},
1021 # Code._1FC9: {I_: {}},
1022 # Code._2309: {I_: {}},
1023 # Code._2389: {I_: {}},
1024 # Code._30C9: {I_: {}},
1025 # },
1026}
1027# TODO: add 1FC9 everywhere?
1028_DEV_KLASSES_HVAC: dict[str, dict[Code, dict[VerbT, Any]]] = {
1029 DevType.DIS: { # Orcon RF15 Display: ?a superset of a REM
1030 Code._0001: {RQ: {}},
1031 Code._042F: {I_: {}},
1032 Code._10E0: {I_: {}, RQ: {}},
1033 Code._1470: {RQ: {}},
1034 Code._1FC9: {I_: {}},
1035 Code._1F70: {I_: {}},
1036 Code._22F1: {I_: {}},
1037 Code._22F3: {I_: {}},
1038 Code._22F7: {RQ: {}, W_: {}},
1039 Code._22B0: {W_: {}},
1040 Code._2411: {RQ: {}, W_: {}},
1041 Code._313F: {RQ: {}},
1042 Code._31DA: {RQ: {}},
1043 },
1044 DevType.RFS: { # Itho spIDer: RF to Internet gateway (like a RFG100)
1045 Code._1060: {I_: {}},
1046 Code._10E0: {I_: {}, RP: {}},
1047 Code._12C0: {I_: {}},
1048 Code._22C9: {I_: {}},
1049 Code._22F1: {I_: {}},
1050 Code._22F3: {I_: {}},
1051 Code._2E10: {I_: {}},
1052 Code._30C9: {I_: {}},
1053 Code._3110: {I_: {}},
1054 Code._3120: {I_: {}},
1055 Code._31D9: {RQ: {}},
1056 Code._31DA: {RQ: {}},
1057 Code._3EF0: {I_: {}},
1058 },
1059 DevType.FAN: {
1060 Code._0001: {RP: {}},
1061 Code._0002: {I_: {}},
1062 Code._042F: {I_: {}},
1063 Code._10D0: {I_: {}, RP: {}},
1064 Code._10E0: {I_: {}, RP: {}},
1065 Code._1298: {I_: {}},
1066 Code._12A0: {I_: {}},
1067 Code._12C8: {I_: {}},
1068 Code._1470: {RP: {}},
1069 Code._1F09: {I_: {}, RP: {}},
1070 Code._1FC9: {I_: {}, W_: {}},
1071 Code._2210: {I_: {}, RP: {}},
1072 Code._22E0: {RP: {}},
1073 Code._22E5: {RP: {}},
1074 Code._22E9: {RP: {}},
1075 Code._22F1: {RP: {}},
1076 Code._22F2: {I_: {}, RP: {}},
1077 Code._22F3: {},
1078 Code._22F4: {I_: {}, RP: {}},
1079 Code._22F7: {I_: {}, RP: {}},
1080 Code._2411: {I_: {}, RP: {}},
1081 Code._2E10: {I_: {}},
1082 Code._3120: {I_: {}},
1083 Code._3150: {I_: {}},
1084 Code._313E: {RP: {}},
1085 Code._313F: {I_: {}, RP: {}},
1086 Code._31D9: {I_: {}, RP: {}},
1087 Code._31DA: {I_: {}, RP: {}},
1088 # Code._31E0: {I_: {}},
1089 Code._3200: {I_: {}},
1090 Code._3222: {RP: {}},
1091 },
1092 DevType.CO2: {
1093 Code._042F: {I_: {}},
1094 Code._10E0: {I_: {}, RP: {}},
1095 Code._1298: {I_: {}, RP: {}},
1096 Code._1FC9: {I_: {}},
1097 Code._22F1: {RQ: {}},
1098 Code._2411: {RQ: {}},
1099 Code._2E10: {I_: {}},
1100 Code._3120: {I_: {}},
1101 Code._31DA: {RQ: {}},
1102 Code._31E0: {I_: {}},
1103 },
1104 DevType.HUM: {
1105 Code._042F: {I_: {}},
1106 Code._1060: {I_: {}},
1107 Code._10E0: {I_: {}},
1108 Code._12A0: {I_: {}},
1109 Code._1FC9: {I_: {}},
1110 Code._31DA: {RQ: {}},
1111 Code._31E0: {I_: {}},
1112 },
1113 DevType.REM: { # HVAC: two-way switch; also an "06/22F1"?
1114 Code._0001: {RQ: {}}, # from a VMI (only?)
1115 Code._042F: {I_: {}}, # from a VMI (only?)
1116 Code._1060: {I_: {}},
1117 Code._10D0: {
1118 RP: {},
1119 RQ: {},
1120 W_: {},
1121 }, # RQ/RP Orcon HRC, W=reset filter count from REM
1122 Code._10E0: {I_: {}, RQ: {}}, # RQ from a VMI (only?)
1123 Code._1470: {RQ: {}}, # from a VMI (only?)
1124 Code._1FC9: {I_: {}},
1125 Code._22F1: {I_: {}},
1126 Code._22F3: {I_: {}},
1127 Code._22F7: {RQ: {}, W_: {}}, # from a VMI (only?)
1128 Code._2411: {RQ: {}, W_: {}}, # from a VMI (only?)
1129 Code._313F: {RQ: {}, W_: {}}, # from a VMI (only?)
1130 Code._31DA: {RQ: {}}, # to a VMI (only?)
1131 # Code._31E0: {I_: {}},
1132 }, # https://www.ithodaalderop.nl/nl-NL/professional/product/536-0124
1133 # None: { # unknown, TODO: make generic HVAC
1134 # _4401: {I_: {}},
1135 # },
1136}
1138CODES_BY_DEV_SLUG: dict[str, dict[Code, dict[VerbT, Any]]] = {
1139 DevType.HGI: { # HGI80: RF to (USB) serial gateway interface
1140 Code._PUZZ: {I_: {}, RQ: {}, W_: {}},
1141 }, # HGI80s can do what they like
1142 **{k: v for k, v in _DEV_KLASSES_HVAC.items() if k is not None},
1143 **{k: v for k, v in _DEV_KLASSES_HEAT.items() if k is not None},
1144}
1146CODES_OF_HEAT_DOMAIN: tuple[Code] = sorted( # type: ignore[assignment]
1147 tuple({c for k in _DEV_KLASSES_HEAT.values() for c in k}) + (Code._0B04, Code._2389)
1148)
1149CODES_OF_HVAC_DOMAIN: tuple[Code] = sorted( # type: ignore[assignment]
1150 tuple({c for k in _DEV_KLASSES_HVAC.values() for c in k})
1151 + (Code._22F8, Code._4401, Code._4E01, Code._4E02, Code._4E04)
1152)
1153CODES_OF_HEAT_DOMAIN_ONLY: tuple[Code, ...] = tuple(
1154 c for c in sorted(CODES_OF_HEAT_DOMAIN) if c not in CODES_OF_HVAC_DOMAIN
1155)
1156CODES_OF_HVAC_DOMAIN_ONLY: tuple[Code, ...] = tuple(
1157 c for c in sorted(CODES_OF_HVAC_DOMAIN) if c not in CODES_OF_HEAT_DOMAIN
1158)
1159_CODES_OF_BOTH_DOMAINS: tuple[Code, ...] = tuple(
1160 sorted(set(CODES_OF_HEAT_DOMAIN) & set(CODES_OF_HVAC_DOMAIN))
1161)
1162_CODES_OF_EITHER_DOMAIN: tuple[Code, ...] = tuple(
1163 sorted(set(CODES_OF_HEAT_DOMAIN) | set(CODES_OF_HVAC_DOMAIN))
1164)
1165_CODES_OF_NO_DOMAIN: tuple[Code, ...] = tuple(
1166 c for c in CODES_SCHEMA if c not in _CODES_OF_EITHER_DOMAIN
1167)
1169_CODE_FROM_NON_CTL: tuple[Code, ...] = tuple(
1170 dict.fromkeys(
1171 c
1172 for k, v1 in CODES_BY_DEV_SLUG.items()
1173 for c, v2 in v1.items()
1174 if k != DevType.CTL and (I_ in v2 or RP in v2)
1175 )
1176)
1177_CODE_FROM_CTL = _DEV_KLASSES_HEAT[DevType.CTL].keys()
1179_CODE_ONLY_FROM_CTL: tuple[Code, ...] = tuple(
1180 c for c in _CODE_FROM_CTL if c not in _CODE_FROM_NON_CTL
1181)
1182CODES_ONLY_FROM_CTL: tuple[Code, ...] = (
1183 Code._1030,
1184 Code._1F09,
1185 # Code._22D0, # also _W from the Spider master THM! issue #340
1186 Code._313F,
1187) # I packets, TODO: 31Dx too? not 31D9/31DA!
1189#
1190########################################################################################
1191# Other Stuff
1193# ### WIP:
1194# _result = {}
1195# for domain in (_DEV_KLASSES_HVAC, ):
1196# for klass, kv in domain.items():
1197# if klass in (DEV_TYPE.DIS, DEV_TYPE.RFS):
1198# continue
1199# for code, cv in kv.items():
1200# for verb in cv:
1201# _result.update({(verb, code): _result.get((verb, code), 0) + 1})
1203# _HVAC_VC_PAIR_BY_CLASS = {
1204# (v, c): k
1205# for c, cv in kv.items()
1206# for v in cv
1207# for k, kv in _DEV_KLASSES_HVAC.items()
1208# if (v, c) in [k for k, v in _result.items() if v == 1]
1209# }
1212_HVAC_VC_PAIR_BY_CLASS: dict[DevType, tuple[tuple[VerbT, Code], ...]] = {
1213 DevType.CO2: ((I_, Code._1298),),
1214 DevType.FAN: ((I_, Code._31D9), (I_, Code._31DA), (RP, Code._31DA)),
1215 DevType.HUM: ((I_, Code._12A0),),
1216 DevType.REM: ((I_, Code._22F1), (I_, Code._22F3)),
1217}
1218HVAC_KLASS_BY_VC_PAIR: dict[tuple[VerbT, Code], DevType] = {
1219 t: k for k, v in _HVAC_VC_PAIR_BY_CLASS.items() for t in v
1220}
1223SZ_DESCRIPTION: Final = "description"
1224SZ_MIN_VALUE: Final = "min_value"
1225SZ_MAX_VALUE: Final = "max_value"
1226SZ_PRECISION: Final = "precision"
1227SZ_DATA_TYPE: Final = "data_type"
1228SZ_DATA_UNIT: Final = "data_unit"
1230_22F1_MODE_ITHO: dict[str, str] = {
1231 "00": "off", # not seen
1232 "01": "trickle", # not seen
1233 "02": "low",
1234 "03": "medium",
1235 "04": "high", # aka boost with 22F3
1236}
1238_22F1_MODE_NUAIRE: dict[str, str] = {
1239 "02": "normal",
1240 "03": "boost", # aka purge
1241 "09": "heater_off",
1242 "0A": "heater_auto",
1243} # DRI-ECO-2S (normal/boost only), DRI-ECO-4S
1245_22F1_MODE_ORCON: dict[str, str] = {
1246 "00": "away",
1247 "01": "low",
1248 "02": "medium",
1249 "03": "high", # # The order of the next two may be swapped
1250 "04": "auto", # # economy, as per RH and CO2 <= 1150 ppm (unsure which is which)
1251 "05": "auto_alt", # comfort, as per RH and CO2 <= 950 ppm (unsure which is which)
1252 "06": "boost",
1253 "07": "off",
1254}
1256_22F1_MODE_VASCO: dict[str, str] = { # for VASCO D60 and ClimaRad Minibox remotes
1257 "00": "off",
1258 "01": "away", # 000106 minimum
1259 "02": "low", # 000206
1260 "03": "medium", # 000306
1261 "04": "high", # 000406, aka boost with 22F3
1262 "05": "auto",
1263}
1265_22F1_SCHEMES: dict[str, dict[str, str]] = {
1266 "itho": _22F1_MODE_ITHO,
1267 "nuaire": _22F1_MODE_NUAIRE,
1268 "orcon": _22F1_MODE_ORCON,
1269 "vasco": _22F1_MODE_VASCO,
1270}
1272# unclear if true for only Orcon/*all* models
1273_2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
1274 "01": { # all?
1275 SZ_DESCRIPTION: "Support",
1276 SZ_MIN_VALUE: 0xFF, # None?
1277 SZ_MAX_VALUE: 0xFF,
1278 SZ_PRECISION: 1,
1279 SZ_DATA_TYPE: "00",
1280 SZ_DATA_UNIT: "",
1281 },
1282 "31": { # slot 09 (FANs produced after 2021)
1283 SZ_DESCRIPTION: "Time to change filter (days)",
1284 SZ_MIN_VALUE: 0,
1285 SZ_MAX_VALUE: 1800,
1286 SZ_PRECISION: 30,
1287 SZ_DATA_TYPE: "10",
1288 SZ_DATA_UNIT: "days",
1289 },
1290 "3D": { # slot 00
1291 SZ_DESCRIPTION: "Away mode Supply fan rate (%)",
1292 SZ_MIN_VALUE: 0.0,
1293 SZ_MAX_VALUE: 0.4,
1294 SZ_PRECISION: 0.005,
1295 SZ_DATA_TYPE: "0F",
1296 SZ_DATA_UNIT: "%",
1297 },
1298 "3E": { # slot 01
1299 SZ_DESCRIPTION: "Away mode Exhaust fan rate (%)",
1300 SZ_MIN_VALUE: 0.0,
1301 SZ_MAX_VALUE: 0.4,
1302 SZ_PRECISION: 0.005,
1303 SZ_DATA_TYPE: "0F",
1304 SZ_DATA_UNIT: "%",
1305 },
1306 "3F": { # slot 02
1307 SZ_DESCRIPTION: "Low mode Supply fan rate (%)",
1308 SZ_MIN_VALUE: 0.0,
1309 SZ_MAX_VALUE: 0.8,
1310 SZ_PRECISION: 0.005,
1311 SZ_DATA_TYPE: "0F",
1312 SZ_DATA_UNIT: "%",
1313 },
1314 "40": { # slot 03
1315 SZ_DESCRIPTION: "Low mode Exhaust fan rate (%)",
1316 SZ_MIN_VALUE: 0.0,
1317 SZ_MAX_VALUE: 0.8,
1318 SZ_PRECISION: 0.005,
1319 SZ_DATA_TYPE: "0F",
1320 SZ_DATA_UNIT: "%",
1321 },
1322 "41": { # slot 04
1323 SZ_DESCRIPTION: "Medium mode Supply fan rate (%)",
1324 SZ_MIN_VALUE: 0.1, # Orcon FAN responds with 0.0, but I guess this should be the same as for "42"
1325 SZ_MAX_VALUE: 1.0,
1326 SZ_PRECISION: 0.005,
1327 SZ_DATA_TYPE: "0F",
1328 SZ_DATA_UNIT: "%",
1329 },
1330 "42": { # slot 05
1331 SZ_DESCRIPTION: "Medium mode Exhaust fan rate (%)",
1332 SZ_MIN_VALUE: 0.1,
1333 SZ_MAX_VALUE: 1.0,
1334 SZ_PRECISION: 0.005,
1335 SZ_DATA_TYPE: "0F",
1336 SZ_DATA_UNIT: "%",
1337 },
1338 "43": { # slot 06
1339 SZ_DESCRIPTION: "High mode Supply fan rate (%)",
1340 SZ_MIN_VALUE: 0.1,
1341 SZ_MAX_VALUE: 1.0,
1342 SZ_PRECISION: 0.005,
1343 SZ_DATA_TYPE: "0F",
1344 SZ_DATA_UNIT: "%",
1345 },
1346 "44": { # slot 07
1347 SZ_DESCRIPTION: "High mode Exhaust fan rate (%)",
1348 SZ_MIN_VALUE: 0.1,
1349 SZ_MAX_VALUE: 1.0,
1350 SZ_PRECISION: 0.005,
1351 SZ_DATA_TYPE: "0F",
1352 SZ_DATA_UNIT: "%",
1353 },
1354 "4B": { # slot 09 (FANs produced before 2021) Also check code 22F7
1355 SZ_DESCRIPTION: "(Test) Bypass Valve (0=auto, 1=open, 2=closed)",
1356 SZ_MIN_VALUE: 0,
1357 SZ_MAX_VALUE: 2,
1358 SZ_PRECISION: 1,
1359 SZ_DATA_TYPE: "00",
1360 SZ_DATA_UNIT: "",
1361 },
1362 "4E": { # slot 0A
1363 SZ_DESCRIPTION: "Moisture scenario position (0=medium, 1=high)",
1364 SZ_MIN_VALUE: 0,
1365 SZ_MAX_VALUE: 1,
1366 SZ_PRECISION: 1,
1367 SZ_DATA_TYPE: "00",
1368 SZ_DATA_UNIT: "",
1369 },
1370 "52": { # slot 0B
1371 SZ_DESCRIPTION: "Sensor sensitivity (%)",
1372 SZ_MIN_VALUE: 0,
1373 SZ_MAX_VALUE: 25.0,
1374 SZ_PRECISION: 0.1,
1375 SZ_DATA_TYPE: "01",
1376 SZ_DATA_UNIT: "%",
1377 },
1378 "54": { # slot 0C
1379 SZ_DESCRIPTION: "Moisture sensor overrun time (mins)",
1380 SZ_MIN_VALUE: 15,
1381 SZ_MAX_VALUE: 60,
1382 SZ_PRECISION: 1,
1383 SZ_DATA_TYPE: "00",
1384 SZ_DATA_UNIT: "min",
1385 },
1386 "75": { # slot 0D
1387 SZ_DESCRIPTION: "Comfort temperature (°C)",
1388 SZ_MIN_VALUE: 0.0,
1389 SZ_MAX_VALUE: 30.0,
1390 SZ_PRECISION: 0.01,
1391 SZ_DATA_TYPE: "92",
1392 SZ_DATA_UNIT: "°C",
1393 },
1394 "95": { # slot 08
1395 SZ_DESCRIPTION: "Boost mode Supply/exhaust fan rate (%)",
1396 SZ_MIN_VALUE: 0.0,
1397 SZ_MAX_VALUE: 1.0,
1398 SZ_PRECISION: 0.005,
1399 SZ_DATA_TYPE: "0F",
1400 SZ_DATA_UNIT: "%",
1401 },
1402}
1404# ventilation speed description
1405_31D9_FAN_INFO_VASCO: dict[int, str] = {
1406 0x00: "off",
1407 0x01: "1 (trickle)", # aka low
1408 0x02: "2 (low)", # aka medium
1409 0x03: "3 (medium)", # aka high
1410 0x04: "4 (boost)",
1411 0x05: "auto",
1412 0xC8: "III (boost)", # same code sent for speed II and III, mode manual
1413 0x50: "I (low)",
1414 0x1E: "0 (very low)",
1415}
1417# ventilation speed
1418_31DA_FAN_INFO: dict[int, str] = {
1419 0x00: "off",
1420 0x01: "speed 1, low", # aka low
1421 0x02: "speed 2, medium", # aka medium
1422 0x03: "speed 3, high", # aka high
1423 0x04: "speed 4",
1424 0x05: "speed 5",
1425 0x06: "speed 6",
1426 0x07: "speed 7",
1427 0x08: "speed 8",
1428 0x09: "speed 9",
1429 0x0A: "speed 10",
1430 0x0B: "speed 1 temporary override", # timer
1431 0x0C: "speed 2 temporary override", # timer
1432 0x0D: "speed 3 temporary override", # timer/boost? (timer 1, 2, 3)
1433 0x0E: "speed 4 temporary override",
1434 0x0F: "speed 5 temporary override",
1435 0x10: "speed 6 temporary override",
1436 0x11: "speed 7 temporary override",
1437 0x12: "speed 8 temporary override",
1438 0x13: "speed 9 temporary override",
1439 0x14: "speed 10 temporary override",
1440 0x15: "away", # absolute minimum speed
1441 0x16: "absolute minimum", # trickle?
1442 0x17: "boost", # absolute maximum", # boost?
1443 0x18: "auto",
1444 0x19: "auto_night",
1445 0x1A: "-unknown 0x1A-",
1446 0x1B: "-unknown 0x1B-",
1447 0x1C: "-unknown 0x1C-",
1448 0x1D: "-unknown 0x1D-",
1449 0x1E: "-unknown 0x1E-",
1450 0x1F: "-unknown 0x1F-", # static field, used as filter in parser_31da so keep same
1451}
1453#
1454########################################################################################
1455# CODES_BY_ZONE_TYPE
1456#
1457# RAMSES_ZONES: dict[str, str] = {
1458# "ALL": {
1459# Code._0004: {I_: {}, RP: {}},
1460# Code._000C: {RP: {}},
1461# Code._000A: {I_: {}, RP: {}},
1462# Code._2309: {I_: {}, RP: {}},
1463# Code._2349: {I_: {}, RP: {}},
1464# Code._30C9: {I_: {}, RP: {}},
1465# },
1466# ZON_ROLE.RAD: {
1467# Code._12B0: {I_: {}, RP: {}},
1468# "3150a": {},
1469# },
1470# ZON_ROLE.ELE: {
1471# Code._0008: {I_: {}},
1472# Code._0009: {I_: {}},
1473# },
1474# ZON_ROLE.VAL: {
1475# Code._0008: {I_: {}},
1476# Code._0009: {I_: {}},
1477# "3150a": {},
1478# },
1479# ZON_ROLE.UFH: {
1480# Code._3150: {I_: {}},
1481# },
1482# ZON_ROLE.MIX: {
1483# Code._0008: {I_: {}},
1484# "3150a": {},
1485# },
1486# ZON_ROLE.DHW: {
1487# Code._10A0: {RQ: {}, RP: {}},
1488# Code._1260: {I_: {}},
1489# Code._1F41: {I_: {}},
1490# },
1491# }
1492# RAMSES_ZONES_ALL = RAMSES_ZONES.pop("ALL")
1493# RAMSES_ZONES_DHW = RAMSES_ZONES[ZON_ROLE.DHW]
1494# [RAMSES_ZONES[k].update(RAMSES_ZONES_ALL) for k in RAMSES_ZONES if k != ZON_ROLE.DHW]
1496__all__ = [
1497 "CODES_BY_DEV_SLUG",
1498 "CODES_SCHEMA",
1499 "CODE_NAME_LOOKUP",
1500 "CODES_BY_DEV_SLUG",
1501 "CODES_SCHEMA",
1502 "HVAC_KLASS_BY_VC_PAIR",
1503 "_2411_PARAMS_SCHEMA",
1504 "SZ_DESCRIPTION",
1505 "SZ_MIN_VALUE",
1506 "SZ_MAX_VALUE",
1507 "SZ_PRECISION",
1508 "SZ_DATA_TYPE",
1509 "SZ_DATA_UNIT",
1510]