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

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 

6from __future__ import annotations 

7 

8import struct 

9from collections.abc import Callable 

10from enum import EnumCheck, IntEnum, StrEnum, verify 

11from 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 

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 

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 

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} 

196 

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) 

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 

218READ_WRITE: Final = "RW" 

219READ_ONLY: Final = "R-" 

220WRITE_ONLY: Final = "-W" 

221 

222EN: Final = "en" 

223FLAGS: Final = "flags" 

224DIR: Final = "dir" 

225NL: Final = "nl" 

226SENSOR: Final = "sensor" 

227VAL: Final = "val" 

228VAR: Final = "var" 

229 

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) 

238 

239HB: Final = "hb" 

240LB: Final = "lb" 

241 

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}" 

250 

251 

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)" 

263 

264 

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" 

275 

276 

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} 

287 

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" 

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) 

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} 

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 

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 

1042 

1043 

1044def _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... 

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]] 

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]) 

1136def 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"""