ramses-rf 0.22.40__py3-none-any.whl → 0.51.2__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 +279 -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.2.dist-info/METADATA +72 -0
  29. ramses_rf-0.51.2.dist-info/RECORD +55 -0
  30. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.2.dist-info}/WHEEL +1 -2
  31. ramses_rf-0.51.2.dist-info/entry_points.txt +2 -0
  32. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.2.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
ramses_tx/opentherm.py ADDED
@@ -0,0 +1,1260 @@
1
+ #!/usr/bin/env python3
2
+ """RAMSES RF - Opentherm processor."""
3
+
4
+ # TODO: a fnc to translate OT flags into a list of strs
5
+
6
+ from __future__ import annotations
7
+
8
+ import struct
9
+ from collections.abc import Callable
10
+ from enum import EnumCheck, IntEnum, StrEnum, verify
11
+ from typing import Any, Final, TypeAlias
12
+
13
+ _DataValueT: TypeAlias = float | int | list[int] | str | None
14
+ _FrameT: TypeAlias = str
15
+ _MsgStrT: TypeAlias = str
16
+
17
+
18
+ _FlagsSchemaT: TypeAlias = dict[int, dict[str, str]]
19
+ _OtMsgSchemaT: TypeAlias = dict[str, Any]
20
+
21
+
22
+ class OtDataId(IntEnum): # the subset of data-ids used by the OTB
23
+ STATUS = 0x00
24
+ CONTROL_SETPOINT = 0x01
25
+ MASTER_CONFIG = 0x02
26
+ SLAVE_CONFIG = 0x03
27
+ OEM_FAULTS = 0x05
28
+ REMOTE_FLAGS = 0x06
29
+ ROOM_OVERRIDE = 0x09
30
+ # TSP_NUMBER = 0x0A
31
+ # FHB_SIZE = 0x0C
32
+ # FHB_ENTRY = 0x0D
33
+ ROOM_SETPOINT = 0x10
34
+ REL_MODULATION_LEVEL = 0x11
35
+ CH_WATER_PRESSURE = 0x12
36
+ DHW_FLOW_RATE = 0x13
37
+ ROOM_TEMP = 0x18
38
+ BOILER_OUTPUT_TEMP = 0x19
39
+ DHW_TEMP = 0x1A
40
+ OUTSIDE_TEMP = 0x1B
41
+ BOILER_RETURN_TEMP = 0x1C
42
+ DHW_BOUNDS = 0x30
43
+ CH_BOUNDS = 0x31
44
+ DHW_SETPOINT = 0x38
45
+ CH_MAX_SETPOINT = 0x39
46
+ BURNER_FAILED_STARTS = 0x71
47
+ FLAME_LOW_SIGNALS = 0x72
48
+ OEM_CODE = 0x73
49
+ BURNER_STARTS = 0x74
50
+ CH_PUMP_STARTS = 0x75
51
+ DHW_PUMP_STARTS = 0x76
52
+ DHW_BURNER_STARTS = 0x77
53
+ BURNER_HOURS = 0x78
54
+ CH_PUMP_HOURS = 0x79
55
+ DHW_PUMP_HOURS = 0x7A
56
+ DHW_BURNER_HOURS = 0x7B
57
+ #
58
+ _00 = 0x00
59
+ _01 = 0x01
60
+ _02 = 0x02
61
+ _03 = 0x03
62
+ _05 = 0x05
63
+ _06 = 0x06
64
+ _09 = 0x09
65
+ _0A = 0x0A
66
+ _0C = 0x0C
67
+ _0D = 0x0D
68
+ _0E = 0x0E
69
+ _0F = 0x0F
70
+ _10 = 0x10
71
+ _11 = 0x11
72
+ _12 = 0x12
73
+ _13 = 0x13
74
+ _18 = 0x18
75
+ _19 = 0x19
76
+ _1A = 0x1A
77
+ _1B = 0x1B
78
+ _1C = 0x1C
79
+ _30 = 0x30
80
+ _31 = 0x31
81
+ _38 = 0x38
82
+ _39 = 0x39
83
+ _71 = 0x71
84
+ _72 = 0x72
85
+ _73 = 0x73
86
+ _74 = 0x74
87
+ _75 = 0x75
88
+ _76 = 0x76
89
+ _77 = 0x77
90
+ _78 = 0x78
91
+ _79 = 0x79
92
+ _7A = 0x7A
93
+ _7B = 0x7B
94
+ _7C = 0x7C
95
+ _7D = 0x7D
96
+ _7E = 0x7E
97
+ _7F = 0x7F
98
+
99
+
100
+ _OtDataIdT: TypeAlias = OtDataId # | int
101
+
102
+ # grep -E 'RP.* 34:.* 30:.* 3220 ' | grep -vE ' 005 00..(01 |05| |11|12|13|19|1A|1C |73 )' returns no results
103
+ # grep -E 'RP.* 10:.* 01:.* 3220 ' | grep -vE ' 005 00..( 03|05|0F|11|12|13|19|1A|1C|38|39|71|72|73|74|75|76|77|78|79|7A|7B|7F)' returns no results
104
+
105
+ # These are R8810A/R8820A-supported msg_ids and their descriptions
106
+ SCHEMA_DATA_IDS: Final[dict[_OtDataIdT, _MsgStrT]] = {
107
+ OtDataId._03: "Slave configuration", # . # 3
108
+ # 003:HB0: Slave configuration: DHW present
109
+ # 003:HB1: Slave configuration: Control type
110
+ # 003:HB4: Slave configuration: Master low-off & pump control
111
+ #
112
+ OtDataId._06: "Remote boiler parameter flags", # . # 6
113
+ # 006:HB0: Remote boiler parameter transfer-enable: DHW setpoint
114
+ # 006:HB1: Remote boiler parameter transfer-enable: max. CH setpoint
115
+ # 006:LB0: Remote boiler parameter read/write: DHW setpoint
116
+ # 006:LB1: Remote boiler parameter read/write: max. CH setpoint,
117
+ #
118
+ OtDataId._7F: "Slave product version number and type", # . # 127
119
+ #
120
+ # TODO: deprecate 71-2, 74-7B, as appears that always value=None
121
+ # # These are STATUS seen RQ'd by 01:/30:, but here to retrieve less frequently
122
+ # 0x71: "Number of un-successful burner starts", # . # 113
123
+ # 0x72: "Number of times flame signal was too low", # . # 114
124
+ # 0x74: "Number of starts burner", # . # 116
125
+ # 0x75: "Number of starts central heating pump", # . # 117
126
+ # 0x76: "Number of starts DHW pump/valve", # . # 118
127
+ # 0x77: "Number of starts burner during DHW mode", # . # 119
128
+ # 0x78: "Number of hours burner is in operation (i.e. flame on)", # . # 120
129
+ # 0x79: "Number of hours central heating pump has been running", # . # 121
130
+ # 0x7A: "Number of hours DHW pump has been running/valve has been opened", # . # 122
131
+ # 0x7B: "Number of hours DHW burner is in operation during DHW mode", # . # 123
132
+ }
133
+ PARAMS_DATA_IDS: Final[dict[_OtDataIdT, _MsgStrT]] = {
134
+ OtDataId._0E: "Maximum relative modulation level setting (%)", # . # 14
135
+ OtDataId._0F: "Max. boiler capacity (kW) and modulation level setting (%)", # . # 15
136
+ OtDataId._30: "DHW Setpoint upper & lower bounds for adjustment (°C)", # . # 48
137
+ OtDataId._31: "Max CH water Setpoint upper & lower bounds for adjustment (°C)", # . # 49
138
+ OtDataId._38: "DHW Setpoint (°C) (Remote parameter 1)", # see: 0x06, is R/W # 56
139
+ OtDataId._39: "Max CH water Setpoint (°C) (Remote parameter 2)", # see: 0x06, is R/W # 57
140
+ }
141
+ STATUS_DATA_IDS: Final[dict[_OtDataIdT, _MsgStrT]] = {
142
+ OtDataId._00: "Master/Slave status flags", # . # 0
143
+ # 000:HB0: Master status: CH enable
144
+ # 000:HB1: Master status: DHW enable
145
+ # 000:HB2: Master status: Cooling enable
146
+ # 000:HB3: Master status: OTC active
147
+ # 000:HB5: Master status: Summer/winter mode
148
+ # 000:HB6: Master status: DHW blocking
149
+ # 000:LB0: Slave Status: Fault indication
150
+ # 000:LB1: Slave Status: CH mode
151
+ # 000:LB2: Slave Status: DHW mode
152
+ # 000:LB3: Slave Status: Flame status
153
+ #
154
+ OtDataId._01: "CH water temperature Setpoint (°C)", # NOTE: is W only! # 1
155
+ OtDataId._11: "Relative Modulation Level (%)", # . # 17
156
+ OtDataId._12: "Water pressure in CH circuit (bar)", # . # 18
157
+ OtDataId._13: "Water flow rate in DHW circuit. (L/min)", # . # 19
158
+ OtDataId._18: "Room temperature (°C)", # . # 24
159
+ OtDataId._19: "Boiler flow water temperature (°C)", # . # 25
160
+ OtDataId._1A: "DHW temperature (°C)", # . # 26
161
+ OtDataId._1B: "Outside temperature (°C)", # TODO: any value here? # is R/W # 27
162
+ OtDataId._1C: "Return water temperature (°C)", # . # 28
163
+ #
164
+ # These are error/state codes...
165
+ OtDataId._05: "Fault flags & OEM codes", # . # 5
166
+ # 005:HB0: Service request
167
+ # 005:HB1: Lockout-reset
168
+ # 005:HB2: Low water pressure
169
+ # 005:HB3: Gas/flame fault
170
+ # 005:HB4: Air pressure fault
171
+ # 005:HB5: Water over-temperature
172
+ # 005:LB: OEM fault code
173
+ #
174
+ OtDataId._73: "OEM diagnostic code", # . # 115
175
+ }
176
+ WRITE_DATA_IDS: Final[
177
+ dict[_OtDataIdT, _MsgStrT]
178
+ ] = { # Write-Data, NB: some are also Read-Data
179
+ OtDataId._01: "CH water temperature Setpoint (°C)",
180
+ # 001: Control Setpoint i.e. CH water temperature Setpoint (°C)
181
+ #
182
+ OtDataId._02: "Master configuration",
183
+ # 002:HB0: Master configuration: Smart power
184
+ # 002:LB: Master MemberID code
185
+ #
186
+ OtDataId._09: "Remote override room Setpoint", # c.f. 0x64, 100 # 9
187
+ OtDataId._0E: "Maximum relative modulation level setting (%)", # c.f. 0x11 # 14
188
+ OtDataId._10: "Room Setpoint (°C)", # . # 16
189
+ OtDataId._18: "Room temperature (°C)", # . # 24
190
+ OtDataId._1B: "Outside temperature (°C)", # . # 27
191
+ OtDataId._38: "DHW Setpoint (°C) (Remote parameter 1)", # . # is R/W # 56
192
+ OtDataId._39: "Max CH water Setpoint (°C) (Remote parameters 2)", # is R/W # 57
193
+ OtDataId._7C: "Opentherm version Master", # . # is R/W # 124
194
+ OtDataId._7E: "Master product version number and type", # . # 126
195
+ }
196
+
197
+ OTB_DATA_IDS: Final[dict[_OtDataIdT, _MsgStrT]] = (
198
+ SCHEMA_DATA_IDS
199
+ | PARAMS_DATA_IDS
200
+ | STATUS_DATA_IDS
201
+ | WRITE_DATA_IDS
202
+ | {
203
+ OtDataId._0A: "Number of TSPs supported by slave", # TODO # 10
204
+ OtDataId._0C: "Size of FHB supported by slave", # . TODO # 12
205
+ OtDataId._0D: "FHB Entry", # . TODO # 13
206
+ OtDataId._7D: "Opentherm version Slave", # . TODO # 125
207
+ }
208
+ )
209
+
210
+ # Data structure shamelessy copied, with thanks to @nlrb, from:
211
+ # github.com/nlrb/com.tclcode.otgw (node_modules/otg-api/lib/ot_msg.js),
212
+
213
+ # Other code shamelessy copied, with thanks to @mvn23, from:
214
+ # github.com/mvn23/pyotgw (pyotgw/protocol.py),
215
+
216
+ # Also see:
217
+ # github.com/rvdbreemen/OTGW-firmware
218
+ READ_WRITE: Final = "RW"
219
+ READ_ONLY: Final = "R-"
220
+ WRITE_ONLY: Final = "-W"
221
+
222
+ EN: Final = "en"
223
+ FLAGS: Final = "flags"
224
+ DIR: Final = "dir"
225
+ NL: Final = "nl"
226
+ SENSOR: Final = "sensor"
227
+ VAL: Final = "val"
228
+ VAR: Final = "var"
229
+
230
+ FLAG8: Final = "flag8"
231
+ FLAG: Final = "flag"
232
+ U8: Final = "u8"
233
+ S8: Final = "s8"
234
+ F8_8: Final = "f8.8"
235
+ U16: Final = "u16"
236
+ S16: Final = "s16"
237
+ SPECIAL: Final[str] = U8 # used for ID 0x14 (20)
238
+
239
+ HB: Final = "hb"
240
+ LB: Final = "lb"
241
+
242
+ SZ_MESSAGES: Final = "messages"
243
+ SZ_DESCRIPTION: Final = "description"
244
+ SZ_MSG_ID: Final = "msg_id"
245
+ SZ_MSG_NAME: Final = "msg_name"
246
+ SZ_MSG_TYPE: Final = "msg_type"
247
+ SZ_VALUE: Final = "value"
248
+ SZ_VALUE_HB: Final[str] = f"{SZ_VALUE}_{HB}"
249
+ SZ_VALUE_LB: Final[str] = f"{SZ_VALUE}_{LB}"
250
+
251
+
252
+ @verify(EnumCheck.UNIQUE)
253
+ class Sensor(StrEnum): # all are F8_8, except COUNTER, CO2_LEVEL
254
+ COUNTER = "counter"
255
+ RATIO = "ratio"
256
+ HUMIDITY = "relative humidity (%)"
257
+ PERCENTAGE = "percentage (%)"
258
+ PRESSURE = "pressure (bar)"
259
+ TEMPERATURE = "temperature (°C)"
260
+ CURRENT = "current (µA)"
261
+ FLOW_RATE = "flow rate (L/min)"
262
+ CO2_LEVEL = "CO2 (ppm)"
263
+
264
+
265
+ @verify(EnumCheck.UNIQUE)
266
+ class OtMsgType(StrEnum):
267
+ READ_DATA = "Read-Data"
268
+ WRITE_DATA = "Write-Data"
269
+ INVALID_DATA = "Invalid-Data"
270
+ RESERVED = "-reserved-"
271
+ READ_ACK = "Read-Ack"
272
+ WRITE_ACK = "Write-Ack"
273
+ DATA_INVALID = "Data-Invalid"
274
+ UNKNOWN_DATAID = "Unknown-DataId"
275
+
276
+
277
+ OPENTHERM_MSG_TYPE: dict[int, OtMsgType] = {
278
+ 0b000: OtMsgType.READ_DATA,
279
+ 0b001: OtMsgType.WRITE_DATA,
280
+ 0b010: OtMsgType.INVALID_DATA,
281
+ 0b011: OtMsgType.RESERVED, # as per Unknown-DataId?
282
+ 0b100: OtMsgType.READ_ACK,
283
+ 0b101: OtMsgType.WRITE_ACK,
284
+ 0b110: OtMsgType.DATA_INVALID, # e.g. sensor fault
285
+ 0b111: OtMsgType.UNKNOWN_DATAID,
286
+ }
287
+
288
+ SZ_STATUS_FLAGS: Final = "status_flags"
289
+ SZ_MASTER_CONFIG_FLAGS: Final = "master_config_flags"
290
+ SZ_SLAVE_CONFIG_FLAGS: Final = "slave_config_flags"
291
+ SZ_FAULT_FLAGS: Final = "fault_flags"
292
+ SZ_REMOTE_FLAGS: Final = "remote_flags"
293
+
294
+
295
+ # OpenTherm status flags [ID 0: Master status (HB) & Slave status (LB)]
296
+ _STATUS_FLAGS: Final[_FlagsSchemaT] = {
297
+ 0x0100: {
298
+ EN: "Central heating enable",
299
+ NL: "Centrale verwarming aan",
300
+ VAR: "StatusCHEnabled",
301
+ }, # CH enabled
302
+ 0x0200: {
303
+ EN: "DHW enable",
304
+ NL: "Tapwater aan",
305
+ VAR: "StatusDHWEnabled",
306
+ }, # DHW enabled
307
+ 0x0400: {
308
+ EN: "Cooling enable",
309
+ NL: "Koeling aan",
310
+ VAR: "StatusCoolEnabled",
311
+ }, # cooling enabled
312
+ 0x0800: {
313
+ EN: "Outside temp. comp. active",
314
+ NL: "Compenseren buitentemp.",
315
+ VAR: "StatusOTCActive",
316
+ }, # OTC active
317
+ 0x1000: {
318
+ EN: "Central heating 2 enable",
319
+ NL: "Centrale verwarming 2 aan",
320
+ VAR: "StatusCH2Enabled",
321
+ }, # CH2 enabled
322
+ 0x2000: {
323
+ EN: "Summer/winter mode",
324
+ NL: "Zomer/winter mode",
325
+ VAR: "StatusSummerWinter",
326
+ }, # summer mode active
327
+ 0x4000: {
328
+ EN: "DHW blocking",
329
+ NL: "Tapwater blokkade",
330
+ VAR: "StatusDHWBlocked",
331
+ }, # DHW is blocking
332
+ 0x0001: {
333
+ EN: "Fault indication",
334
+ NL: "Fout indicatie",
335
+ VAR: "StatusFault",
336
+ }, # fault state
337
+ 0x0002: {
338
+ EN: "Central heating mode",
339
+ NL: "Centrale verwarming mode",
340
+ VAR: "StatusCHMode",
341
+ }, # CH active
342
+ 0x0004: {
343
+ EN: "DHW mode",
344
+ NL: "Tapwater mode",
345
+ VAR: "StatusDHWMode",
346
+ }, # DHW active
347
+ 0x0008: {
348
+ EN: "Flame status",
349
+ NL: "Vlam status",
350
+ VAR: "StatusFlame",
351
+ }, # flame on
352
+ 0x0010: {
353
+ EN: "Cooling status",
354
+ NL: "Status koelen",
355
+ VAR: "StatusCooling",
356
+ }, # cooling active
357
+ 0x0020: {
358
+ EN: "Central heating 2 mode",
359
+ NL: "Centrale verwarming 2 mode",
360
+ VAR: "StatusCH2Mode",
361
+ }, # CH2 active
362
+ 0x0040: {
363
+ EN: "Diagnostic indication",
364
+ NL: "Diagnose indicatie",
365
+ VAR: "StatusDiagnostic",
366
+ }, # diagnostics mode
367
+ }
368
+ # OpenTherm Master configuration flags [ID 2: master config flags (HB)]
369
+ _MASTER_CONFIG_FLAGS: Final[_FlagsSchemaT] = {
370
+ 0x0100: {
371
+ EN: "Smart Power",
372
+ VAR: "ConfigSmartPower",
373
+ },
374
+ }
375
+ # OpenTherm Slave configuration flags [ID 3: slave config flags (HB)]
376
+ _SLAVE_CONFIG_FLAGS: Final[_FlagsSchemaT] = {
377
+ 0x0100: {
378
+ EN: "DHW present",
379
+ VAR: "ConfigDHWpresent",
380
+ },
381
+ 0x0200: {
382
+ EN: "Control type (modulating on/off)",
383
+ VAR: "ConfigControlType",
384
+ },
385
+ 0x0400: {
386
+ EN: "Cooling supported",
387
+ VAR: "ConfigCooling",
388
+ },
389
+ 0x0800: {
390
+ EN: "DHW storage tank",
391
+ VAR: "ConfigDHW",
392
+ },
393
+ 0x1000: {
394
+ EN: "Master low-off & pump control allowed",
395
+ VAR: "ConfigMasterPump",
396
+ },
397
+ 0x2000: {
398
+ EN: "Central heating 2 present",
399
+ VAR: "ConfigCH2",
400
+ },
401
+ }
402
+ # OpenTherm fault flags [ID 5: Application-specific fault flags (HB)]
403
+ _FAULT_FLAGS: Final[_FlagsSchemaT] = {
404
+ 0x0100: {
405
+ EN: "Service request",
406
+ NL: "Onderhoudsvraag",
407
+ VAR: "FaultServiceRequest",
408
+ },
409
+ 0x0200: {
410
+ EN: "Lockout-reset",
411
+ NL: "Geen reset op afstand",
412
+ VAR: "FaultLockoutReset",
413
+ },
414
+ 0x0400: {
415
+ EN: "Low water pressure",
416
+ NL: "Waterdruk te laag", # codespell:ignore te
417
+ VAR: "FaultLowWaterPressure",
418
+ },
419
+ 0x0800: {
420
+ EN: "Gas/flame fault",
421
+ NL: "Gas/vlam fout",
422
+ VAR: "FaultGasFlame",
423
+ },
424
+ 0x1000: {
425
+ EN: "Air pressure fault",
426
+ NL: "Luchtdruk fout",
427
+ VAR: "FaultAirPressure",
428
+ },
429
+ 0x2000: {
430
+ EN: "Water over-temperature",
431
+ NL: "Water te heet", # codespell:ignore te
432
+ VAR: "FaultOverTemperature",
433
+ },
434
+ }
435
+ # OpenTherm remote flags [ID 6: Remote parameter flags (HB)]
436
+ _REMOTE_FLAGS: Final[_FlagsSchemaT] = {
437
+ 0x0100: {
438
+ EN: "DHW setpoint enable",
439
+ VAR: "RemoteDHWEnabled",
440
+ },
441
+ 0x0200: {
442
+ EN: "Max. CH setpoint enable",
443
+ VAR: "RemoteMaxCHEnabled",
444
+ },
445
+ 0x0001: {
446
+ EN: "DHW setpoint read/write",
447
+ VAR: "RemoteDHWReadWrite",
448
+ },
449
+ 0x0002: {
450
+ EN: "Max. CH setpoint read/write",
451
+ VAR: "RemoteMaxCHReadWrite",
452
+ },
453
+ }
454
+ # OpenTherm messages # NOTE: this is used in entity_base.py (traits)
455
+ OPENTHERM_MESSAGES: Final[dict[_OtDataIdT, _OtMsgSchemaT]] = {
456
+ OtDataId._00: { # 0, Status
457
+ EN: "Status",
458
+ DIR: READ_ONLY,
459
+ VAL: {HB: FLAG8, LB: FLAG8},
460
+ FLAGS: SZ_STATUS_FLAGS,
461
+ },
462
+ OtDataId._01: { # 1, Control Setpoint
463
+ EN: "Control setpoint",
464
+ NL: "Ketel doeltemperatuur",
465
+ DIR: WRITE_ONLY,
466
+ VAL: F8_8,
467
+ VAR: "ControlSetpoint",
468
+ SENSOR: Sensor.TEMPERATURE,
469
+ },
470
+ OtDataId._02: { # 2, Master configuration (Member ID)
471
+ EN: "Master configuration",
472
+ DIR: WRITE_ONLY,
473
+ VAL: {HB: FLAG8, LB: U8},
474
+ FLAGS: SZ_MASTER_CONFIG_FLAGS,
475
+ VAR: {LB: "MasterMemberId"},
476
+ },
477
+ OtDataId._03: { # 3, Slave configuration (Member ID)
478
+ EN: "Slave configuration",
479
+ DIR: READ_ONLY,
480
+ VAL: {HB: FLAG8, LB: U8},
481
+ FLAGS: SZ_SLAVE_CONFIG_FLAGS,
482
+ VAR: {LB: "SlaveMemberId"},
483
+ },
484
+ OtDataId._05: { # 5, OEM Fault code
485
+ EN: "Fault flags & OEM fault code",
486
+ DIR: READ_ONLY,
487
+ VAL: {HB: FLAG8, LB: U8},
488
+ VAR: {LB: "OEMFaultCode"},
489
+ FLAGS: SZ_FAULT_FLAGS,
490
+ },
491
+ OtDataId._06: { # 6, Remote Flags
492
+ EN: "Remote parameter flags",
493
+ DIR: READ_ONLY,
494
+ VAL: FLAG8,
495
+ FLAGS: SZ_REMOTE_FLAGS,
496
+ },
497
+ OtDataId._09: { # 9, Remote Override Room Setpoint
498
+ EN: "Remote override room setpoint",
499
+ NL: "Overschreven kamer doeltemperatuur",
500
+ DIR: READ_ONLY,
501
+ VAL: F8_8,
502
+ VAR: "RemoteOverrideRoomSetpoint",
503
+ SENSOR: Sensor.TEMPERATURE,
504
+ },
505
+ OtDataId._0A: { # 10, TSP Number
506
+ EN: "Number of transparent slave parameters supported by slave",
507
+ DIR: READ_ONLY,
508
+ VAL: U8,
509
+ VAR: {HB: "TSPNumber"},
510
+ },
511
+ OtDataId._0C: { # 12, FHB Size
512
+ EN: "Size of fault history buffer supported by slave",
513
+ DIR: READ_ONLY,
514
+ VAL: U8,
515
+ VAR: {HB: "FHBSize"},
516
+ },
517
+ OtDataId._0D: { # 13, FHB Entry
518
+ EN: "Index number/value of referred-to fault history buffer entry",
519
+ DIR: READ_ONLY,
520
+ VAL: U8,
521
+ VAR: {HB: "FHBIndex", LB: "FHBValue"},
522
+ },
523
+ OtDataId._0E: { # 14, Max Relative Modulation Level
524
+ EN: "Max. relative modulation level",
525
+ NL: "Max. relatief modulatie-niveau",
526
+ DIR: WRITE_ONLY,
527
+ VAL: F8_8,
528
+ VAR: "MaxRelativeModulationLevel",
529
+ SENSOR: Sensor.PERCENTAGE,
530
+ },
531
+ OtDataId._0F: { # 15, Max Boiler Capacity & Min Modulation Level
532
+ EN: "Max. boiler capacity (kW) and modulation level setting (%)",
533
+ DIR: READ_ONLY,
534
+ VAL: U8,
535
+ VAR: {HB: "MaxBoilerCapacity", LB: "MinModulationLevel"},
536
+ },
537
+ OtDataId._10: { # 16, Current Setpoint
538
+ EN: "Room setpoint",
539
+ NL: "Kamer doeltemperatuur",
540
+ DIR: WRITE_ONLY,
541
+ VAL: F8_8,
542
+ VAR: "CurrentSetpoint",
543
+ SENSOR: Sensor.TEMPERATURE,
544
+ },
545
+ OtDataId._11: { # 17, Relative Modulation Level
546
+ EN: "Relative modulation level",
547
+ NL: "Relatief modulatie-niveau",
548
+ DIR: READ_ONLY,
549
+ VAL: F8_8,
550
+ VAR: "RelativeModulationLevel",
551
+ SENSOR: Sensor.PERCENTAGE,
552
+ },
553
+ OtDataId._12: { # 18, CH Water Pressure
554
+ EN: "Central heating water pressure (bar)",
555
+ NL: "Keteldruk",
556
+ DIR: READ_ONLY,
557
+ VAL: F8_8,
558
+ VAR: "CHWaterPressure",
559
+ SENSOR: Sensor.PRESSURE,
560
+ },
561
+ OtDataId._13: { # 19, DHW Flow Rate
562
+ EN: "DHW flow rate (litres/minute)",
563
+ DIR: READ_ONLY,
564
+ VAL: F8_8,
565
+ VAR: "DHWFlowRate",
566
+ SENSOR: Sensor.FLOW_RATE,
567
+ },
568
+ OtDataId._18: { # 24, Current Room Temperature
569
+ EN: "Room temperature",
570
+ NL: "Kamertemperatuur",
571
+ DIR: READ_ONLY,
572
+ VAL: F8_8,
573
+ VAR: "CurrentTemperature",
574
+ SENSOR: Sensor.TEMPERATURE,
575
+ },
576
+ OtDataId._19: { # 25, Boiler Water Temperature
577
+ EN: "Boiler water temperature",
578
+ NL: "Ketelwatertemperatuur",
579
+ DIR: READ_ONLY,
580
+ VAL: F8_8,
581
+ VAR: "BoilerWaterTemperature",
582
+ SENSOR: Sensor.TEMPERATURE,
583
+ },
584
+ OtDataId._1A: { # 26, DHW Temperature
585
+ EN: "DHW temperature",
586
+ NL: "Tapwatertemperatuur",
587
+ DIR: READ_ONLY,
588
+ VAL: F8_8,
589
+ VAR: "DHWTemperature",
590
+ SENSOR: Sensor.TEMPERATURE,
591
+ },
592
+ OtDataId._1B: { # 27, Outside Temperature
593
+ EN: "Outside temperature",
594
+ NL: "Buitentemperatuur",
595
+ DIR: READ_ONLY,
596
+ VAL: F8_8,
597
+ VAR: "OutsideTemperature",
598
+ SENSOR: Sensor.TEMPERATURE,
599
+ },
600
+ OtDataId._1C: { # 28, Return Water Temperature
601
+ EN: "Return water temperature",
602
+ NL: "Retourtemperatuur",
603
+ DIR: READ_ONLY,
604
+ VAL: F8_8,
605
+ VAR: "ReturnWaterTemperature",
606
+ SENSOR: Sensor.TEMPERATURE,
607
+ },
608
+ OtDataId._30: { # 48, DHW Boundaries
609
+ EN: "DHW setpoint boundaries",
610
+ DIR: READ_ONLY,
611
+ VAL: S8,
612
+ VAR: {HB: "DHWUpperBound", LB: "DHWLowerBound"},
613
+ SENSOR: Sensor.TEMPERATURE,
614
+ },
615
+ OtDataId._31: { # 49, CH Boundaries
616
+ EN: "Max. central heating setpoint boundaries",
617
+ DIR: READ_ONLY,
618
+ VAL: S8,
619
+ VAR: {HB: "CHUpperBound", LB: "CHLowerBound"},
620
+ SENSOR: Sensor.TEMPERATURE,
621
+ },
622
+ OtDataId._38: { # 56, DHW Setpoint
623
+ EN: "DHW setpoint",
624
+ NL: "Tapwater doeltemperatuur",
625
+ DIR: READ_WRITE,
626
+ VAL: F8_8,
627
+ VAR: "DHWSetpoint",
628
+ SENSOR: Sensor.TEMPERATURE,
629
+ },
630
+ OtDataId._39: { # 57, Max CH Water Setpoint
631
+ EN: "Max. central heating water setpoint",
632
+ NL: "Max. ketel doeltemperatuur",
633
+ DIR: READ_WRITE,
634
+ VAL: F8_8,
635
+ VAR: "MaxCHWaterSetpoint",
636
+ SENSOR: Sensor.TEMPERATURE,
637
+ },
638
+ # OpenTherm 2.2 IDs
639
+ OtDataId._73: { # 115, OEM Diagnostic code
640
+ EN: "OEM diagnostic code",
641
+ DIR: READ_ONLY,
642
+ VAL: U16,
643
+ VAR: "OEMDiagnosticCode",
644
+ },
645
+ OtDataId._74: { # 116, Starts Burner
646
+ EN: "Number of starts burner",
647
+ DIR: READ_WRITE,
648
+ VAL: U16,
649
+ VAR: "StartsBurner",
650
+ SENSOR: Sensor.COUNTER,
651
+ },
652
+ OtDataId._75: { # 117, Starts CH Pump
653
+ EN: "Number of starts central heating pump",
654
+ DIR: READ_WRITE,
655
+ VAL: U16,
656
+ VAR: "StartsCHPump",
657
+ SENSOR: Sensor.COUNTER,
658
+ },
659
+ OtDataId._76: { # 118, Starts DHW Pump
660
+ EN: "Number of starts DHW pump/valve",
661
+ DIR: READ_WRITE,
662
+ VAL: U16,
663
+ VAR: "StartsDHWPump",
664
+ SENSOR: Sensor.COUNTER,
665
+ },
666
+ OtDataId._77: { # 119, Starts Burner DHW
667
+ EN: "Number of starts burner during DHW mode",
668
+ DIR: READ_WRITE,
669
+ VAL: U16,
670
+ VAR: "StartsBurnerDHW",
671
+ SENSOR: Sensor.COUNTER,
672
+ },
673
+ OtDataId._78: { # 120, Hours Burner
674
+ EN: "Number of hours burner is in operation (i.e. flame on)",
675
+ DIR: READ_WRITE,
676
+ VAL: U16,
677
+ VAR: "HoursBurner",
678
+ SENSOR: Sensor.COUNTER,
679
+ },
680
+ OtDataId._79: { # 121, Hours CH Pump
681
+ EN: "Number of hours central heating pump has been running",
682
+ DIR: READ_WRITE,
683
+ VAL: U16,
684
+ VAR: "HoursCHPump",
685
+ SENSOR: Sensor.COUNTER,
686
+ },
687
+ OtDataId._7A: { # 122, Hours DHW Pump
688
+ EN: "Number of hours DHW pump has been running/valve has been opened",
689
+ DIR: READ_WRITE,
690
+ VAL: U16,
691
+ VAR: "HoursDHWPump",
692
+ SENSOR: Sensor.COUNTER,
693
+ },
694
+ OtDataId._7B: { # 123, Hours DHW Burner
695
+ EN: "Number of hours DHW burner is in operation during DHW mode",
696
+ DIR: READ_WRITE,
697
+ VAL: U16,
698
+ VAR: "HoursDHWBurner",
699
+ SENSOR: Sensor.COUNTER,
700
+ },
701
+ OtDataId._7C: { # 124, Master OpenTherm Version
702
+ EN: "Opentherm version Master",
703
+ DIR: WRITE_ONLY,
704
+ VAL: F8_8,
705
+ VAR: "MasterOpenThermVersion",
706
+ },
707
+ OtDataId._7D: { # 125, Slave OpenTherm Version
708
+ EN: "Opentherm version Slave",
709
+ DIR: READ_ONLY,
710
+ VAL: F8_8,
711
+ VAR: "SlaveOpenThermVersion",
712
+ },
713
+ OtDataId._7E: { # 126, Master Product Type/Version
714
+ EN: "Master product version and type",
715
+ DIR: WRITE_ONLY,
716
+ VAL: U8,
717
+ VAR: {HB: "MasterProductType", LB: "MasterProductVersion"},
718
+ },
719
+ OtDataId._7F: { # 127, Slave Product Type/Version
720
+ EN: "Slave product version and type",
721
+ DIR: READ_ONLY,
722
+ VAL: U8,
723
+ VAR: {HB: "SlaveProductType", LB: "SlaveProductVersion"},
724
+ },
725
+ # ZX-DAVB extras
726
+ OtDataId._71: { # 113, Bad Starts Burner
727
+ EN: "Number of un-successful burner starts",
728
+ DIR: READ_WRITE,
729
+ VAL: U16,
730
+ VAR: "BadStartsBurner?",
731
+ SENSOR: Sensor.COUNTER,
732
+ },
733
+ OtDataId._72: { # 114, Low Signals Flame
734
+ EN: "Number of times flame signal was too low",
735
+ DIR: READ_WRITE,
736
+ VAL: U16,
737
+ VAR: "LowSignalsFlame?",
738
+ SENSOR: Sensor.COUNTER,
739
+ },
740
+ }
741
+
742
+ _OPENTHERM_MESSAGES: Final[dict[int, _OtMsgSchemaT]] = {
743
+ 0x04: { # 4, Remote Command
744
+ EN: "Remote command",
745
+ DIR: WRITE_ONLY,
746
+ VAL: U8,
747
+ VAR: "RemoteCommand",
748
+ },
749
+ 0x07: { # 7, Cooling Control Signal
750
+ EN: "Cooling control signal",
751
+ DIR: WRITE_ONLY,
752
+ VAL: F8_8,
753
+ VAR: "CoolingControlSignal",
754
+ SENSOR: Sensor.PERCENTAGE,
755
+ },
756
+ 0x08: { # 8, CH2 Control Setpoint
757
+ EN: "Control setpoint for 2nd CH circuit",
758
+ DIR: WRITE_ONLY,
759
+ VAL: F8_8,
760
+ VAR: "CH2ControlSetpoint",
761
+ SENSOR: Sensor.TEMPERATURE,
762
+ },
763
+ 0x0B: { # 11, TSP Entry
764
+ EN: "Index number/value of referred-to transparent slave parameter",
765
+ DIR: READ_WRITE,
766
+ VAL: U8,
767
+ VAR: {HB: "TSPIndex", LB: "TSPValue"},
768
+ },
769
+ 0x14: { # 20, Day/Time
770
+ EN: "Day of week & Time of day",
771
+ DIR: READ_WRITE,
772
+ VAL: {HB: SPECIAL, LB: U8}, # 1..7/0..23, 0..59
773
+ VAR: {HB: "DayHour", LB: "Minutes"}, # HB7-5: Day, HB4-0: Hour
774
+ },
775
+ 0x15: { # 21, Date
776
+ EN: "Date",
777
+ DIR: READ_WRITE,
778
+ VAL: U8, # 1..12, 1..31
779
+ VAR: {HB: "Month", LB: "DayOfMonth"},
780
+ },
781
+ 0x16: { # 22, Year
782
+ EN: "Year",
783
+ DIR: READ_WRITE,
784
+ VAL: U16, # 1999-2099
785
+ VAR: "Year",
786
+ },
787
+ 0x17: { # 23, CH2 Current Setpoint
788
+ EN: "Room setpoint for 2nd CH circuit",
789
+ DIR: WRITE_ONLY,
790
+ VAL: F8_8,
791
+ VAR: "CH2CurrentSetpoint",
792
+ SENSOR: Sensor.TEMPERATURE,
793
+ },
794
+ 0x1D: { # 29, Solar Storage Temperature
795
+ EN: "Solar storage temperature",
796
+ DIR: READ_ONLY,
797
+ VAL: F8_8,
798
+ VAR: "SolarStorageTemperature",
799
+ SENSOR: Sensor.TEMPERATURE,
800
+ },
801
+ 0x1E: { # 30, Solar Collector Temperature
802
+ EN: "Solar collector temperature",
803
+ DIR: READ_ONLY,
804
+ VAL: F8_8,
805
+ VAR: "SolarCollectorTemperature",
806
+ SENSOR: Sensor.TEMPERATURE,
807
+ },
808
+ 0x1F: { # 31, CH2 Flow Temperature
809
+ EN: "Flow temperature for 2nd CH circuit",
810
+ DIR: READ_ONLY,
811
+ VAL: F8_8,
812
+ VAR: "CH2FlowTemperature",
813
+ SENSOR: Sensor.TEMPERATURE,
814
+ },
815
+ 0x20: { # 32, DHW2 Temperature
816
+ EN: "DHW 2 temperature",
817
+ DIR: READ_ONLY,
818
+ VAL: F8_8,
819
+ VAR: "DHW2Temperature",
820
+ SENSOR: Sensor.TEMPERATURE,
821
+ },
822
+ 0x21: { # 33, Boiler Exhaust Temperature
823
+ EN: "Boiler exhaust temperature",
824
+ DIR: READ_ONLY,
825
+ VAL: S16,
826
+ VAR: "BoilerExhaustTemperature",
827
+ SENSOR: Sensor.TEMPERATURE,
828
+ },
829
+ 0x32: { # 50, OTC Boundaries
830
+ EN: "OTC heat curve ratio upper & lower bounds",
831
+ DIR: READ_ONLY,
832
+ VAL: S8,
833
+ VAR: {HB: "OTCUpperBound", LB: "OTCLowerBound"},
834
+ },
835
+ 0x3A: { # 58, OTC Heat Curve Ratio
836
+ EN: "OTC heat curve ratio",
837
+ DIR: READ_WRITE,
838
+ VAL: F8_8,
839
+ VAR: "OTCHeatCurveRatio",
840
+ SENSOR: Sensor.RATIO,
841
+ },
842
+ # OpenTherm 2.3 IDs (70-91) for ventilation/heat-recovery applications
843
+ 0x46: { # 70, VH Status
844
+ EN: "Status ventilation/heat-recovery",
845
+ DIR: READ_ONLY,
846
+ VAL: FLAG8,
847
+ VAR: "VHStatus",
848
+ },
849
+ 0x47: { # 71, VH Control Setpoint
850
+ EN: "Control setpoint ventilation/heat-recovery",
851
+ DIR: WRITE_ONLY,
852
+ VAL: U8,
853
+ VAR: {HB: "VHControlSetpoint"},
854
+ },
855
+ 0x48: { # 72, VH Fault code
856
+ EN: "Fault flags/code ventilation/heat-recovery",
857
+ DIR: READ_ONLY,
858
+ VAL: {HB: FLAG, LB: U8},
859
+ VAR: {LB: "VHFaultCode"},
860
+ },
861
+ 0x49: { # 73, VH Diagnostic code
862
+ EN: "Diagnostic code ventilation/heat-recovery",
863
+ DIR: READ_ONLY,
864
+ VAL: U16,
865
+ VAR: "VHDiagnosticCode",
866
+ },
867
+ 0x4A: { # 74, VH Member ID
868
+ EN: "Config/memberID ventilation/heat-recovery",
869
+ DIR: READ_ONLY,
870
+ VAL: {HB: FLAG, LB: U8},
871
+ VAR: {LB: "VHMemberId"},
872
+ },
873
+ 0x4B: { # 75, VH OpenTherm Version
874
+ EN: "OpenTherm version ventilation/heat-recovery",
875
+ DIR: READ_ONLY,
876
+ VAL: F8_8,
877
+ VAR: "VHOpenThermVersion",
878
+ },
879
+ 0x4C: { # 76, VH Product Type/Version
880
+ EN: "Version & type ventilation/heat-recovery",
881
+ DIR: READ_ONLY,
882
+ VAL: U8,
883
+ VAR: {HB: "VHProductType", LB: "VHProductVersion"},
884
+ },
885
+ 0x4D: { # 77, Relative Ventilation
886
+ EN: "Relative ventilation",
887
+ DIR: READ_ONLY,
888
+ VAL: U8,
889
+ VAR: {HB: "RelativeVentilation"},
890
+ },
891
+ 0x4E: { # 78, Relative Humidity
892
+ EN: "Relative humidity",
893
+ NL: "Luchtvochtigheid",
894
+ DIR: READ_WRITE,
895
+ VAL: U8,
896
+ VAR: {HB: "RelativeHumidity"},
897
+ SENSOR: Sensor.HUMIDITY,
898
+ },
899
+ 0x4F: { # 79, CO2 Level
900
+ EN: "CO2 level",
901
+ NL: "CO2 niveau",
902
+ DIR: READ_WRITE,
903
+ VAL: U16, # 0-2000 ppm
904
+ VAR: "CO2Level",
905
+ SENSOR: Sensor.CO2_LEVEL,
906
+ },
907
+ 0x50: { # 80, Supply Inlet Temperature
908
+ EN: "Supply inlet temperature",
909
+ DIR: READ_ONLY,
910
+ VAL: F8_8,
911
+ VAR: "SupplyInletTemperature",
912
+ SENSOR: Sensor.TEMPERATURE,
913
+ },
914
+ 0x51: { # 81, Supply Outlet Temperature
915
+ EN: "Supply outlet temperature",
916
+ DIR: READ_ONLY,
917
+ VAL: F8_8,
918
+ VAR: "SupplyOutletTemperature",
919
+ SENSOR: Sensor.TEMPERATURE,
920
+ },
921
+ 0x52: { # 82, Exhaust Inlet Temperature
922
+ EN: "Exhaust inlet temperature",
923
+ DIR: READ_ONLY,
924
+ VAL: F8_8,
925
+ VAR: "ExhaustInletTemperature",
926
+ SENSOR: Sensor.TEMPERATURE,
927
+ },
928
+ 0x53: { # 83, Exhaust Outlet Temperature
929
+ EN: "Exhaust outlet temperature",
930
+ DIR: READ_ONLY,
931
+ VAL: F8_8,
932
+ VAR: "ExhaustOutletTemperature",
933
+ SENSOR: Sensor.TEMPERATURE,
934
+ },
935
+ 0x54: { # 84, Exhaust Fan Speed
936
+ EN: "Actual exhaust fan speed",
937
+ DIR: READ_ONLY,
938
+ VAL: U16,
939
+ VAR: "ExhaustFanSpeed",
940
+ },
941
+ 0x55: { # 85, Inlet Fan Speed
942
+ EN: "Actual inlet fan speed",
943
+ DIR: READ_ONLY,
944
+ VAL: U16,
945
+ VAR: "InletFanSpeed",
946
+ },
947
+ 0x56: { # 86, VH Remote Parameter
948
+ EN: "Remote parameter settings ventilation/heat-recovery",
949
+ DIR: READ_ONLY,
950
+ VAL: FLAG8,
951
+ VAR: "VHRemoteParameter",
952
+ },
953
+ 0x57: { # 87, Nominal Ventilation
954
+ EN: "Nominal ventilation value",
955
+ DIR: READ_WRITE,
956
+ VAL: U8,
957
+ VAR: "NominalVentilation",
958
+ },
959
+ 0x58: { # 88, VH TSP Size
960
+ EN: "TSP number ventilation/heat-recovery",
961
+ DIR: READ_ONLY,
962
+ VAL: U8,
963
+ VAR: {HB: "VHTSPSize"},
964
+ },
965
+ 0x59: { # 89, VH TSP Entry
966
+ EN: "TSP entry ventilation/heat-recovery",
967
+ DIR: READ_WRITE,
968
+ VAL: U8,
969
+ VAR: {HB: "VHTSPIndex", LB: "VHTSPValue"},
970
+ },
971
+ 0x5A: { # 90, VH FHB Size
972
+ EN: "Fault buffer size ventilation/heat-recovery",
973
+ DIR: READ_ONLY,
974
+ VAL: U8,
975
+ VAR: {HB: "VHFHBSize"},
976
+ },
977
+ 0x5B: { # 91, VH FHB Entry
978
+ EN: "Fault buffer entry ventilation/heat-recovery",
979
+ DIR: READ_ONLY,
980
+ VAL: U8,
981
+ VAR: {HB: "VHFHBIndex", LB: "VHFHBValue"},
982
+ },
983
+ # OpenTherm 2.2 IDs
984
+ 0x64: { # 100, Remote Override Function
985
+ EN: "Remote override function",
986
+ DIR: READ_ONLY,
987
+ VAL: {HB: FLAG8, LB: U8},
988
+ VAR: {HB: "RemoteOverrideFunction"},
989
+ },
990
+ # https://www.domoticaforum.eu/viewtopic.php?f=70&t=10893
991
+ # 0x23: { # 35, Boiler Fan Speed (rpm/60?)?
992
+ # },
993
+ 0x24: { # 36, Electrical current through burner flame (µA)
994
+ EN: "Electrical current through burner flame (µA)",
995
+ DIR: READ_ONLY,
996
+ VAL: F8_8,
997
+ VAR: "BurnerCurrent",
998
+ SENSOR: Sensor.CURRENT,
999
+ },
1000
+ 0x25: { # 37, CH2 Room Temperature
1001
+ EN: "Room temperature for 2nd CH circuit",
1002
+ DIR: READ_ONLY,
1003
+ VAL: F8_8,
1004
+ VAR: "CH2CurrentTemperature",
1005
+ SENSOR: Sensor.TEMPERATURE,
1006
+ },
1007
+ 0x26: { # 38, Relative Humidity, c.f. 0x4E
1008
+ EN: "Relative humidity",
1009
+ DIR: READ_ONLY,
1010
+ VAL: U8,
1011
+ VAR: {HB: "RelativeHumidity"}, # TODO: or LB?
1012
+ SENSOR: Sensor.HUMIDITY,
1013
+ },
1014
+ }
1015
+
1016
+ # These must have either a FLAGS (preferred) or a VAR for their message name
1017
+ _OT_FLAG_LOOKUP: Final[dict[str, _FlagsSchemaT]] = {
1018
+ SZ_STATUS_FLAGS: _STATUS_FLAGS,
1019
+ SZ_MASTER_CONFIG_FLAGS: _MASTER_CONFIG_FLAGS,
1020
+ SZ_SLAVE_CONFIG_FLAGS: _SLAVE_CONFIG_FLAGS,
1021
+ SZ_FAULT_FLAGS: _FAULT_FLAGS,
1022
+ SZ_REMOTE_FLAGS: _REMOTE_FLAGS,
1023
+ # SZ_MESSAGES: OPENTHERM_MESSAGES,
1024
+ }
1025
+
1026
+ # R8810A 1018 v4: https://www.opentherm.eu/request-details/?post_ids=2944
1027
+ # as at: 2021/06/28
1028
+
1029
+ # see also: http://otgw.tclcode.com/matrix.cgi#boilers
1030
+ # 0x00, 0x01, 0x03, 0x05, 0x09, 0x0E, 0x10-13, 0x18-1C, 0x38-39, 0x3F, 0x80, 0xFF
1031
+ # personal testing:
1032
+ # 0x00, 0x03, 0x05, 0x06, 0x0C-0D, 0x11-12, 0x19-1A, 0x1C, 0x30-31, 0x38, 0x7D
1033
+
1034
+
1035
+ def parity(x: int) -> int:
1036
+ """Make this the docstring."""
1037
+ shiftamount = 1
1038
+ while x >> shiftamount:
1039
+ x ^= x >> shiftamount
1040
+ shiftamount <<= 1
1041
+ return x & 1
1042
+
1043
+
1044
+ def _msg_value(val_seqx: str, val_type: str) -> _DataValueT:
1045
+ """Make this the docstring."""
1046
+
1047
+ assert len(val_seqx) in (2, 4), f"Invalid value sequence: {val_seqx}"
1048
+
1049
+ # based upon: https://github.com/mvn23/pyotgw/blob/master/pyotgw/protocol.py
1050
+
1051
+ def flag8(byte: str, *args: str) -> list[int]:
1052
+ """Split a byte (as a str) into a list of 8 bits.
1053
+
1054
+ In the original payload (the OT specification), the lsb is bit 0 (the last bit),
1055
+ so the order of bits is reversed here, giving flags[0] (the 1st bit in the
1056
+ array) as the lsb.
1057
+ """
1058
+ assert len(args) == 0 or (len(args) == 1 and args[0] == "")
1059
+ return [(bytes.fromhex(byte)[0] & (1 << x)) >> x for x in range(8)]
1060
+
1061
+ def u8(byte: str, *args: str) -> int:
1062
+ """Convert a byte (as a str) into an unsigned int."""
1063
+ assert len(args) == 0 or (len(args) == 1 and args[0] == "")
1064
+ result = struct.unpack(">B", bytes.fromhex(byte))[0]
1065
+ assert isinstance(result, int) # mypy hint
1066
+ return result
1067
+
1068
+ def s8(byte: str, *args: str) -> int:
1069
+ """Convert a byte (as a str) into a signed int."""
1070
+ assert len(args) == 0 or (len(args) == 1 and args[0] == "")
1071
+ result = struct.unpack(">b", bytes.fromhex(byte))[0]
1072
+ assert isinstance(result, int) # mypy hint
1073
+ return result
1074
+
1075
+ def f8_8(high_byte: str, low_byte: str) -> float:
1076
+ """Convert 2 bytes (as strs) into an OpenTherm f8_8 value."""
1077
+ if high_byte == low_byte == "FF": # TODO: move up to parser?
1078
+ raise ValueError()
1079
+ return float(s16(high_byte, low_byte) / 256)
1080
+
1081
+ def u16(high_byte: str, low_byte: str) -> int:
1082
+ """Convert 2 bytes (as strs) into an unsigned int."""
1083
+ if high_byte == low_byte == "FF": # TODO: move up to parser?
1084
+ raise ValueError()
1085
+ buf = struct.pack(">BB", u8(high_byte), u8(low_byte))
1086
+ return int(struct.unpack(">H", buf)[0])
1087
+
1088
+ def s16(high_byte: str, low_byte: str) -> int:
1089
+ """Convert 2 bytes (as strs) into a signed int."""
1090
+ if high_byte == low_byte == "FF": # TODO: move up to parser?
1091
+ raise ValueError()
1092
+ buf = struct.pack(">bB", s8(high_byte), u8(low_byte))
1093
+ return int(struct.unpack(">h", buf)[0])
1094
+
1095
+ DATA_TYPES: dict[str, Callable[..., _DataValueT]] = {
1096
+ FLAG8: flag8,
1097
+ U8: u8,
1098
+ S8: s8,
1099
+ F8_8: f8_8,
1100
+ U16: u16,
1101
+ S16: s16,
1102
+ }
1103
+
1104
+ # assert not [
1105
+ # k
1106
+ # for k, v in OPENTHERM_MESSAGES.items()
1107
+ # if not isinstance(v[VAL], dict)
1108
+ # and not isinstance(v.get(VAR), dict)
1109
+ # and v[VAL] not in DATA_TYPES
1110
+ # ], "Corrupt OPENTHERM_MESSAGES schema"
1111
+
1112
+ try:
1113
+ fnc = DATA_TYPES[val_type]
1114
+ except KeyError:
1115
+ return val_seqx
1116
+
1117
+ try:
1118
+ result: _DataValueT = fnc(val_seqx[:2], val_seqx[2:])
1119
+ return result
1120
+ except ValueError:
1121
+ return None
1122
+
1123
+
1124
+ # FIXME: this is not finished...
1125
+ def _decode_flags(data_id: OtDataId, flags: str) -> _FlagsSchemaT: # TBA: list[str]:
1126
+ try: # FIXME: don't use _OT_FLAG_LOOKUP
1127
+ flag_schema: _FlagsSchemaT = _OT_FLAG_LOOKUP[OPENTHERM_MESSAGES[data_id][FLAGS]]
1128
+
1129
+ except KeyError as err:
1130
+ raise KeyError(f"Invalid data-id: 0x{data_id}: has no flags") from err
1131
+
1132
+ return flag_schema
1133
+
1134
+
1135
+ # ot_type, ot_id, ot_value, ot_schema = decode_frame(payload[2:10])
1136
+ def decode_frame(
1137
+ frame: _FrameT,
1138
+ ) -> tuple[OtMsgType, OtDataId, dict[str, Any], _OtMsgSchemaT]:
1139
+ """Decode a 3220 payload."""
1140
+
1141
+ if not isinstance(frame, str) or len(frame) != 8:
1142
+ raise TypeError(f"Invalid frame (type or length): {frame}")
1143
+
1144
+ if int(frame[:2], 16) // 0x80 != parity(int(frame, 16) & 0x7FFFFFFF):
1145
+ raise ValueError(f"Invalid parity bit: 0b{int(frame[:2], 16) // 0x80}")
1146
+
1147
+ if int(frame[:2], 16) & 0x0F != 0x00:
1148
+ raise ValueError(f"Invalid spare bits: 0b{int(frame[:2], 16) & 0x0F:04b}")
1149
+
1150
+ msg_type = (int(frame[:2], 16) & 0x70) >> 4
1151
+
1152
+ # if msg_type == 0b011: # NOTE: this msg-type may no longer be reserved (R8820?)
1153
+ # raise ValueError(f"Reserved msg-type (0b{msg_type:03b})")
1154
+
1155
+ data_id: OtDataId = int(frame[2:4], 16) # type: ignore[assignment]
1156
+ try:
1157
+ msg_schema = OPENTHERM_MESSAGES[data_id]
1158
+ except KeyError as err:
1159
+ raise KeyError(f"Unknown data-id: 0x{frame[2:4]} ({data_id})") from err
1160
+
1161
+ # There are five msg_id with FLAGS - the following is not 100% correct...
1162
+ data_value = {SZ_MSG_NAME: msg_schema.get(FLAGS, msg_schema.get(VAR))}
1163
+
1164
+ if msg_type in (0b000, 0b010, 0b011, 0b110, 0b111):
1165
+ # if frame[4:] != "0000": # NOTE: this is not a hard rule, even for 0b000
1166
+ # raise ValueError(f"Invalid data-value for msg-type: 0x{frame[4:]}")
1167
+ return OPENTHERM_MSG_TYPE[msg_type], data_id, data_value, msg_schema
1168
+
1169
+ if not msg_schema: # may be a corrupt payload
1170
+ data_value[SZ_VALUE] = _msg_value(frame[4:8], U16)
1171
+
1172
+ elif isinstance(msg_schema[VAL], dict):
1173
+ value_hb = _msg_value(frame[4:6], msg_schema[VAL].get(HB, msg_schema[VAL]))
1174
+ value_lb = _msg_value(frame[6:8], msg_schema[VAL].get(LB, msg_schema[VAL]))
1175
+
1176
+ if isinstance(value_hb, list) and isinstance(value_lb, list): # FLAG8
1177
+ data_value[SZ_VALUE] = value_hb + value_lb # only data_id 0x00
1178
+ else:
1179
+ data_value[SZ_VALUE_HB] = value_hb
1180
+ data_value[SZ_VALUE_LB] = value_lb
1181
+
1182
+ elif isinstance(msg_schema.get(VAR), dict):
1183
+ data_value[SZ_VALUE_HB] = _msg_value(frame[4:6], msg_schema[VAL])
1184
+ data_value[SZ_VALUE_LB] = _msg_value(frame[6:8], msg_schema[VAL])
1185
+
1186
+ elif msg_schema[VAL] in (FLAG8, U8, S8):
1187
+ data_value[SZ_VALUE] = _msg_value(frame[4:6], msg_schema[VAL])
1188
+
1189
+ elif msg_schema[VAL] in (S16, U16):
1190
+ data_value[SZ_VALUE] = _msg_value(frame[4:8], msg_schema[VAL])
1191
+
1192
+ elif msg_schema[VAL] != F8_8: # shouldn't reach here
1193
+ data_value[SZ_VALUE] = _msg_value(frame[4:8], U16)
1194
+
1195
+ elif msg_schema[VAL] == F8_8: # TODO: needs finishing
1196
+ result: float | None = _msg_value(frame[4:8], msg_schema[VAL]) # type: ignore[assignment]
1197
+
1198
+ if result is None:
1199
+ data_value[SZ_VALUE] = result
1200
+ elif msg_schema.get(SENSOR) == Sensor.PERCENTAGE:
1201
+ # NOTE: OT defines % as 0.0-100.0, but (this) ramses uses 0.0-1.0 elsewhere
1202
+ data_value[SZ_VALUE] = int(result * 2) / 200 # seems precision of 1%
1203
+ elif msg_schema.get(SENSOR) == Sensor.FLOW_RATE:
1204
+ data_value[SZ_VALUE] = int(result * 100) / 100
1205
+ elif msg_schema.get(SENSOR) == Sensor.PRESSURE:
1206
+ data_value[SZ_VALUE] = int(result * 10) / 10
1207
+ else: # if msg_schema.get(SENSOR) == (Sensor.TEMPERATURE, Sensor.HUMIDITY):
1208
+ data_value[SZ_VALUE] = int(result * 100) / 100
1209
+
1210
+ return OPENTHERM_MSG_TYPE[msg_type], data_id, data_value, msg_schema
1211
+
1212
+
1213
+ # https://github.com/rvdbreemen/OTGW-firmware/blob/main/Specification/New%20OT%20data-ids.txt # noqa: E501
1214
+
1215
+ """
1216
+ New OT Data-ID's - Found two new ID's at this device description:
1217
+ http://www.opentherm.eu/product/view/18/feeling-d201-ot
1218
+ ID 98: For a specific RF sensor the RF strength and battery level is written
1219
+ ID 99: Operating Mode HC1, HC2/ Operating Mode DHW
1220
+
1221
+ Found new data-id's at this page:
1222
+ https://www.opentherm.eu/request-details/?post_ids=1833
1223
+ ID 109: Electricity producer starts
1224
+ ID 110: Electricity producer hours
1225
+ ID 111: Electricity production
1226
+ ID 112: Cumulative Electricity production
1227
+
1228
+ Found new Data-ID's at this page:
1229
+ https://www.opentherm.eu/request-details/?post_ids=1833
1230
+ ID 36: {f8.8} "Electrical current through burner flame" (µA)
1231
+ ID 37: {f8.8} "Room temperature for 2nd CH circuit"
1232
+ ID 38: {u8 u8} "Relative Humidity"
1233
+
1234
+ For Data-ID's 37 and 38 I assumed their data types, for Data ID 36 I determined
1235
+ it by matching qSense value with the correct data-type.
1236
+
1237
+ I also analysed OT Remeha qSense <-> Remeha Tzerra communication.
1238
+ ID 131: {u8 u8} "Remeha dF-/dU-codes"
1239
+ ID 132: {u8 u8} "Remeha Service message"
1240
+ ID 133: {u8 u8} "Remeha detection connected SCUs"
1241
+
1242
+ "Remeha dF-/dU-codes": Should match the dF-/dU-codes written on boiler nameplate.
1243
+ Read-Data Request (0 0) returns the data. Also accepts Write-Data Requests (dF
1244
+ dU),this returns the boiler to its factory defaults.
1245
+
1246
+ "Remeha Service message" Read-Data Request (0 0), boiler returns (0 2) in case of no
1247
+ boiler service. Write-Data Request (1 255) clears the boiler service message.
1248
+ boiler returns (1 1) = next service type is "A"
1249
+ boiler returns (1 2) = next service type is "B"
1250
+ boiler returns (1 3) = next service type is "C"
1251
+
1252
+ "Remeha detection connected SCUs": Write-Data Request (255 1) enables detection of
1253
+ connected SCU prints, correct response is (Write-Ack 255 1).
1254
+
1255
+ Other Remeha info:
1256
+ ID 5: corresponds with the Remeha E:xx fault codes
1257
+ ID 11: corresponds with the Remeha Pxx parameter codes
1258
+ ID 35: reported value is fan speed in rpm/60
1259
+ ID 115: corresponds with Remeha Status & Sub-status numbers, {u8 u8} data-type
1260
+ """