Coverage for src/ramses_tx/opentherm.py: 63%
249 statements
« prev ^ index » next coverage.py v7.11.3, created at 2026-01-05 21:46 +0100
« prev ^ index » next coverage.py v7.11.3, created at 2026-01-05 21:46 +0100
1#!/usr/bin/env python3
2"""RAMSES RF - Opentherm processor."""
4# TODO: a fnc to translate OT flags into a list of strs
6from __future__ import annotations
8import struct
9from collections.abc import Callable
10from enum import EnumCheck, IntEnum, StrEnum, verify
11from typing import Any, Final, TypeAlias
13_DataValueT: TypeAlias = float | int | list[int] | str | None
14_FrameT: TypeAlias = str
15_MsgStrT: TypeAlias = str
18_FlagsSchemaT: TypeAlias = dict[int, dict[str, str]]
19_OtMsgSchemaT: TypeAlias = dict[str, Any]
22class 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
100_OtDataIdT: TypeAlias = OtDataId # | int
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
105# These are R8810A/R8820A-supported msg_ids and their descriptions
106SCHEMA_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}
133PARAMS_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}
141STATUS_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}
176WRITE_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}
197OTB_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)
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),
213# Other code shamelessy copied, with thanks to @mvn23, from:
214# github.com/mvn23/pyotgw (pyotgw/protocol.py),
216# Also see:
217# github.com/rvdbreemen/OTGW-firmware
218READ_WRITE: Final = "RW"
219READ_ONLY: Final = "R-"
220WRITE_ONLY: Final = "-W"
222EN: Final = "en"
223FLAGS: Final = "flags"
224DIR: Final = "dir"
225NL: Final = "nl"
226SENSOR: Final = "sensor"
227VAL: Final = "val"
228VAR: Final = "var"
230FLAG8: Final = "flag8"
231FLAG: Final = "flag"
232U8: Final = "u8"
233S8: Final = "s8"
234F8_8: Final = "f8.8"
235U16: Final = "u16"
236S16: Final = "s16"
237SPECIAL: Final[str] = U8 # used for ID 0x14 (20)
239HB: Final = "hb"
240LB: Final = "lb"
242SZ_MESSAGES: Final = "messages"
243SZ_DESCRIPTION: Final = "description"
244SZ_MSG_ID: Final = "msg_id"
245SZ_MSG_NAME: Final = "msg_name"
246SZ_MSG_TYPE: Final = "msg_type"
247SZ_VALUE: Final = "value"
248SZ_VALUE_HB: Final[str] = f"{SZ_VALUE}_{HB}"
249SZ_VALUE_LB: Final[str] = f"{SZ_VALUE}_{LB}"
252@verify(EnumCheck.UNIQUE)
253class 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)"
265@verify(EnumCheck.UNIQUE)
266class 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"
277OPENTHERM_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}
288SZ_STATUS_FLAGS: Final = "status_flags"
289SZ_MASTER_CONFIG_FLAGS: Final = "master_config_flags"
290SZ_SLAVE_CONFIG_FLAGS: Final = "slave_config_flags"
291SZ_FAULT_FLAGS: Final = "fault_flags"
292SZ_REMOTE_FLAGS: Final = "remote_flags"
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)
455OPENTHERM_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}
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}
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}
1026# R8810A 1018 v4: https://www.opentherm.eu/request-details/?post_ids=2944
1027# as at: 2021/06/28
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
1035def 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
1044def _msg_value(val_seqx: str, val_type: str) -> _DataValueT:
1045 """Make this the docstring."""
1047 assert len(val_seqx) in (2, 4), f"Invalid value sequence: {val_seqx}"
1049 # based upon: https://github.com/mvn23/pyotgw/blob/master/pyotgw/protocol.py
1051 def flag8(byte: str, *args: str) -> list[int]:
1052 """Split a byte (as a str) into a list of 8 bits.
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)]
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
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
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)
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])
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])
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 }
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"
1112 try:
1113 fnc = DATA_TYPES[val_type]
1114 except KeyError:
1115 return val_seqx
1117 try:
1118 result: _DataValueT = fnc(val_seqx[:2], val_seqx[2:])
1119 return result
1120 except ValueError:
1121 return None
1124# FIXME: this is not finished...
1125def _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]]
1129 except KeyError as err:
1130 raise KeyError(f"Invalid data-id: 0x{data_id}: has no flags") from err
1132 return flag_schema
1135# ot_type, ot_id, ot_value, ot_schema = decode_frame(payload[2:10])
1136def decode_frame(
1137 frame: _FrameT,
1138) -> tuple[OtMsgType, OtDataId, dict[str, Any], _OtMsgSchemaT]:
1139 """Decode a 3220 payload."""
1141 if not isinstance(frame, str) or len(frame) != 8:
1142 raise TypeError(f"Invalid frame (type or length): {frame}")
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}")
1147 if int(frame[:2], 16) & 0x0F != 0x00:
1148 raise ValueError(f"Invalid spare bits: 0b{int(frame[:2], 16) & 0x0F:04b}")
1150 msg_type = (int(frame[:2], 16) & 0x70) >> 4
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})")
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
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))}
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
1169 if not msg_schema: # may be a corrupt payload
1170 data_value[SZ_VALUE] = _msg_value(frame[4:8], U16)
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]))
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
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])
1186 elif msg_schema[VAL] in (FLAG8, U8, S8):
1187 data_value[SZ_VALUE] = _msg_value(frame[4:6], msg_schema[VAL])
1189 elif msg_schema[VAL] in (S16, U16):
1190 data_value[SZ_VALUE] = _msg_value(frame[4:8], msg_schema[VAL])
1192 elif msg_schema[VAL] != F8_8: # shouldn't reach here
1193 data_value[SZ_VALUE] = _msg_value(frame[4:8], U16)
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]
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
1210 return OPENTHERM_MSG_TYPE[msg_type], data_id, data_value, msg_schema
1213# https://github.com/rvdbreemen/OTGW-firmware/blob/main/Specification/New%20OT%20data-ids.txt # noqa: E501
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
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
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"
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.
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"
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.
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"
1252 "Remeha detection connected SCUs": Write-Data Request (255 1) enables detection of
1253 connected SCU prints, correct response is (Write-Ack 255 1).
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"""