ramses-rf 0.22.40__py3-none-any.whl → 0.51.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. ramses_cli/__init__.py +18 -0
  2. ramses_cli/client.py +597 -0
  3. ramses_cli/debug.py +20 -0
  4. ramses_cli/discovery.py +405 -0
  5. ramses_cli/utils/cat_slow.py +17 -0
  6. ramses_cli/utils/convert.py +60 -0
  7. ramses_rf/__init__.py +31 -10
  8. ramses_rf/binding_fsm.py +787 -0
  9. ramses_rf/const.py +124 -105
  10. ramses_rf/database.py +297 -0
  11. ramses_rf/device/__init__.py +69 -39
  12. ramses_rf/device/base.py +187 -376
  13. ramses_rf/device/heat.py +540 -552
  14. ramses_rf/device/hvac.py +286 -171
  15. ramses_rf/dispatcher.py +153 -177
  16. ramses_rf/entity_base.py +478 -361
  17. ramses_rf/exceptions.py +82 -0
  18. ramses_rf/gateway.py +377 -513
  19. ramses_rf/helpers.py +57 -19
  20. ramses_rf/py.typed +0 -0
  21. ramses_rf/schemas.py +148 -194
  22. ramses_rf/system/__init__.py +16 -23
  23. ramses_rf/system/faultlog.py +363 -0
  24. ramses_rf/system/heat.py +295 -302
  25. ramses_rf/system/schedule.py +312 -198
  26. ramses_rf/system/zones.py +318 -238
  27. ramses_rf/version.py +2 -8
  28. ramses_rf-0.51.1.dist-info/METADATA +72 -0
  29. ramses_rf-0.51.1.dist-info/RECORD +55 -0
  30. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.1.dist-info}/WHEEL +1 -2
  31. ramses_rf-0.51.1.dist-info/entry_points.txt +2 -0
  32. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.1.dist-info/licenses}/LICENSE +1 -1
  33. ramses_tx/__init__.py +160 -0
  34. {ramses_rf/protocol → ramses_tx}/address.py +65 -59
  35. ramses_tx/command.py +1454 -0
  36. ramses_tx/const.py +903 -0
  37. ramses_tx/exceptions.py +92 -0
  38. {ramses_rf/protocol → ramses_tx}/fingerprints.py +56 -15
  39. {ramses_rf/protocol → ramses_tx}/frame.py +132 -131
  40. ramses_tx/gateway.py +338 -0
  41. ramses_tx/helpers.py +883 -0
  42. {ramses_rf/protocol → ramses_tx}/logger.py +67 -53
  43. {ramses_rf/protocol → ramses_tx}/message.py +155 -191
  44. ramses_tx/opentherm.py +1260 -0
  45. ramses_tx/packet.py +210 -0
  46. {ramses_rf/protocol → ramses_tx}/parsers.py +1266 -1003
  47. ramses_tx/protocol.py +801 -0
  48. ramses_tx/protocol_fsm.py +672 -0
  49. ramses_tx/py.typed +0 -0
  50. {ramses_rf/protocol → ramses_tx}/ramses.py +262 -185
  51. {ramses_rf/protocol → ramses_tx}/schemas.py +150 -133
  52. ramses_tx/transport.py +1471 -0
  53. ramses_tx/typed_dicts.py +492 -0
  54. ramses_tx/typing.py +181 -0
  55. ramses_tx/version.py +4 -0
  56. ramses_rf/discovery.py +0 -398
  57. ramses_rf/protocol/__init__.py +0 -59
  58. ramses_rf/protocol/backports.py +0 -42
  59. ramses_rf/protocol/command.py +0 -1576
  60. ramses_rf/protocol/const.py +0 -697
  61. ramses_rf/protocol/exceptions.py +0 -111
  62. ramses_rf/protocol/helpers.py +0 -390
  63. ramses_rf/protocol/opentherm.py +0 -1170
  64. ramses_rf/protocol/packet.py +0 -235
  65. ramses_rf/protocol/protocol.py +0 -613
  66. ramses_rf/protocol/transport.py +0 -1011
  67. ramses_rf/protocol/version.py +0 -10
  68. ramses_rf/system/hvac.py +0 -82
  69. ramses_rf-0.22.40.dist-info/METADATA +0 -64
  70. ramses_rf-0.22.40.dist-info/RECORD +0 -42
  71. ramses_rf-0.22.40.dist-info/top_level.txt +0 -1
@@ -1,42 +1,37 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- #
4
2
  """RAMSES RF - a RAMSES-II protocol decoder & analyser."""
3
+
4
+ # TODO: code a lifespan for most packets
5
+
5
6
  from __future__ import annotations
6
7
 
7
- import logging
8
8
  from datetime import timedelta as td
9
+ from typing import Any, Final
9
10
 
10
- from .const import DEV_TYPE, SZ_NAME, __dev_mode__
11
+ from .const import SZ_NAME, DevType
11
12
 
12
- # skipcq: PY-W2000
13
13
  from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
14
14
  I_,
15
15
  RP,
16
16
  RQ,
17
17
  W_,
18
18
  Code,
19
+ VerbT,
19
20
  )
20
21
 
21
- DEV_MODE = __dev_mode__ and False
22
22
 
23
- _LOGGER = logging.getLogger(__name__)
24
- if DEV_MODE:
25
- _LOGGER.setLevel(logging.DEBUG)
23
+ SZ_LIFESPAN: Final = "lifespan" # WIP
26
24
 
27
- RQ_NULL = "rq_null"
28
- EXPIRES = "expires"
29
25
 
30
- EXPIRY = "expiry"
26
+ #
27
+ ########################################################################################
28
+ # CODES_SCHEMA - HEAT (CH/DHW, Honeywell/Resideo) vs HVAC (ventilation, Itho/Orcon/etc.)
31
29
 
32
30
  # The master list - all known codes are here, even if there's no corresponding parser
33
31
  # Anything with a zone-idx should start: ^0[0-9A-F], ^(0[0-9A-F], or ^((0[0-9A-F]
34
32
 
35
33
  #
36
- ########################################################################################
37
- # CODES_SCHEMA - HEAT (CH/DHW, Honeywell/Resideo) vs HVAC (ventilation, Itho/Orcon/etc.)
38
- #
39
- CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
34
+ CODES_SCHEMA: dict[Code, dict[str, Any]] = { # rf_unknown
40
35
  Code._0001: {
41
36
  SZ_NAME: "rf_unknown",
42
37
  I_: r"^00FFFF02(00|FF)$", # loopback
@@ -45,6 +40,9 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
45
40
  W_: r"^(0[0-9A-F]|FC|FF)000005(01|05)$",
46
41
  }, # TODO: there appears to be a dodgy? RQ/RP for UFC
47
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?
48
46
  SZ_NAME: "outdoor_sensor",
49
47
  I_: r"^0[0-4][0-9A-F]{4}(00|01|02|05)$", # Domoticz sends ^02!!
50
48
  RQ: r"^00$", # NOTE: sent by an RFG100
@@ -53,15 +51,16 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
53
51
  SZ_NAME: "zone_name",
54
52
  I_: r"^0[0-9A-F]00([0-9A-F]){40}$", # RP is same, null_rp: xxxx,7F*20
55
53
  RQ: r"^0[0-9A-F]00$",
56
- EXPIRES: td(days=1),
54
+ W_: r"^0[0-9A-F]00([0-9A-F]){40}$", # contrived
55
+ SZ_LIFESPAN: td(days=1),
57
56
  },
58
57
  Code._0005: { # system_zones
59
58
  SZ_NAME: "system_zones",
60
59
  # .I --- 34:092243 --:------ 34:092243 0005 012 000A0000-000F0000-00100000
61
60
  I_: r"^(00[01][0-9A-F]{5}){1,3}$",
62
- RQ: r"^00[01][0-9A-F]$", # f"00{zone_type}", evohome wont respond to 00
61
+ RQ: r"^00[01][0-9A-F]$", # f"00{zone_type}", evohome won't respond to 00
63
62
  RP: r"^00[01][0-9A-F]{3,5}$",
64
- EXPIRES: False,
63
+ SZ_LIFESPAN: False,
65
64
  },
66
65
  Code._0006: { # schedule_version # TODO: what for DHW schedule?
67
66
  SZ_NAME: "schedule_version",
@@ -93,7 +92,7 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
93
92
  # 17:54:13.141 045 RP --- 01:145038 34:064023 --:------ 000A 006 031002260B86
94
93
  # 19:20:49.460 062 RQ --- 12:010740 01:145038 --:------ 000A 006 080001F40DAC
95
94
  # 19:20:49.476 045 RP --- 01:145038 12:010740 --:------ 000A 006 081001F40DAC
96
- EXPIRES: td(days=1),
95
+ SZ_LIFESPAN: td(days=1),
97
96
  },
98
97
  Code._000C: { # zone_devices
99
98
  SZ_NAME: "zone_devices",
@@ -101,11 +100,11 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
101
100
  # RP --- 01:145038 18:013393 --:------ 000C 016 05-08-00-109901 08-00-109902 08-00-109903
102
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}$",
103
102
  RQ: r"^0[0-9A-F][01][0-9A-F]$", # TODO: f"{zone_idx}{device_type}"
104
- EXPIRES: False,
103
+ SZ_LIFESPAN: False,
105
104
  },
106
105
  Code._000E: { # unknown_000e
107
106
  SZ_NAME: "message_000e",
108
- I_: r"^000014$",
107
+ I_: r"^0000(14|28)$",
109
108
  },
110
109
  Code._0016: { # rf_check
111
110
  SZ_NAME: "rf_check",
@@ -116,7 +115,7 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
116
115
  SZ_NAME: "language",
117
116
  RQ: r"^00([0-9A-F]{4}F{4})?$", # NOTE: RQ/04/0100 has a payload
118
117
  RP: r"^00[0-9A-F]{4}F{4}$",
119
- EXPIRES: td(days=1), # TODO: make longer?
118
+ SZ_LIFESPAN: td(days=1), # TODO: make longer?
120
119
  },
121
120
  Code._0150: { # unknown_0150
122
121
  SZ_NAME: "message_0150",
@@ -137,13 +136,19 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
137
136
  # .W --- 04:000722 01:158182 --:------ 01E9 002 0003 # is a guess, the
138
137
  # .I --- 01:158182 04:000722 --:------ 01E9 002 0000 # TRV was in zone 00
139
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
+ },
140
145
  Code._0404: { # zone_schedule
141
146
  SZ_NAME: "zone_schedule",
142
147
  I_: r"^0[0-9A-F](20|23)[0-9A-F]{2}08[0-9A-F]{6}$",
143
148
  RQ: r"^0[0-9A-F](20|23)000800[0-9A-F]{4}$",
144
149
  RP: r"^0[0-9A-F](20|23)0008[0-9A-F]{6}[0-9A-F]{2,82}$",
145
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
146
- EXPIRES: None,
151
+ SZ_LIFESPAN: None,
147
152
  },
148
153
  Code._0418: { # system_fault
149
154
  SZ_NAME: "system_fault",
@@ -167,11 +172,13 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
167
172
  SZ_NAME: "mixvalve_params",
168
173
  # .I --- --:------ --:------ 12:138834 1030 016 01C80137C9010FCA0196CB010FCC0101
169
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
170
177
  },
171
178
  Code._1060: { # device_battery
172
179
  SZ_NAME: "device_battery",
173
180
  I_: r"^0[0-9A-F](FF|[0-9A-F]{2})0[01]$", # HCW: r"^(FF|0[0-9A-F]...
174
- EXPIRES: td(days=1),
181
+ SZ_LIFESPAN: td(days=1),
175
182
  },
176
183
  Code._1081: { # max_ch_setpoint
177
184
  SZ_NAME: "max_ch_setpoint",
@@ -196,34 +203,34 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
196
203
  # NOTE: RFG100 uses a domain id! (00|01)
197
204
  # 19:14:24.662 051 RQ --- 30:185469 01:037519 --:------ 10A0 001 00
198
205
  # 19:14:31.463 053 RQ --- 30:185469 01:037519 --:------ 10A0 001 01
199
- I_: r"^0[01][0-9A-F]{4}([0-9A-F]{6})?$", # NOTE: RQ/07/10A0 has a payload
200
- RQ: r"^0[01]([0-9A-F]{10})?$", # NOTE: RQ/07/10A0 has a payload
201
- W_: r"^0[01][0-9A-F]{4}([0-9A-F]{6})?$", # TODO: needs checking
202
- EXPIRES: td(hours=4),
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),
203
210
  },
204
211
  Code._10B0: { # unknown_10b0
205
212
  SZ_NAME: "message_10b0",
206
213
  RQ: r"^00$",
207
214
  RP: r"^00[0-9A-F]{8}$",
208
215
  },
209
- Code._10D0: { # filter_change
216
+ Code._10D0: { # filter_change - polling interval should be 1/day
210
217
  SZ_NAME: "filter_change",
211
- I_: r"^00[0-9A-F]{6}(0000)?$",
218
+ I_: r"^00[0-9A-F]{6}(0000|FFFF)?$",
212
219
  RQ: r"^00(00)?$",
213
220
  W_: r"^00FF$",
214
221
  },
215
222
  Code._10E0: { # device_info
216
223
  SZ_NAME: "device_info",
217
- I_: r"^00[0-9A-F]{30,}$", # r"^[0-9A-F]{32,}$" might be OK
218
- RQ: r"^00$", # NOTE: will accept [0-9A-F]{2}
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}
219
226
  # RP: r"^[0-9A-F]{2}([0-9A-F]){30,}$", # NOTE: indx same as RQ
220
- EXPIRES: False,
227
+ SZ_LIFESPAN: False,
221
228
  },
222
229
  Code._10E1: { # device_id
223
230
  SZ_NAME: "device_id",
224
231
  RP: r"^00[0-9A-F]{6}$",
225
232
  RQ: r"^00$",
226
- EXPIRES: False,
233
+ SZ_LIFESPAN: False,
227
234
  },
228
235
  Code._10E2: { # unknown_10e2, HVAC?
229
236
  SZ_NAME: "unknown_10e2",
@@ -231,14 +238,16 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
231
238
  },
232
239
  Code._1100: { # tpi_params
233
240
  SZ_NAME: "tpi_params",
241
+ # I --- 01:172368 --:------ 01:172368 1100 008 FC180400007FFF00
242
+ # I --- 01:172368 13:040439 --:------ 1100 008 FC042814007FFF00
234
243
  # RQ --- 01:145038 13:163733 --:------ 1100 008 00180400007FFF01 # boiler relay
235
244
  # RP --- 13:163733 01:145038 --:------ 1100 008 00180400FF7FFF01
236
245
  # RQ --- 01:145038 13:035462 --:------ 1100 008 FC240428007FFF01 # not bolier relay
237
246
  # RP --- 13:035462 01:145038 --:------ 1100 008 00240428007FFF01
238
- I_: r"^(00|FC)[0-9A-F]{6}(00|FF)([0-9A-F]{4}01)?$",
239
- W_: r"^(00|FC)[0-9A-F]{6}(00|FF)([0-9A-F]{4}01)?$", # TODO: is there no I?
240
- RQ: r"^(00|FC)([0-9A-F]{6}(00|FF)([0-9A-F]{4}01)?)?$", # RQ/13:/00, or RQ/01:/FC:
241
- EXPIRES: td(days=1),
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),
242
251
  },
243
252
  Code._11F0: { # unknown_11f0, from heatpump relay
244
253
  SZ_NAME: "message_11f0",
@@ -251,9 +260,9 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
251
260
  # RQ --- 18:200202 10:067219 --:------ 1260 002 0000
252
261
  # RP --- 10:067219 18:200202 --:------ 1260 003 007FFF
253
262
  # .I --- 07:045960 --:------ 07:045960 1260 003 0007A9
254
- I_: r"^0[01][0-9A-F]{4}$", # NOTE: RP is same
255
- RQ: r"^0[01](00)?$", # TODO: officially: r"^0[01]$"
256
- EXPIRES: td(hours=1),
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),
257
266
  },
258
267
  Code._1280: { # outdoor_humidity
259
268
  SZ_NAME: "outdoor_humidity",
@@ -266,21 +275,20 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
266
275
  },
267
276
  Code._1298: { # co2_level
268
277
  SZ_NAME: "co2_level",
269
- I_: r"^00[0-9A-F]{4}$",
278
+ I_: r"^00[0-9A-F]{4}$", # NOTE: RP is same
279
+ RQ: r"^00$",
270
280
  },
271
281
  Code._12A0: { # indoor_humidity
272
- # .I --- 32:168090 --:------ 32:168090 12A0 006 0030093504A8
273
- # .I --- 32:132125 --:------ 32:132125 12A0 007 003107B67FFF00 # only dev_id with 007
274
- # RP --- 20:008749 18:142609 --:------ 12A0 002 00EF
275
282
  SZ_NAME: "indoor_humidity",
276
- I_: r"^00[0-9A-F]{2}([0-9A-F]{8}(00)?)?$",
277
- EXPIRES: td(hours=1),
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),
278
286
  },
279
287
  Code._12B0: { # window_state (HVAC % window open)
280
288
  SZ_NAME: "window_state",
281
289
  I_: r"^0[0-9A-F](0000|C800|FFFF)$", # NOTE: RP is same
282
290
  RQ: r"^0[0-9A-F](00)?$",
283
- EXPIRES: td(hours=1),
291
+ SZ_LIFESPAN: td(hours=1),
284
292
  },
285
293
  Code._12C0: { # displayed_temp (HVAC room temp)
286
294
  SZ_NAME: "displayed_temp", # displayed room temp
@@ -321,10 +329,10 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
321
329
  },
322
330
  Code._1F41: { # dhw_mode
323
331
  SZ_NAME: "dhw_mode",
324
- I_: r"^0[01](00|01|FF)0[0-5]F{6}(([0-9A-F]){12})?$",
325
- RQ: r"^0[01]$", # will accept: r"^0[01](00)$"
326
- W_: r"^0[01](00|01|FF)0[0-5]F{6}(([0-9A-F]){12})?$",
327
- EXPIRES: td(hours=4),
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),
328
336
  },
329
337
  Code._1F70: { # programme_config, HVAC (1470, 1F70, 22B0)
330
338
  SZ_NAME: "programme_config",
@@ -333,14 +341,11 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
333
341
  W_: r"^00[0-9A-F]{30}$",
334
342
  },
335
343
  Code._1FC9: { # rf_bind
336
- # RP --- 13:035462 18:013393 --:------ 1FC9 018 00-3EF0-348A86 00-11F0-348A86 90-3FF1-956ABD # noqa: E501
337
- # RP --- 13:035462 18:013393 --:------ 1FC9 018 00-3EF0-348A86 00-11F0-348A86 90-7FE1-DD6ABD # noqa: E501
338
- # RP --- 01:145038 18:013393 --:------ 1FC9 012 FF-10E0-06368E FF-1FC9-06368E
339
344
  SZ_NAME: "rf_bind", # idx-code-dev_id
340
345
  RQ: r"^00$",
341
- RP: r"^((0[0-9A-F]|F[69ABCF]|90)([0-9A-F]{10}))+$", # # NOTE: idx can be 90 (HEAT)
342
- I_: r"^((0[0-9A-F]|F[69ABCF]|63|67)([0-9A-F]{10}))+|00$", # NOTE: idx can be 63|67 (HVAC), payload can be 00
343
- W_: r"^((0[0-9A-F]|F[69ABCF])([0-9A-F]{10}))+$",
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}))+$",
344
349
  },
345
350
  Code._1FCA: { # unknown_1fca
346
351
  SZ_NAME: "message_1fca",
@@ -358,20 +363,19 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
358
363
  SZ_NAME: "opentherm_sync",
359
364
  I_: r"^00([0-9A-F]{4})$",
360
365
  },
361
- Code._2210: { # unknown_2210, HVAC, NB: no I
366
+ Code._2210: { # unknown_2210, HVAC,
362
367
  SZ_NAME: "unknown_2210",
368
+ I_: r"^00[0-9A-F]{82}$",
363
369
  RQ: r"^00$",
364
- RP: r"^00[0-9A-F]{82}$",
365
370
  },
366
371
  Code._2249: { # setpoint_now?
367
372
  SZ_NAME: "setpoint_now", # setpt_now_next
368
373
  I_: r"^(0[0-9A-F]{13}){1,2}$",
369
374
  }, # TODO: This could be an array
370
- Code._22C9: { # ufh_setpoint
371
- SZ_NAME: "ufh_setpoint",
372
- I_: r"^(0[0-9A-F][0-9A-F]{8}0[12]){1,4}(0203)?$", # ~000A array, but max_len 24, not 48!
373
- W_: r"^(0[0-9A-F][0-9A-F]{8}0[12])$", # ~000A array, but max_len 24, not 48!
374
- # RP: Appear wont get any?,
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
375
379
  },
376
380
  Code._22D0: { # unknown_22d0, HVAC system switch?
377
381
  SZ_NAME: "message_22d0",
@@ -401,6 +405,7 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
401
405
  Code._22F1: { # fan_mode, HVAC
402
406
  SZ_NAME: "fan_mode",
403
407
  RQ: r"^00$",
408
+ RP: r"^00[0-9A-F]{4}$",
404
409
  I_: r"^(00|63)(0[0-9A-F]){1,2}$",
405
410
  },
406
411
  Code._22F2: { # unknown_22f2, HVAC, NB: no I
@@ -410,12 +415,12 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
410
415
  },
411
416
  Code._22F3: { # fan_boost, HVAC
412
417
  SZ_NAME: "fan_boost",
413
- I_: r"^(00|63)[0-9A-F]{4}([0-9A-F]{8})?$",
414
- }, # minutes
415
- Code._22F4: { # unknown_22f4, HVAC, NB: no I
418
+ I_: r"^(00|63)(021E)?[0-9A-F]{4}([0-9A-F]{8})?$",
419
+ }, # minutes only?
420
+ Code._22F4: { # unknown_22f4, HVAC
416
421
  SZ_NAME: "unknown_22f4",
422
+ I_: r"^00[0-9A-F]{24}$",
417
423
  RQ: r"^00$",
418
- RP: r"^00[0-9A-F]{24}$",
419
424
  },
420
425
  Code._22F7: { # fan_bypass_mode (% open), HVAC
421
426
  SZ_NAME: "fan_bypass_mode",
@@ -439,7 +444,7 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
439
444
  W_: r"^0[0-9A-F]{5}$",
440
445
  # RQ --- 12:010740 01:145038 --:------ 2309 003 03073A # No RPs
441
446
  RQ: r"^0[0-9A-F]([0-9A-F]{4})?$", # NOTE: 12 uses: r"^0[0-9A-F]$"
442
- EXPIRES: td(minutes=30),
447
+ SZ_LIFESPAN: td(minutes=30),
443
448
  },
444
449
  Code._2349: { # zone_mode
445
450
  SZ_NAME: "zone_mode",
@@ -449,7 +454,7 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
449
454
  # .W --- 18:141846 01:050858 --:------ 2349 007 02-08FC-01-FFFFFF
450
455
  RQ: r"^0[0-9A-F](00|[0-9A-F]{12})?$",
451
456
  # RQ --- 22:070483 01:063844 --:------ 2349 007 06-0708-03-000027
452
- EXPIRES: td(hours=4),
457
+ SZ_LIFESPAN: td(hours=4),
453
458
  },
454
459
  Code._2389: { # unknown_2389 - CODE_IDX_COMPLEX?
455
460
  # .I 024 03:052382 --:------ 03:052382 2389 003 02001B
@@ -473,9 +478,9 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
473
478
  },
474
479
  Code._2411: { # fan_params, HVAC
475
480
  SZ_NAME: "fan_params",
476
- I_: r"^0000[0-9A-F]{6}([0-9A-F]{8}){4}[0-9A-F]{4}$",
477
- RQ: r"^0000[0-9A-F]{2}((00){19})?$",
478
- W_: r"^0000[0-9A-F]{6}[0-9A-F]{8}(([0-9A-F]{8}){3}[0-9A-F]{4})?$",
481
+ I_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}([0-9A-F]{8}){4}[0-9A-F]{4}$",
482
+ RQ: r"^(00|01|15|16|17|21)00[0-9A-F]{2}((00){19})?$",
483
+ 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]{4})?$",
479
484
  },
480
485
  Code._2420: { # unknown_2420, from OTB
481
486
  SZ_NAME: "message_2420",
@@ -496,7 +501,7 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
496
501
  I_: r"^0[0-7][0-9A-F]{12}0[01]$",
497
502
  RQ: r"^FF$",
498
503
  W_: r"^0[0-7][0-9A-F]{12}0[01]$",
499
- EXPIRES: td(hours=4),
504
+ SZ_LIFESPAN: td(hours=4),
500
505
  },
501
506
  Code._2E10: { # presence_detect - HVAC
502
507
  SZ_NAME: "presence_detect",
@@ -507,11 +512,11 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
507
512
  I_: r"^(0[0-9A-F][0-9A-F]{4})+$",
508
513
  RQ: r"^0[0-9A-F](00)?$", # TODO: officially: r"^0[0-9A-F]$"
509
514
  RP: r"^0[0-9A-F][0-9A-F]{4}$", # Null: r"^0[0-9A-F]7FFF$"
510
- EXPIRES: td(hours=1),
515
+ SZ_LIFESPAN: td(hours=1),
511
516
  },
512
- Code._3110: { # unknown_3110 - HVAC
513
- SZ_NAME: "message_3110",
514
- I_: r"^00",
517
+ Code._3110: { # ufc_demand - HVAC
518
+ SZ_NAME: "ufc_demand",
519
+ I_: r"^(00|01)00[0-9A-F]{2}(00|10|20)", # (00|10|20|FF)???
515
520
  },
516
521
  Code._3120: { # unknown_3120 - Error Report?
517
522
  SZ_NAME: "message_3120",
@@ -529,24 +534,24 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
529
534
  I_: r"^00[0-9A-F]{16}$", # NOTE: RP is same
530
535
  RQ: r"^00$",
531
536
  W_: r"^00[0-9A-F]{16}$",
532
- EXPIRES: td(seconds=3),
537
+ SZ_LIFESPAN: td(seconds=3),
533
538
  },
534
- Code._3150: { # heat_demand
539
+ Code._3150: { # heat_demand, also fans with preheat
535
540
  SZ_NAME: "heat_demand",
536
541
  I_: r"^((0[0-9A-F])[0-9A-F]{2}|FC[0-9A-F]{2})+$",
537
- EXPIRES: td(minutes=20),
542
+ SZ_LIFESPAN: td(minutes=20),
538
543
  },
539
544
  Code._31D9: { # fan_state
540
545
  SZ_NAME: "fan_state",
541
546
  # I_: r"^(00|21)[0-9A-F]{32}$",
542
547
  # I_: r"^(00|01|21)[0-9A-F]{4}((00|FE)(00|20){12}(00|08))?$",
543
- I_: r"^(00|01|21)[0-9A-F]{4}(([0-9A-F]{2})(00|20){0,12}(00|01|04|08)?)?$", # 00-0004-FE
544
- RQ: r"^(00|01|21)$",
548
+ 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
549
+ RQ: r"^(00|01|15|16|17|21)$",
545
550
  },
546
551
  Code._31DA: { # hvac_state (fan_state_extended)
547
552
  SZ_NAME: "hvac_state",
548
- I_: r"^(00|01|21)[0-9A-F]{56}(00|20)?$",
549
- RQ: r"^(00|01|21)$"
553
+ I_: r"^(00|01|15|16|17|21)[0-9A-F]{56}(00|20)?$",
554
+ RQ: r"^(00|01|15|16|17|21)$",
550
555
  # RQ --- 32:168090 30:082155 --:------ 31DA 001 21
551
556
  },
552
557
  Code._31E0: { # fan_demand
@@ -556,12 +561,12 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
556
561
  SZ_NAME: "fan_demand",
557
562
  I_: r"^00([0-9A-F]{4}){1,3}(00|FF)?$",
558
563
  },
559
- Code._3200: { # boiler output temp
564
+ Code._3200: { # boiler (or CV?) output temp
560
565
  SZ_NAME: "boiler_output",
566
+ I_: r"^00[0-9A-F]{4}$",
561
567
  RQ: r"^00$",
562
- RP: r"^00[0-9A-F]{4}$",
563
568
  },
564
- Code._3210: { # boiler return temp
569
+ Code._3210: { # boiler (or CV?) return temp
565
570
  SZ_NAME: "boiler_return",
566
571
  RQ: r"^00$",
567
572
  RP: r"^00[0-9A-F]{4}$",
@@ -579,7 +584,7 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
579
584
  Code._3222: { # unknown_3222, HVAC, NB: no I
580
585
  SZ_NAME: "unknown_3222",
581
586
  RQ: r"^00$",
582
- RP: r"^00[0-9A-F]{4,20}$",
587
+ RP: r"^00[0-9A-F]{4,24}$",
583
588
  },
584
589
  Code._3223: { # unknown_3223, from OTB
585
590
  SZ_NAME: "message_3223",
@@ -618,30 +623,45 @@ CODES_SCHEMA: dict[Code, dict] = { # rf_unknown
618
623
  RQ: r"^[0-9A-F]{40}$",
619
624
  W_: r"^[0-9A-F]{40}$",
620
625
  },
621
- Code._4E01: { # hvac_4e01 - HVAC
626
+ Code._4E01: { # xxx (HVAC) - Itho Spider
622
627
  SZ_NAME: "hvac_4e01",
623
- I_: r"^00([0-9A-F]{4}){8}00$",
628
+ I_: r"^00([0-9A-F]{4}){3,12}00$",
624
629
  },
625
- Code._4E02: { # hvac_4e02 - HVAC
630
+ Code._4E02: { # xxx (HVAC) - Itho Spider
626
631
  SZ_NAME: "hvac_4e02",
627
- I_: r"^00([0-9A-F]{4}){8}02([0-9A-F]{4}){8}$",
632
+ I_: r"^00([0-9A-F]{4}){3,12}(02|03|04|05)([0-9A-F]{4}){3,12}$",
628
633
  },
629
- Code._4E04: { # hvac_4e04 - HVAC
634
+ Code._4E04: { # xxx (HVAC) - Itho Spider
630
635
  SZ_NAME: "hvac_4e04",
631
- I_: r"^00(00FF|01FE)$",
632
- W_: r"^00(00FF|01FE)$",
636
+ I_: r"^00(00|01|02)[0-9A-F]{2}$",
637
+ W_: r"^00(00|01|02)[0-9A-F]{2}$",
638
+ },
639
+ Code._4E0D: { # xxx (HVAC) - Itho Spider
640
+ SZ_NAME: "hvac_4e0d",
641
+ I_: r"^(01|02)(00|01)$",
642
+ },
643
+ Code._4E15: { # xxx (HVAC) - Itho Spider
644
+ SZ_NAME: "hvac_4e15",
645
+ I_: r"^000[0-7]$",
646
+ },
647
+ Code._4E16: { # xxx (HVAC) - Itho Spider
648
+ SZ_NAME: "hvac_4e16",
649
+ I_: r"^00(00){6}$",
633
650
  },
634
651
  Code._PUZZ: {
635
652
  SZ_NAME: "puzzle_packet",
636
653
  I_: r"^00(([0-9A-F]){2})+$",
637
654
  },
638
655
  }
639
- for code in CODES_SCHEMA.values(): # map any RPs to (missing) I_s
656
+ CODE_NAME_LOOKUP = {k: v["name"] for k, v in CODES_SCHEMA.items()}
657
+
658
+
659
+ for code in CODES_SCHEMA.values(): # map any (missing) RPs to I_s
640
660
  if RQ in code and RP not in code and I_ in code:
641
661
  code[RP] = code[I_]
642
662
  #
643
663
  # .I --- 01:210309 --:------ 01:210309 0009 006 FC00FFF900FF
644
- CODES_WITH_ARRAYS: dict[Code, list] = { # 000C/1FC9 are special
664
+ CODES_WITH_ARRAYS: dict[Code, list[int | tuple[str, ...]]] = { # 000C/1FC9 are special
645
665
  Code._0005: [4, ("34",)],
646
666
  Code._0009: [3, ("01", "12", "22")],
647
667
  Code._000A: [6, ("01", "12", "22")], # single element I after a W
@@ -659,6 +679,7 @@ RQ_IDX_COMPLEX: list[Code] = [
659
679
  Code._0016, # optional payload
660
680
  Code._0100, # optional payload
661
681
  Code._0404, # context: index, fragment_idx (fragment_header)
682
+ Code._0418, # context: index
662
683
  Code._10A0, # optional payload
663
684
  Code._1100, # optional payload
664
685
  Code._2309, # optional payload
@@ -673,44 +694,50 @@ RQ_NO_PAYLOAD: list[Code] = [
673
694
  ]
674
695
  RQ_NO_PAYLOAD.extend((Code._0418,))
675
696
 
676
- # IDX_COMPLEX - *usually has* a context, but doesn't satisfy criteria for IDX_SIMPLE:
697
+
698
+ ########################################################################################
699
+ # IDX:_xxxxxx: index (and context)
700
+
677
701
  # all known codes should be in only one of IDX_COMPLEX, IDX_NONE, IDX_SIMPLE
678
- CODE_IDX_COMPLEX: list[Code] = [
702
+
703
+ # IDX_COMPLEX - *usually has* a context, but doesn't satisfy criteria for IDX_SIMPLE:
704
+ CODE_IDX_ARE_COMPLEX: set[Code] = {
679
705
  Code._0005,
680
- Code._000C,
706
+ Code._000C, # idx = fx(payload[0:4])
707
+ # Code._0404, # use "HW" for idx if payload[4:6] == "23" # TODO: should be used
708
+ Code._0418, # log_idx (payload[4:6]) # null RPs are missing an idx
681
709
  Code._1100,
682
- Code._3220,
683
- ] # TODO: 0005 to ..._NONE?
684
- # CODE_IDX_COMPLEX.sort()
710
+ Code._3220, # data_id (payload[4:6])
711
+ } # TODO: 0005 to ..._NONE?
685
712
 
686
713
  # IDX_SIMPLE - *can have* a context, but sometimes not (usu. 00): only ever payload[:2],
687
714
  # either a zone_idx, domain_id or (UFC) circuit_idx (or array of such, i.e. seqx[:2])
688
- CODE_IDX_SIMPLE: list[Code] = [
715
+
716
+ _SIMPLE_IDX = ("^0[0-9A-F]", "^(0[0-9A-F]", "^((0[0-9A-F]", "^(00|01)")
717
+ CODE_IDX_ARE_SIMPLE: set[Code] = {
689
718
  k
690
719
  for k, v in CODES_SCHEMA.items()
691
- if k not in CODE_IDX_COMPLEX
692
- and (
693
- (RQ in v and v[RQ].startswith(("^0[0-9A-F]", "^(0[0-9A-F]")))
694
- or (I_ in v and v[I_].startswith(("^0[0-9A-F]", "^(0[0-9A-F]", "^((0[0-9A-F]")))
695
- )
696
- ]
697
- CODE_IDX_SIMPLE.extend(
698
- (Code._10A0, Code._1260, Code._1F41, Code._22D0, Code._31D9, Code._3B00)
699
- )
700
- # CODE_IDX_SIMPLE.sort()
720
+ for verb in (RQ, I_)
721
+ if k not in CODE_IDX_ARE_COMPLEX and v.get(verb, "").startswith(_SIMPLE_IDX)
722
+ }
723
+ CODE_IDX_ARE_SIMPLE |= {
724
+ Code._22D0,
725
+ Code._2411,
726
+ Code._31D9,
727
+ Code._31DA,
728
+ Code._3B00,
729
+ Code._4E0D,
730
+ }
701
731
 
702
732
  # IDX_NONE - *never has* a context: most payloads start 00, but no context even if the
703
733
  # payload starts with something else (e.g. 2E04)
704
- CODE_IDX_NONE: list[Code] = [
734
+ CODE_IDX_ARE_NONE: set[Code] = {
705
735
  k
706
736
  for k, v in CODES_SCHEMA.items()
707
- if k not in CODE_IDX_COMPLEX + CODE_IDX_SIMPLE
737
+ if k not in CODE_IDX_ARE_COMPLEX | CODE_IDX_ARE_SIMPLE
708
738
  and ((RQ in v and v[RQ][:3] == "^00") or (I_ in v and v[I_][:3] == "^00"))
709
- ]
710
- CODE_IDX_NONE.extend(
711
- (Code._0002, Code._22F1, Code._22F3, Code._2389, Code._2E04, Code._31DA, Code._4401)
712
- ) # 31DA does appear to have an idx?
713
- # CODE_IDX_NONE.sort()
739
+ }
740
+ CODE_IDX_ARE_NONE |= {Code._22F3, Code._2389, Code._2E04, Code._4401}
714
741
 
715
742
  # CODE_IDX_DOMAIN - NOTE: not necc. mutex with other 3
716
743
  CODE_IDX_DOMAIN: dict[Code, str] = {
@@ -723,13 +750,13 @@ CODE_IDX_DOMAIN: dict[Code, str] = {
723
750
  Code._3B00: "^FC",
724
751
  }
725
752
 
753
+
726
754
  #
727
755
  ########################################################################################
728
756
  # CODES_BY_DEV_SLUG - HEAT (CH/DHW) vs HVAC (ventilation)
729
- #
730
-
731
- _DEV_KLASSES_HEAT: dict[str, dict] = {
732
- DEV_TYPE.RFG: { # RFG100: RF to Internet gateway (and others)
757
+ # TODO: 34: can 3220 - split out RND from THM/STA
758
+ _DEV_KLASSES_HEAT: dict[str, dict[Code, dict[VerbT, Any]]] = {
759
+ DevType.RFG: { # RFG100: RF to Internet gateway (and others)
733
760
  Code._0002: {RQ: {}},
734
761
  Code._0004: {I_: {}, RQ: {}},
735
762
  Code._0005: {RQ: {}},
@@ -755,7 +782,7 @@ _DEV_KLASSES_HEAT: dict[str, dict] = {
755
782
  Code._3220: {RQ: {}},
756
783
  Code._3EF0: {RQ: {}},
757
784
  },
758
- DEV_TYPE.CTL: { # e.g. ATC928: Evohome Colour Controller
785
+ DevType.CTL: { # e.g. ATC928: Evohome Colour Controller
759
786
  Code._0001: {W_: {}},
760
787
  Code._0002: {I_: {}, RP: {}},
761
788
  Code._0004: {I_: {}, RP: {}},
@@ -794,7 +821,7 @@ _DEV_KLASSES_HEAT: dict[str, dict] = {
794
821
  Code._3B00: {I_: {}},
795
822
  Code._3EF0: {RQ: {}},
796
823
  },
797
- DEV_TYPE.PRG: { # e.g. HCF82/HCW82: Room Temperature Sensor
824
+ DevType.PRG: { # e.g. HCF82/HCW82: Room Temperature Sensor
798
825
  Code._0009: {I_: {}},
799
826
  Code._1090: {RP: {}},
800
827
  Code._10A0: {RP: {}},
@@ -806,7 +833,7 @@ _DEV_KLASSES_HEAT: dict[str, dict] = {
806
833
  Code._3B00: {I_: {}},
807
834
  Code._3EF1: {RP: {}},
808
835
  },
809
- DEV_TYPE.THM: { # e.g. Generic Thermostat
836
+ DevType.THM: { # e.g. Generic Thermostat
810
837
  Code._0001: {W_: {}},
811
838
  Code._0005: {I_: {}},
812
839
  Code._0008: {I_: {}},
@@ -824,6 +851,7 @@ _DEV_KLASSES_HEAT: dict[str, dict] = {
824
851
  Code._12C0: {I_: {}},
825
852
  Code._1F09: {I_: {}},
826
853
  Code._1FC9: {I_: {}},
854
+ Code._22C9: {W_: {}}, # DT4R
827
855
  Code._2309: {I_: {}, RQ: {}, W_: {}},
828
856
  Code._2349: {RQ: {}, W_: {}},
829
857
  Code._30C9: {I_: {}},
@@ -831,11 +859,12 @@ _DEV_KLASSES_HEAT: dict[str, dict] = {
831
859
  Code._313F: {
832
860
  I_: {}
833
861
  }, # .W --- 30:253184 34:010943 --:------ 313F 009 006000070E0...
862
+ Code._3220: {RP: {}}, # RND (using OT)
834
863
  Code._3B00: {I_: {}},
835
864
  Code._3EF0: {RQ: {}}, # when bound direct to a 13:
836
865
  Code._3EF1: {RQ: {}}, # when bound direct to a 13:
837
866
  },
838
- DEV_TYPE.UFC: { # e.g. HCE80/HCC80: Underfloor Heating Controller
867
+ DevType.UFC: { # e.g. HCE80/HCC80: Underfloor Heating Controller
839
868
  Code._0001: {RP: {}, W_: {}}, # TODO: Ix RP
840
869
  Code._0005: {RP: {}},
841
870
  Code._0008: {I_: {}},
@@ -848,7 +877,7 @@ _DEV_KLASSES_HEAT: dict[str, dict] = {
848
877
  Code._2309: {RP: {}},
849
878
  Code._3150: {I_: {}},
850
879
  },
851
- DEV_TYPE.TRV: { # e.g. HR92/HR91: Radiator Controller
880
+ DevType.TRV: { # e.g. HR92/HR91: Radiator Controller
852
881
  Code._0001: {W_: {r"^0[0-9A-F]"}},
853
882
  Code._0004: {RQ: {r"^0[0-9A-F]00$"}},
854
883
  Code._0016: {RQ: {}, RP: {}},
@@ -865,14 +894,14 @@ _DEV_KLASSES_HEAT: dict[str, dict] = {
865
894
  Code._313F: {RQ: {r"^00$"}},
866
895
  Code._3150: {I_: {r"^0[0-9A-F]{3}$"}},
867
896
  },
868
- DEV_TYPE.DHW: { # e.g. CS92: (DHW) Cylinder Thermostat
897
+ DevType.DHW: { # e.g. CS92: (DHW) Cylinder Thermostat
869
898
  Code._0016: {RQ: {}},
870
899
  Code._1060: {I_: {}},
871
900
  Code._10A0: {RQ: {}}, # This RQ/07/10A0 includes a payload
872
901
  Code._1260: {I_: {}},
873
902
  Code._1FC9: {I_: {}},
874
903
  },
875
- DEV_TYPE.OTB: { # e.g. R8810/R8820: OpenTherm Bridge
904
+ DevType.OTB: { # e.g. R8810/R8820: OpenTherm Bridge
876
905
  Code._0009: {I_: {}}, # 1/24h for a R8820 (not an R8810)
877
906
  Code._0150: {RP: {}}, # R8820A only?
878
907
  Code._042F: {I_: {}, RP: {}},
@@ -903,7 +932,7 @@ _DEV_KLASSES_HEAT: dict[str, dict] = {
903
932
  Code._3EF0: {I_: {}, RP: {}},
904
933
  Code._3EF1: {RP: {}},
905
934
  }, # see: https://www.opentherm.eu/request-details/?post_ids=2944
906
- DEV_TYPE.BDR: { # e.g. BDR91A/BDR91T: Wireless Relay Box
935
+ DevType.BDR: { # e.g. BDR91A/BDR91T: Wireless Relay Box
907
936
  Code._0008: {RP: {}}, # doesn't RP/0009
908
937
  Code._0016: {RP: {}},
909
938
  # Code._10E0: {}, # 13: will not RP/10E0 # TODO: how to indicate that fact here
@@ -916,24 +945,24 @@ _DEV_KLASSES_HEAT: dict[str, dict] = {
916
945
  # RP: {}, # RQ --- 01:145038 13:237335 --:------ 3EF0 001 00
917
946
  Code._3EF1: {RP: {}},
918
947
  },
919
- DEV_TYPE.OUT: {
948
+ DevType.OUT: {
920
949
  Code._0002: {I_: {}},
921
950
  Code._1FC9: {I_: {}},
922
951
  }, # i.e. HB85 (ext. temperature/luminosity(lux)), HB95 (+ wind speed)
923
952
  #
924
- DEV_TYPE.JIM: { # Jasper Interface Module, 08
953
+ DevType.JIM: { # Jasper Interface Module, 08
925
954
  Code._0008: {RQ: {}},
926
955
  Code._10E0: {I_: {}},
927
956
  Code._1100: {I_: {}},
928
957
  Code._3EF0: {I_: {}},
929
958
  Code._3EF1: {RP: {}},
930
959
  },
931
- DEV_TYPE.JST: { # Jasper Stat, 31
960
+ DevType.JST: { # Jasper Stat, 31
932
961
  Code._0008: {I_: {}},
933
962
  Code._10E0: {I_: {}},
934
963
  Code._3EF1: {RQ: {}, RP: {}},
935
964
  },
936
- # DEV_TYPE.RND: { # e.g. TR87RF: Single (round) Zone Thermostat
965
+ # DevType.RND: { # e.g. TR87RF: Single (round) Zone Thermostat
937
966
  # Code._0005: {I_: {}},
938
967
  # Code._0008: {I_: {}},
939
968
  # Code._000A: {I_: {}, RQ: {}},
@@ -953,7 +982,7 @@ _DEV_KLASSES_HEAT: dict[str, dict] = {
953
982
  # Code._3EF0: {I_: {}, RQ: {}}, # when bound direct to a 13:
954
983
  # Code._3EF1: {RQ: {}}, # when bound direct to a 13:
955
984
  # },
956
- # DEV_TYPE.DTS: { # e.g. DTS92(E)
985
+ # DevType.DTS: { # e.g. DTS92(E)
957
986
  # Code._0001: {W_: {}},
958
987
  # Code._0008: {I_: {}},
959
988
  # Code._0009: {I_: {}},
@@ -973,7 +1002,7 @@ _DEV_KLASSES_HEAT: dict[str, dict] = {
973
1002
  # Code._3B00: {I_: {}},
974
1003
  # Code._3EF1: {RQ: {}},
975
1004
  # },
976
- # DEV_TYPE.HCW: { # e.g. HCF82/HCW82: Room Temperature Sensor
1005
+ # DevType.HCW: { # e.g. HCF82/HCW82: Room Temperature Sensor
977
1006
  # Code._0001: {W_: {}},
978
1007
  # Code._0002: {I_: {}},
979
1008
  # Code._0008: {I_: {}},
@@ -988,8 +1017,8 @@ _DEV_KLASSES_HEAT: dict[str, dict] = {
988
1017
  # },
989
1018
  }
990
1019
  # TODO: add 1FC9 everywhere?
991
- _DEV_KLASSES_HVAC: dict[str, dict] = {
992
- DEV_TYPE.DIS: { # Orcon RF15 Display: ?a superset of a REM
1020
+ _DEV_KLASSES_HVAC: dict[str, dict[Code, dict[VerbT, Any]]] = {
1021
+ DevType.DIS: { # Orcon RF15 Display: ?a superset of a REM
993
1022
  Code._0001: {RQ: {}},
994
1023
  Code._042F: {I_: {}},
995
1024
  Code._10E0: {I_: {}, RQ: {}},
@@ -1004,7 +1033,7 @@ _DEV_KLASSES_HVAC: dict[str, dict] = {
1004
1033
  Code._313F: {RQ: {}},
1005
1034
  Code._31DA: {RQ: {}},
1006
1035
  },
1007
- DEV_TYPE.RFS: { # Itho spIDer: RF to Internet gateway (like a RFG100)
1036
+ DevType.RFS: { # Itho spIDer: RF to Internet gateway (like a RFG100)
1008
1037
  Code._1060: {I_: {}},
1009
1038
  Code._10E0: {I_: {}, RP: {}},
1010
1039
  Code._12C0: {I_: {}},
@@ -1019,8 +1048,9 @@ _DEV_KLASSES_HVAC: dict[str, dict] = {
1019
1048
  Code._31DA: {RQ: {}},
1020
1049
  Code._3EF0: {I_: {}},
1021
1050
  },
1022
- DEV_TYPE.FAN: {
1051
+ DevType.FAN: {
1023
1052
  Code._0001: {RP: {}},
1053
+ Code._0002: {I_: {}},
1024
1054
  Code._042F: {I_: {}},
1025
1055
  Code._10D0: {I_: {}, RP: {}},
1026
1056
  Code._10E0: {I_: {}, RP: {}},
@@ -1030,26 +1060,40 @@ _DEV_KLASSES_HVAC: dict[str, dict] = {
1030
1060
  Code._1470: {RP: {}},
1031
1061
  Code._1F09: {I_: {}, RP: {}},
1032
1062
  Code._1FC9: {W_: {}},
1063
+ Code._2210: {I_: {}, RP: {}},
1064
+ Code._22E0: {RP: {}},
1065
+ Code._22E5: {RP: {}},
1066
+ Code._22E9: {RP: {}},
1067
+ Code._22F1: {RP: {}},
1068
+ Code._22F2: {I_: {}, RP: {}},
1069
+ Code._22F3: {},
1070
+ Code._22F4: {I_: {}, RP: {}},
1033
1071
  Code._22F7: {I_: {}, RP: {}},
1034
1072
  Code._2411: {I_: {}, RP: {}},
1073
+ Code._2E10: {I_: {}},
1035
1074
  Code._3120: {I_: {}},
1075
+ Code._3150: {I_: {}},
1076
+ Code._313E: {RP: {}},
1036
1077
  Code._313F: {I_: {}, RP: {}},
1037
1078
  Code._31D9: {I_: {}, RP: {}},
1038
1079
  Code._31DA: {I_: {}, RP: {}},
1039
1080
  # Code._31E0: {I_: {}},
1081
+ Code._3200: {I_: {}},
1082
+ Code._3222: {RP: {}},
1040
1083
  },
1041
- DEV_TYPE.CO2: {
1084
+ DevType.CO2: {
1042
1085
  Code._042F: {I_: {}},
1043
1086
  Code._10E0: {I_: {}, RP: {}},
1044
- Code._1298: {I_: {}},
1087
+ Code._1298: {I_: {}, RP: {}},
1045
1088
  Code._1FC9: {I_: {}},
1089
+ Code._22F1: {RQ: {}},
1046
1090
  Code._2411: {RQ: {}},
1047
1091
  Code._2E10: {I_: {}},
1048
1092
  Code._3120: {I_: {}},
1049
1093
  Code._31DA: {RQ: {}},
1050
1094
  Code._31E0: {I_: {}},
1051
1095
  },
1052
- DEV_TYPE.HUM: {
1096
+ DevType.HUM: {
1053
1097
  Code._042F: {I_: {}},
1054
1098
  Code._1060: {I_: {}},
1055
1099
  Code._10E0: {I_: {}},
@@ -1058,10 +1102,15 @@ _DEV_KLASSES_HVAC: dict[str, dict] = {
1058
1102
  Code._31DA: {RQ: {}},
1059
1103
  Code._31E0: {I_: {}},
1060
1104
  },
1061
- DEV_TYPE.REM: { # HVAC: two-way switch; also an "06/22F1"?
1105
+ DevType.REM: { # HVAC: two-way switch; also an "06/22F1"?
1062
1106
  Code._0001: {RQ: {}}, # from a VMI (only?)
1063
1107
  Code._042F: {I_: {}}, # from a VMI (only?)
1064
1108
  Code._1060: {I_: {}},
1109
+ Code._10D0: {
1110
+ RP: {},
1111
+ RQ: {},
1112
+ W_: {},
1113
+ }, # RQ/RP Orcon HRC, W=reset filter count from REM
1065
1114
  Code._10E0: {I_: {}, RQ: {}}, # RQ from a VMI (only?)
1066
1115
  Code._1470: {RQ: {}}, # from a VMI (only?)
1067
1116
  Code._1FC9: {I_: {}},
@@ -1078,8 +1127,8 @@ _DEV_KLASSES_HVAC: dict[str, dict] = {
1078
1127
  # },
1079
1128
  }
1080
1129
 
1081
- CODES_BY_DEV_SLUG: dict[str, dict] = {
1082
- DEV_TYPE.HGI: { # HGI80: RF to (USB) serial gateway interface
1130
+ CODES_BY_DEV_SLUG: dict[str, dict[Code, dict[VerbT, Any]]] = {
1131
+ DevType.HGI: { # HGI80: RF to (USB) serial gateway interface
1083
1132
  Code._PUZZ: {I_: {}, RQ: {}, W_: {}},
1084
1133
  }, # HGI80s can do what they like
1085
1134
  **{k: v for k, v in _DEV_KLASSES_HVAC.items() if k is not None},
@@ -1087,11 +1136,10 @@ CODES_BY_DEV_SLUG: dict[str, dict] = {
1087
1136
  }
1088
1137
 
1089
1138
  CODES_OF_HEAT_DOMAIN: tuple[Code] = sorted( # type: ignore[assignment]
1090
- tuple(set(c for k in _DEV_KLASSES_HEAT.values() for c in k))
1091
- + (Code._0B04, Code._2389)
1139
+ tuple({c for k in _DEV_KLASSES_HEAT.values() for c in k}) + (Code._0B04, Code._2389)
1092
1140
  )
1093
1141
  CODES_OF_HVAC_DOMAIN: tuple[Code] = sorted( # type: ignore[assignment]
1094
- tuple(set(c for k in _DEV_KLASSES_HVAC.values() for c in k))
1142
+ tuple({c for k in _DEV_KLASSES_HVAC.values() for c in k})
1095
1143
  + (Code._22F8, Code._4401, Code._4E01, Code._4E02, Code._4E04)
1096
1144
  )
1097
1145
  CODES_OF_HEAT_DOMAIN_ONLY: tuple[Code, ...] = tuple(
@@ -1100,27 +1148,27 @@ CODES_OF_HEAT_DOMAIN_ONLY: tuple[Code, ...] = tuple(
1100
1148
  CODES_OF_HVAC_DOMAIN_ONLY: tuple[Code, ...] = tuple(
1101
1149
  c for c in sorted(CODES_OF_HVAC_DOMAIN) if c not in CODES_OF_HEAT_DOMAIN
1102
1150
  )
1103
- _CODES_OF_BOTH_DOMAINS: tuple[Code, ...] = sorted(
1104
- tuple(set(CODES_OF_HEAT_DOMAIN) & set(CODES_OF_HVAC_DOMAIN))
1151
+ _CODES_OF_BOTH_DOMAINS: tuple[Code, ...] = tuple(
1152
+ sorted(set(CODES_OF_HEAT_DOMAIN) & set(CODES_OF_HVAC_DOMAIN))
1105
1153
  )
1106
- _CODES_OF_EITHER_DOMAIN: tuple[Code, ...] = sorted(
1107
- tuple(set(CODES_OF_HEAT_DOMAIN) | set(CODES_OF_HVAC_DOMAIN))
1154
+ _CODES_OF_EITHER_DOMAIN: tuple[Code, ...] = tuple(
1155
+ sorted(set(CODES_OF_HEAT_DOMAIN) | set(CODES_OF_HVAC_DOMAIN))
1108
1156
  )
1109
1157
  _CODES_OF_NO_DOMAIN: tuple[Code, ...] = tuple(
1110
1158
  c for c in CODES_SCHEMA if c not in _CODES_OF_EITHER_DOMAIN
1111
1159
  )
1112
1160
 
1113
- _CODE_FROM_NON_CTL: tuple[Code] = tuple( # type: ignore[assignment]
1161
+ _CODE_FROM_NON_CTL: tuple[Code, ...] = tuple(
1114
1162
  dict.fromkeys(
1115
1163
  c
1116
1164
  for k, v1 in CODES_BY_DEV_SLUG.items()
1117
1165
  for c, v2 in v1.items()
1118
- if k != DEV_TYPE.CTL and (I_ in v2 or RP in v2)
1166
+ if k != DevType.CTL and (I_ in v2 or RP in v2)
1119
1167
  )
1120
1168
  )
1121
- _CODE_FROM_CTL = _DEV_KLASSES_HEAT[DEV_TYPE.CTL].keys()
1169
+ _CODE_FROM_CTL = _DEV_KLASSES_HEAT[DevType.CTL].keys()
1122
1170
 
1123
- _CODE_ONLY_FROM_CTL: tuple[Code] = tuple( # type: ignore[assignment]
1171
+ _CODE_ONLY_FROM_CTL: tuple[Code, ...] = tuple(
1124
1172
  c for c in _CODE_FROM_CTL if c not in _CODE_FROM_NON_CTL
1125
1173
  )
1126
1174
  CODES_ONLY_FROM_CTL: tuple[Code, ...] = (
@@ -1130,6 +1178,10 @@ CODES_ONLY_FROM_CTL: tuple[Code, ...] = (
1130
1178
  Code._313F,
1131
1179
  ) # I packets, TODO: 31Dx too?
1132
1180
 
1181
+ #
1182
+ ########################################################################################
1183
+ # Other Stuff
1184
+
1133
1185
  # ### WIP:
1134
1186
  # _result = {}
1135
1187
  # for domain in (_DEV_KLASSES_HVAC, ):
@@ -1149,22 +1201,22 @@ CODES_ONLY_FROM_CTL: tuple[Code, ...] = (
1149
1201
  # }
1150
1202
 
1151
1203
 
1152
- _HVAC_VC_PAIR_BY_CLASS: dict[str, tuple] = {
1153
- DEV_TYPE.CO2: ((I_, Code._1298),),
1154
- DEV_TYPE.FAN: ((I_, Code._31D9), (I_, Code._31DA), (RP, Code._31DA)),
1155
- DEV_TYPE.HUM: ((I_, Code._12A0),),
1156
- DEV_TYPE.REM: ((I_, Code._22F1), (I_, Code._22F3)),
1204
+ _HVAC_VC_PAIR_BY_CLASS: dict[DevType, tuple[tuple[VerbT, Code], ...]] = {
1205
+ DevType.CO2: ((I_, Code._1298),),
1206
+ DevType.FAN: ((I_, Code._31D9), (I_, Code._31DA), (RP, Code._31DA)),
1207
+ DevType.HUM: ((I_, Code._12A0),),
1208
+ DevType.REM: ((I_, Code._22F1), (I_, Code._22F3)),
1157
1209
  }
1158
- HVAC_KLASS_BY_VC_PAIR: dict[tuple, str] = {
1210
+ HVAC_KLASS_BY_VC_PAIR: dict[tuple[VerbT, Code], DevType] = {
1159
1211
  t: k for k, v in _HVAC_VC_PAIR_BY_CLASS.items() for t in v
1160
1212
  }
1161
1213
 
1162
1214
 
1163
- SZ_DESCRIPTION = "description"
1164
- SZ_MIN_VALUE = "min_value"
1165
- SZ_MAX_VALUE = "max_value"
1166
- SZ_PRECISION = "precision"
1167
- SZ_DATA_TYPE = "data_type"
1215
+ SZ_DESCRIPTION: Final = "description"
1216
+ SZ_MIN_VALUE: Final = "min_value"
1217
+ SZ_MAX_VALUE: Final = "max_value"
1218
+ SZ_PRECISION: Final = "precision"
1219
+ SZ_DATA_TYPE: Final = "data_type"
1168
1220
 
1169
1221
  _22F1_MODE_ITHO: dict[str, str] = {
1170
1222
  "00": "off", # not seen
@@ -1192,13 +1244,24 @@ _22F1_MODE_ORCON: dict[str, str] = {
1192
1244
  "07": "off",
1193
1245
  }
1194
1246
 
1195
- _22F1_SCHEMES: dict[str, dict] = {
1247
+ _22F1_MODE_VASCO: dict[str, str] = { # for VASCO D60 and ClimaRad Minibox remotes
1248
+ "00": "off",
1249
+ "01": "away", # 000106 minimum
1250
+ "02": "low", # 000206
1251
+ "03": "medium", # 000306
1252
+ "04": "high", # 000406, aka boost with 22F3
1253
+ "05": "auto",
1254
+ }
1255
+
1256
+ _22F1_SCHEMES: dict[str, dict[str, str]] = {
1196
1257
  "itho": _22F1_MODE_ITHO,
1197
1258
  "nuaire": _22F1_MODE_NUAIRE,
1198
1259
  "orcon": _22F1_MODE_ORCON,
1260
+ "vasco": _22F1_MODE_VASCO,
1199
1261
  }
1200
1262
 
1201
- _2411_PARAMS_SCHEMA: dict[str, dict] = { # unclear if true for only Orcon/*all* models
1263
+ # unclear if true for only Orcon/*all* models
1264
+ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
1202
1265
  "31": { # slot 09
1203
1266
  SZ_DESCRIPTION: "Time to change filter (days)",
1204
1267
  SZ_MIN_VALUE: 0,
@@ -1299,11 +1362,25 @@ _2411_PARAMS_SCHEMA: dict[str, dict] = { # unclear if true for only Orcon/*all*
1299
1362
  },
1300
1363
  }
1301
1364
 
1365
+ # ventilation speed description
1366
+ _31D9_FAN_INFO_VASCO: dict[int, str] = {
1367
+ 0x00: "off",
1368
+ 0x01: "1 (trickle)", # aka low
1369
+ 0x02: "2 (low)", # aka medium
1370
+ 0x03: "3 (medium)", # aka high
1371
+ 0x04: "4 (boost)",
1372
+ 0x05: "auto",
1373
+ 0xC8: "III (boost)", # same code sent for speed II and III, mode manual
1374
+ 0x50: "I (low)",
1375
+ 0x1E: "0 (very low)",
1376
+ }
1377
+
1378
+ # ventilation speed
1302
1379
  _31DA_FAN_INFO: dict[int, str] = {
1303
1380
  0x00: "off",
1304
- 0x01: "speed 1", # low
1305
- 0x02: "speed 2", # medium
1306
- 0x03: "speed 3", # high
1381
+ 0x01: "speed 1, low", # aka low
1382
+ 0x02: "speed 2, medium", # aka medium
1383
+ 0x03: "speed 3, high", # aka high
1307
1384
  0x04: "speed 4",
1308
1385
  0x05: "speed 5",
1309
1386
  0x06: "speed 6",
@@ -1311,8 +1388,8 @@ _31DA_FAN_INFO: dict[int, str] = {
1311
1388
  0x08: "speed 8",
1312
1389
  0x09: "speed 9",
1313
1390
  0x0A: "speed 10",
1314
- 0x0B: "speed 1 temporary override",
1315
- 0x0C: "speed 2 temporary override",
1391
+ 0x0B: "speed 1 temporary override", # timer
1392
+ 0x0C: "speed 2 temporary override", # timer
1316
1393
  0x0D: "speed 3 temporary override", # timer/boost? (timer 1, 2, 3)
1317
1394
  0x0E: "speed 4 temporary override",
1318
1395
  0x0F: "speed 5 temporary override",
@@ -1321,17 +1398,17 @@ _31DA_FAN_INFO: dict[int, str] = {
1321
1398
  0x12: "speed 8 temporary override",
1322
1399
  0x13: "speed 9 temporary override",
1323
1400
  0x14: "speed 10 temporary override",
1324
- 0x15: "away",
1401
+ 0x15: "away", # absolute minimum speed
1325
1402
  0x16: "absolute minimum", # trickle?
1326
- 0x17: "absolute maximum", # boost?
1403
+ 0x17: "boost", # absolute maximum", # boost?
1327
1404
  0x18: "auto",
1328
- 0x19: "auto night",
1405
+ 0x19: "auto_night",
1329
1406
  0x1A: "-unknown 0x1A-",
1330
1407
  0x1B: "-unknown 0x1B-",
1331
1408
  0x1C: "-unknown 0x1C-",
1332
1409
  0x1D: "-unknown 0x1D-",
1333
1410
  0x1E: "-unknown 0x1E-",
1334
- 0x1F: "-unknown 0x1F-",
1411
+ 0x1F: "-unknown 0x1F-", # static field, used as filter in parser_31da so keep same
1335
1412
  }
1336
1413
 
1337
1414