Coverage for src/ramses_tx/opentherm.py: 63%

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