Coverage for src/ramses_tx/ramses.py: 100%

53 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2026-01-05 21:44 +0100

1#!/usr/bin/env python3 

2"""RAMSES RF - a RAMSES-II protocol decoder & analyser.""" 

3 

4# TODO: code a lifespan for most packets 

5 

6from __future__ import annotations 

7 

8from datetime import timedelta as td 

9from typing import Any, Final 

10 

11from .const import SZ_NAME, DevType 

12 

13from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import 

14 I_, 

15 RP, 

16 RQ, 

17 W_, 

18 Code, 

19 VerbT, 

20) 

21 

22 

23SZ_LIFESPAN: Final = "lifespan" # WIP 

24 

25 

26# 

27######################################################################################## 

28# CODES_SCHEMA - HEAT (CH/DHW, Honeywell/Resideo) vs HVAC (ventilation, Itho/Orcon/etc.) 

29 

30# The master list - all known codes are here, even if there's no corresponding parser 

31# Anything with a zone-idx should start: ^0[0-9A-F], ^(0[0-9A-F], or ^((0[0-9A-F] 

32 

33# 

34CODES_SCHEMA: dict[Code, dict[str, Any]] = { # rf_unknown 

35 Code._0001: { 

36 SZ_NAME: "rf_unknown", 

37 I_: r"^00FFFF02(00|FF)$", # loopback 

38 RQ: r"^00([28A]0)00(0[0-9A-F])(FF|04)$", # HVAC 

39 RP: r"^00([28A]0)00(0[0-9A-F])", # HVAC 

40 W_: r"^(0[0-9A-F]|FC|FF)000005(01|05)$", 

41 }, # TODO: there appears to be a dodgy? RQ/RP for UFC 

42 Code._0002: { # WIP: outdoor_sensor - CODE_IDX_COMPLEX? 

43 # is it CODE_IDX_COMPLEX: 

44 # - 02...... for outside temp? 

45 # - 03...... for other stuff? 

46 SZ_NAME: "outdoor_sensor", 

47 I_: r"^0[0-4][0-9A-F]{4}(00|01|02|05)$", # Domoticz sends ^02!! 

48 RQ: r"^00$", # NOTE: sent by an RFG100 

49 }, 

50 Code._0004: { # zone_name 

51 SZ_NAME: "zone_name", 

52 I_: r"^0[0-9A-F]00([0-9A-F]){40}$", # RP is same, null_rp: xxxx,7F*20 

53 RQ: r"^0[0-9A-F]00$", 

54 W_: r"^0[0-9A-F]00([0-9A-F]){40}$", # contrived 

55 SZ_LIFESPAN: td(days=1), 

56 }, 

57 Code._0005: { # system_zones 

58 SZ_NAME: "system_zones", 

59 # .I --- 34:092243 --:------ 34:092243 0005 012 000A0000-000F0000-00100000 

60 I_: r"^(00[01][0-9A-F]{5}){1,3}$", 

61 RQ: r"^00[01][0-9A-F]$", # f"00{zone_type}", evohome won't respond to 00 

62 RP: r"^00[01][0-9A-F]{3,5}$", 

63 SZ_LIFESPAN: False, 

64 }, 

65 Code._0006: { # schedule_version # TODO: what for DHW schedule? 

66 SZ_NAME: "schedule_version", 

67 RQ: r"^00$", 

68 RP: r"^0005[0-9A-F]{4}$", 

69 }, 

70 Code._0008: { # relay_demand, TODO: check RP 

71 SZ_NAME: "relay_demand", 

72 # 000 I --- 31:012319 08:006244 --:------ 0008 013 0006958C33CA6ECD2067AA53DD 

73 I_: r"^((0[0-9A-F]|F[9AC])[0-9A-F]{2}|00[0-9A-F]{24})$", 

74 RQ: r"^00$", 

75 RP: r"^00[0-9A-F]{2}$", # seems only 13: RP (TODO: what about 10:, 08/31:) 

76 }, 

77 Code._0009: { # relay_failsafe (only is_controller, OTB send an 0009?) 

78 SZ_NAME: "relay_failsafe", 

79 # .I --- 01:145038 --:------ 01:145038 0009 006 FC01FFF901FF 

80 # .I --- 01:145038 --:------ 01:145038 0009 003 0700FF 

81 # .I --- 10:040239 01:223036 --:------ 0009 003 000000 

82 # .I --- --:------ --:------ 12:227486 0009 003 0000FF 

83 I_: r"^((0[0-9A-F]|F[9AC])0[0-1](00|FF))+$", 

84 }, 

85 Code._000A: { # zone_params 

86 SZ_NAME: "zone_params", 

87 I_: r"^(0[0-9A-F][0-9A-F]{10}){1,8}$", 

88 W_: r"^0[0-9A-F][0-9A-F]{10}$", 

89 RQ: r"^0[0-9A-F]((00)?|([0-9A-F]{10})+)$", # is: r"^0[0-9A-F]([0-9A-F]{10})+$" 

90 RP: r"^0[0-9A-F][0-9A-F]{10}$", # null_rp: xx/007FFF7FFF 

91 # 17:54:13.126 063 RQ --- 34:064023 01:145038 --:------ 000A 001 03 

92 # 17:54:13.141 045 RP --- 01:145038 34:064023 --:------ 000A 006 031002260B86 

93 # 19:20:49.460 062 RQ --- 12:010740 01:145038 --:------ 000A 006 080001F40DAC 

94 # 19:20:49.476 045 RP --- 01:145038 12:010740 --:------ 000A 006 081001F40DAC 

95 SZ_LIFESPAN: td(days=1), 

96 }, 

97 Code._000C: { # zone_devices 

98 SZ_NAME: "zone_devices", 

99 # RP --- 01:145038 18:013393 --:------ 000C 018 06-08-00-1099C3 06-08-00-1099C5 06-08-00-1099BF 

100 # RP --- 01:145038 18:013393 --:------ 000C 016 05-08-00-109901 08-00-109902 08-00-109903 

101 I_: r"^0[0-9A-F][01][0-9A-F]|7F[0-9A-F]{6}([0-9A-F]{10}|[0-9A-F]{12}){1,7}$", 

102 RQ: r"^0[0-9A-F][01][0-9A-F]$", # TODO: f"{zone_idx}{device_type}" 

103 SZ_LIFESPAN: False, 

104 }, 

105 Code._000E: { # unknown_000e 

106 SZ_NAME: "message_000e", 

107 I_: r"^0000(14|28)$", 

108 }, 

109 Code._0016: { # rf_check 

110 SZ_NAME: "rf_check", 

111 RQ: r"^0[0-9A-F]([0-9A-F]{2})?$", # TODO: officially: r"^0[0-9A-F]{3}$" 

112 RP: r"^0[0-9A-F]{3}$", 

113 }, 

114 Code._0100: { # language 

115 SZ_NAME: "language", 

116 RQ: r"^00([0-9A-F]{4}F{4})?$", # NOTE: RQ/04/0100 has a payload 

117 RP: r"^00[0-9A-F]{4}F{4}$", 

118 SZ_LIFESPAN: td(days=1), # TODO: make longer? 

119 }, 

120 Code._0150: { # unknown_0150 

121 SZ_NAME: "message_0150", 

122 RQ: r"^00$", 

123 RP: r"^000000$", 

124 }, 

125 Code._01D0: { # unknown_01d0, TODO: definitely a real code, zone_idx is a guess 

126 SZ_NAME: "message_01d0", 

127 I_: r"^0[0-9A-F][0-9A-F]{2}$", 

128 W_: r"^0[0-9A-F][0-9A-F]{2}$", 

129 # .W --- 04:000722 01:158182 --:------ 01D0 002 0003 # is a guess, the 

130 # .I --- 01:158182 04:000722 --:------ 01D0 002 0003 # TRV was in zone 00 

131 }, 

132 Code._01E9: { # unknown_01e9, TODO: definitely a real code, zone_idx is a guess 

133 SZ_NAME: "message_01e9", 

134 I_: r"^0[0-9A-F][0-9A-F]{2}$", 

135 W_: r"^0[0-9A-F][0-9A-F]{2}$", 

136 # .W --- 04:000722 01:158182 --:------ 01E9 002 0003 # is a guess, the 

137 # .I --- 01:158182 04:000722 --:------ 01E9 002 0000 # TRV was in zone 00 

138 }, 

139 Code._01FF: { # unknown_01ff, TODO: definitely a real code, Itho Spider 

140 SZ_NAME: "message_01ff", 

141 I_: r"^(00|01)[0-9A-F]{50}$", 

142 RQ: r"^(00|01)[0-9A-F]{50}$", 

143 W_: r"^00[0-9A-F]{50}$", 

144 }, 

145 Code._0404: { # zone_schedule 

146 SZ_NAME: "zone_schedule", 

147 I_: r"^0[0-9A-F](20|23)[0-9A-F]{2}08[0-9A-F]{6}$", 

148 RQ: r"^0[0-9A-F](20|23)000800[0-9A-F]{4}$", 

149 RP: r"^0[0-9A-F](20|23)0008[0-9A-F]{6}[0-9A-F]{2,82}$", 

150 W_: r"^0[0-9A-F](20|23)[0-9A-F]{2}08[0-9A-F]{6}[0-9A-F]{2,82}$", # as per RP 

151 SZ_LIFESPAN: None, 

152 }, 

153 Code._0418: { # system_fault 

154 SZ_NAME: "system_fault", 

155 I_: r"^00(00|40|C0)[0-3][0-9A-F]B0[0-9A-F]{6}0000[0-9A-F]{12}FFFF700[012][0-9A-F]{6}$", 

156 RQ: r"^0000[0-3][0-9A-F]$", # f"0000{log_idx}", no payload 

157 }, 

158 Code._042F: { # unknown_042f, # non-evohome are len==9, seen only once? 

159 # .I --- 32:168090 --:------ 32:168090 042F 009 000000100F00105050 

160 # RP --- 10:048122 18:006402 --:------ 042F 009 000200001400163010 

161 SZ_NAME: "message_042f", 

162 I_: r"^00([0-9A-F]{2}){7,8}$", 

163 RQ: r"^00$", 

164 RP: r"^00([0-9A-F]{2}){7,8}$", 

165 }, 

166 Code._0B04: { # unknown_0b04 

167 # .I --- --:------ --:------ 12:207082 0B04 002 00C8 

168 SZ_NAME: "message_0b04", 

169 I_: r"^00(00|C8)$", 

170 }, 

171 Code._1030: { # mixvalve_params 

172 SZ_NAME: "mixvalve_params", 

173 # .I --- --:------ --:------ 12:138834 1030 016 01C80137C9010FCA0196CB010FCC0101 

174 I_: r"^0[0-9A-F](C[89A-C]01[0-9A-F]{2}){5}$", 

175 RP: r"^00((20|21)01[0-9A-F]{2}){2}$", # rarely seen, HVAC 

176 W_: r"^0[0-9A-F](C[89A-C]01[0-9A-F]{2}){5}$", # contrived 

177 }, 

178 Code._1060: { # device_battery 

179 SZ_NAME: "device_battery", 

180 I_: r"^0[0-9A-F](FF|[0-9A-F]{2})0[01]$", # HCW: r"^(FF|0[0-9A-F]... 

181 SZ_LIFESPAN: td(days=1), 

182 }, 

183 Code._1081: { # max_ch_setpoint 

184 SZ_NAME: "max_ch_setpoint", 

185 RQ: r"^00$", 

186 RP: r"^00[0-9A-F]{4}$", 

187 }, 

188 Code._1090: { # unknown_1090 

189 # 095 RP --- 23:100224 22:219457 --:------ 1090 005 00-7FFF-01F4 

190 SZ_NAME: "message_1090", 

191 RQ: r"^00$", 

192 RP: r"^00", 

193 }, 

194 Code._1098: { # unknown_1098 

195 SZ_NAME: "message_1098", 

196 RQ: r"^00$", 

197 RP: r"^00", 

198 }, 

199 Code._10A0: { # dhw_params 

200 SZ_NAME: "dhw_params", 

201 # RQ --- 07:045960 01:145038 --:------ 10A0 006 0013740003E4 

202 # RP --- 10:048122 18:006402 --:------ 10A0 003 001B58 

203 # NOTE: RFG100 uses a domain id! (00|01) 

204 # 19:14:24.662 051 RQ --- 30:185469 01:037519 --:------ 10A0 001 00 

205 # 19:14:31.463 053 RQ --- 30:185469 01:037519 --:------ 10A0 001 01 

206 I_: r"^(00|01)[0-9A-F]{4}([0-9A-F]{6})?$", # NOTE: RQ/07/10A0 has a payload 

207 RQ: r"^(00|01)([0-9A-F]{10})?$", # NOTE: RQ/07/10A0 has a payload 

208 W_: r"^(00|01)[0-9A-F]{4}([0-9A-F]{6})?$", # TODO: needs checking 

209 SZ_LIFESPAN: td(hours=4), 

210 }, 

211 Code._10B0: { # unknown_10b0 

212 SZ_NAME: "message_10b0", 

213 RQ: r"^00$", 

214 RP: r"^00[0-9A-F]{8}$", 

215 }, 

216 Code._10D0: { # filter_change - polling interval should be 1/day 

217 SZ_NAME: "filter_change", 

218 I_: r"^00[0-9A-F]{6}(0000|FFFF)?$", 

219 RQ: r"^00(00)?$", 

220 W_: r"^00FF$", 

221 }, 

222 Code._10E0: { # device_info 

223 SZ_NAME: "device_info", 

224 I_: r"^(00|FF)([0-9A-F]{30,})?$", # r"^[0-9A-F]{32,}$" might be OK 

225 RQ: r"^00$", # NOTE: 63 seen (no RP), some devices will accept [0-9A-F]{2} 

226 # RP: r"^[0-9A-F]{2}([0-9A-F]){30,}$", # NOTE: index same as RQ 

227 SZ_LIFESPAN: False, 

228 }, 

229 Code._10E1: { # device_id 

230 SZ_NAME: "device_id", 

231 RP: r"^00[0-9A-F]{6}$", 

232 RQ: r"^00$", 

233 SZ_LIFESPAN: False, 

234 }, 

235 Code._10E2: { # unknown_10e2, HVAC? 

236 SZ_NAME: "unknown_10e2", 

237 I_: r"^00[0-9A-F]{4}$", 

238 }, 

239 Code._1100: { # tpi_params 

240 SZ_NAME: "tpi_params", 

241 # I --- 01:172368 --:------ 01:172368 1100 008 FC180400007FFF00 

242 # I --- 01:172368 13:040439 --:------ 1100 008 FC042814007FFF00 

243 # RQ --- 01:145038 13:163733 --:------ 1100 008 00180400007FFF01 # boiler relay 

244 # RP --- 13:163733 01:145038 --:------ 1100 008 00180400FF7FFF01 

245 # RQ --- 01:145038 13:035462 --:------ 1100 008 FC240428007FFF01 # not bolier relay 

246 # RP --- 13:035462 01:145038 --:------ 1100 008 00240428007FFF01 

247 I_: r"^(00|FC)[0-9A-F]{6}(00|FF)([0-9A-F]{4}0[01])?$", 

248 W_: r"^(00|FC)[0-9A-F]{6}(00|FF)([0-9A-F]{4}0[01])?$", # TODO: is there no I? 

249 RQ: r"^(00|FC)([0-9A-F]{6}(00|FF)([0-9A-F]{4}0[01])?)?$", # RQ/13:/00, or RQ/01:/FC: 

250 SZ_LIFESPAN: td(days=1), 

251 }, 

252 Code._11F0: { # unknown_11f0, from heatpump relay 

253 SZ_NAME: "message_11f0", 

254 I_: r"^00", 

255 }, 

256 Code._1260: { # dhw_temp 

257 SZ_NAME: "dhw_temp", 

258 # RQ --- 30:185469 01:037519 --:------ 1260 001 00 

259 # RP --- 01:037519 30:185469 --:------ 1260 003 000837 

260 # RQ --- 18:200202 10:067219 --:------ 1260 002 0000 

261 # RP --- 10:067219 18:200202 --:------ 1260 003 007FFF 

262 # .I --- 07:045960 --:------ 07:045960 1260 003 0007A9 

263 I_: r"^(00|01)[0-9A-F]{4}$", # NOTE: RP is same 

264 RQ: r"^(00|01)(00)?$", # TODO: officially: r"^(00|01)$" 

265 SZ_LIFESPAN: td(hours=1), 

266 }, 

267 Code._1280: { # outdoor_humidity 

268 SZ_NAME: "outdoor_humidity", 

269 I_: r"^00[0-9A-F]{2}[0-9A-F]{8}?$", 

270 }, 

271 Code._1290: { # outdoor_temp 

272 SZ_NAME: "outdoor_temp", 

273 I_: r"^00[0-9A-F]{4}$", # NOTE: RP is same 

274 RQ: r"^00$", 

275 }, 

276 Code._1298: { # co2_level 

277 SZ_NAME: "co2_level", 

278 I_: r"^00[0-9A-F]{4}$", # NOTE: RP is same 

279 RQ: r"^00$", 

280 }, 

281 Code._12A0: { # indoor_humidity 

282 SZ_NAME: "indoor_humidity", 

283 I_: r"^(0[0-9A-F]{3}([0-9A-F]{8}(00)?)?)+$", 

284 RP: r"^0[0-9A-F]{3}([0-9A-F]{8}(00)?)?$", 

285 SZ_LIFESPAN: td(hours=1), 

286 }, 

287 Code._12B0: { # window_state (HVAC % window open) 

288 SZ_NAME: "window_state", 

289 I_: r"^0[0-9A-F](0000|C800|FFFF)$", # NOTE: RP is same 

290 RQ: r"^0[0-9A-F](00)?$", 

291 SZ_LIFESPAN: td(hours=1), 

292 }, 

293 Code._12C0: { # displayed_temp (HVAC room temp) 

294 SZ_NAME: "displayed_temp", # displayed room temp 

295 I_: r"^00[0-9A-F]{2}0[01](FF)?$", 

296 }, 

297 Code._12C8: { # air_quality, HVAC 

298 SZ_NAME: "air_quality", 

299 I_: r"^00[0-9A-F]{4}$", 

300 }, 

301 Code._12F0: { # dhw_flow_rate 

302 # 2021-11-05T06:25:20.399400 065 RP --- 10:023327 18:131597 --:------ 12F0 003 000307 

303 # 2021-11-05T06:25:20.669382 066 RP --- 10:023327 18:131597 --:------ 3220 005 00C01307C0 

304 # 2021-11-05T06:35:20.450201 065 RP --- 10:023327 18:131597 --:------ 12F0 003 000023 

305 # 2021-11-05T06:35:20.721228 066 RP --- 10:023327 18:131597 --:------ 3220 005 0040130059 

306 # 2021-12-06T06:35:54.575298 073 RP --- 10:051349 18:135447 --:------ 12F0 003 00059F 

307 # 2021-12-06T06:35:55.949502 071 RP --- 10:051349 18:135447 --:------ 3220 005 00C0130ECC 

308 SZ_NAME: "dhw_flow_rate", 

309 RQ: r"^00$", 

310 RP: r"^00[0-9A-F]{4}$", 

311 }, 

312 Code._1300: { # cv water pressure (usu. for ch) 

313 SZ_NAME: "ch_pressure", 

314 RQ: r"^00$", 

315 RP: r"^00[0-9A-F]{4}$", 

316 }, 

317 Code._1470: { # programme_scheme, HVAC (1470, 1F70, 22B0) 

318 SZ_NAME: "programme_scheme", 

319 RQ: r"^00$", 

320 I_: r"^00[0-9A-F]{14}$", 

321 W_: r"^00[0-9A-F]{2}0{4}800{6}$", 

322 }, 

323 Code._1F09: { # system_sync - FF (I), 00 (RP), F8 (W, after 1FC9) 

324 SZ_NAME: "system_sync", 

325 I_: r"^(00|01|DB|FF)[0-9A-F]{4}$", # FF is evohome, DB is Hometronics 

326 RQ: r"^00$", 

327 RP: r"^00[0-9A-F]{4}$", # xx-secs 

328 W_: r"^F8[0-9A-F]{4}$", 

329 }, 

330 Code._1F41: { # dhw_mode 

331 SZ_NAME: "dhw_mode", 

332 I_: r"^(00|01)(00|01|FF)0[0-5]F{6}(([0-9A-F]){12})?$", 

333 RQ: r"^(00|01)$", # will accept: r"^(00|01)(00)$" 

334 W_: r"^(00|01)(00|01|FF)0[0-5]F{6}(([0-9A-F]){12})?$", 

335 SZ_LIFESPAN: td(hours=4), 

336 }, 

337 Code._1F70: { # programme_config, HVAC (1470, 1F70, 22B0) 

338 SZ_NAME: "programme_config", 

339 I_: r"^00[0-9A-F]{30}$", 

340 RQ: r"^00[0-9A-F]{30}$", 

341 W_: r"^00[0-9A-F]{30}$", 

342 }, 

343 Code._1FC9: { # rf_bind 

344 SZ_NAME: "rf_bind", # idx-code-dev_id 

345 RQ: r"^00$", 

346 RP: r"^((0[0-9A-F]|F[69ABCF]|[0-9A-F]{2})([0-9A-F]{10}))+$", 

347 I_: r"^((0[0-9A-F]|F[69ABCF]|[0-9A-F]{2})([0-9A-F]{10}))+|00|21$", # NOTE: payload can be 00 

348 W_: r"^((0[0-9A-F]|F[69ABCF]|[0-9A-F]{2})([0-9A-F]{10}))+$", 

349 }, 

350 Code._1FCA: { # unknown_1fca 

351 SZ_NAME: "message_1fca", 

352 RQ: r"^00$", 

353 RP: r"^((0[0-9A-F]|F[9ABCF]|90)([0-9A-F]{10}))+$", # xx-code-dev_id 

354 I_: r"^((0[0-9A-F]|F[9ABCF])([0-9A-F]{10}))+$", 

355 W_: r"^((0[0-9A-F]|F[9ABCF])([0-9A-F]{10}))+$", 

356 }, 

357 Code._1FD0: { # unknown_1fd0 

358 SZ_NAME: "message_1fd0", 

359 RQ: r"^00$", 

360 RP: r"^00", 

361 }, 

362 Code._1FD4: { # opentherm_sync 

363 SZ_NAME: "opentherm_sync", 

364 I_: r"^00([0-9A-F]{4})$", 

365 }, 

366 Code._2210: { # unknown_2210, HVAC, 

367 SZ_NAME: "unknown_2210", 

368 I_: r"^00[0-9A-F]{82}$", 

369 RQ: r"^00$", 

370 }, 

371 Code._2249: { # setpoint_now? 

372 SZ_NAME: "setpoint_now", # setpt_now_next 

373 I_: r"^(0[0-9A-F]{13}){1,2}$", 

374 }, # TODO: This could be an array 

375 Code._22C9: { # setpoint_bounds (was: ufh_setpoint) 

376 SZ_NAME: "setpoint_bounds", 

377 I_: r"^(0[0-9A-F][0-9A-F]{8}0[12]){1,4}(0[12]03)?$", # (0[12]03)? only if len(array) == 1 

378 W_: r"^(0[0-9A-F][0-9A-F]{8}0[12])$", # never an array 

379 }, 

380 Code._22D0: { # unknown_22d0, Spider thermostat, HVAC system switch? 

381 SZ_NAME: "message_22d0", 

382 I_: r"^(00|03)[0-9]{6}$", 

383 W_: r"^03[0-9]{4}1E14030020$", 

384 }, 

385 Code._22D9: { # boiler_setpoint 

386 SZ_NAME: "boiler_setpoint", 

387 RQ: r"^00$", 

388 RP: r"^00[0-9A-F]{4}$", 

389 }, 

390 Code._22E0: { # unknown_22e0, HVAC, NB: no I 

391 SZ_NAME: "unknown_22e0", 

392 RQ: r"^00$", 

393 RP: r"^00[0-9A-F]{6}$", 

394 }, 

395 Code._22E5: { # unknown_22e5, HVAC, NB: no I 

396 SZ_NAME: "unknown_22e5", 

397 RQ: r"^00$", 

398 RP: r"^00[0-9A-F]{6}$", 

399 }, 

400 Code._22E9: { # unknown_22e9, HVAC, NB: no I 

401 SZ_NAME: "unknown_22e9", 

402 RQ: r"^00$", 

403 RP: r"^00[0-9A-F]{6}$", 

404 }, 

405 Code._22F1: { # fan_mode, HVAC 

406 SZ_NAME: "fan_mode", 

407 RQ: r"^00$", 

408 RP: r"^00[0-9A-F]{4}$", 

409 I_: r"^(00|63)(0[0-9A-F]){1,2}$", 

410 }, 

411 Code._22F2: { # unknown_22f2, HVAC, NB: no I 

412 SZ_NAME: "unknown_22f2", 

413 RQ: r"^00$", # (00|01)? 

414 RP: r"^00[0-9A-F]{4}(01[0-9A-F]{4})?$", 

415 }, 

416 Code._22F3: { # fan_boost, HVAC 

417 SZ_NAME: "fan_boost", 

418 I_: r"^(00|63)(021E)?[0-9A-F]{4}([0-9A-F]{8})?$", 

419 }, # minutes only? 

420 Code._22F4: { # unknown_22f4, HVAC 

421 SZ_NAME: "unknown_22f4", 

422 I_: r"^00[0-9A-F]{24}$", 

423 RQ: r"^00$", 

424 }, 

425 Code._22F7: { # fan_bypass_mode (% open), HVAC 

426 SZ_NAME: "fan_bypass_mode", 

427 I_: r"^00([0-9A-F]{2}){1,2}$", # RP is the same 

428 RQ: r"^00$", 

429 W_: r"^00[0-9A-F]{2}(EF)?$", 

430 }, 

431 Code._22F8: { # fan_22f8 (moisture scenario?), HVAC 

432 SZ_NAME: "fan_22f8", 

433 RQ: r"^00$", 

434 I_: r"^00[0-9A-F]{4}$", 

435 }, 

436 Code._22B0: { # programme_status, HVAC (1470, 1F70, 22B0) 

437 SZ_NAME: "programme_status", 

438 W_: r"^00[0-9A-F]{2}$", 

439 I_: r"^00[0-9A-F]{2}$", 

440 }, 

441 Code._2309: { # setpoint 

442 SZ_NAME: "setpoint", 

443 I_: r"^(0[0-9A-F]{5})+$", 

444 W_: r"^0[0-9A-F]{5}$", 

445 # RQ --- 12:010740 01:145038 --:------ 2309 003 03073A # No RPs 

446 RQ: r"^0[0-9A-F]([0-9A-F]{4})?$", # NOTE: 12 uses: r"^0[0-9A-F]$" 

447 SZ_LIFESPAN: td(minutes=30), 

448 }, 

449 Code._2349: { # zone_mode 

450 SZ_NAME: "zone_mode", 

451 I_: r"^0[0-9A-F]{5}0[0-4][0-9A-F]{6}([0-9A-F]{12})?$", 

452 W_: r"^0[0-9A-F]{5}0[0-4][0-9A-F]{6}([0-9A-F]{12})?$", 

453 # .W --- 18:141846 01:050858 --:------ 2349 013 02-0960-04-FFFFFF-0409160607E5 

454 # .W --- 18:141846 01:050858 --:------ 2349 007 02-08FC-01-FFFFFF 

455 RQ: r"^0[0-9A-F](00|[0-9A-F]{12})?$", 

456 # RQ --- 22:070483 01:063844 --:------ 2349 007 06-0708-03-000027 

457 SZ_LIFESPAN: td(hours=4), 

458 }, 

459 Code._2389: { # unknown_2389 - CODE_IDX_COMPLEX? 

460 # .I 024 03:052382 --:------ 03:052382 2389 003 02001B 

461 SZ_NAME: "unknown_2389", 

462 I_: r"^0[0-4][0-9A-F]{4}$", 

463 }, 

464 Code._2400: { # unknown_2400, from OTB 

465 SZ_NAME: "message_2400", 

466 RQ: r"^00$", 

467 RP: r"^00", 

468 }, 

469 Code._2401: { # unknown_2401, from OTB 

470 SZ_NAME: "message_2401", 

471 RQ: r"^00$", 

472 RP: r"^00", 

473 }, 

474 Code._2410: { # unknown_2410, from OTB 

475 SZ_NAME: "message_2410", 

476 RQ: r"^00$", 

477 RP: r"^00", 

478 }, 

479 Code._2411: { # fan_params, HVAC 

480 SZ_NAME: "fan_params", 

481 I_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}([0-9A-F]{8}){4}[0-9A-F]{2,6}$", # Allow 2-6 byte footer 

482 RQ: r"^(00|01|15|16|17|21)00[0-9A-F]{2}((00){19})?$", 

483 RP: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}([0-9A-F]{8}){3}[0-9A-F]{2,6}$", # 4 blocks + footer 

484 W_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}([0-9A-F]{8}){3}[0-9A-F]{2,6}$", # Same as RP 

485 }, 

486 Code._2420: { # unknown_2420, from OTB 

487 SZ_NAME: "message_2420", 

488 RQ: r"^00$", 

489 RP: r"^00", 

490 }, 

491 Code._2D49: { # unknown_2d49 

492 SZ_NAME: "message_2d49", 

493 # 10:14:08.526 045 I --- 01:023389 --:------ 01:023389 2D49 003 010000 

494 # 10:14:12.253 047 I --- 01:023389 --:------ 01:023389 2D49 003 00C800 

495 # 10:14:12.272 047 I --- 01:023389 --:------ 01:023389 2D49 003 01C800 

496 # 10:14:12.390 049 I --- 01:023389 --:------ 01:023389 2D49 003 880000 

497 # 10:14:12.399 048 I --- 01:023389 --:------ 01:023389 2D49 003 FD0000 

498 I_: r"^(0[0-9A-F]|88|F6|FD)[0-9A-F]{2}(00||FF)$", 

499 }, # seen with Hometronic systems 

500 Code._2E04: { # system_mode 

501 SZ_NAME: "system_mode", 

502 I_: r"^0[0-7][0-9A-F]{12}0[01]$", 

503 RQ: r"^FF$", 

504 W_: r"^0[0-7][0-9A-F]{12}0[01]$", 

505 SZ_LIFESPAN: td(hours=4), 

506 }, 

507 Code._2E10: { # presence_detect - HVAC 

508 SZ_NAME: "presence_detect", 

509 I_: r"^00(00|01)(00)?$", 

510 }, 

511 Code._30C9: { # temperature 

512 SZ_NAME: "temperature", 

513 I_: r"^(0[0-9A-F][0-9A-F]{4})+$", 

514 RQ: r"^0[0-9A-F](00)?$", # TODO: officially: r"^0[0-9A-F]$" 

515 RP: r"^0[0-9A-F][0-9A-F]{4}$", # Null: r"^0[0-9A-F]7FFF$" 

516 SZ_LIFESPAN: td(hours=1), 

517 }, 

518 Code._3110: { # ufc_demand - HVAC 

519 SZ_NAME: "ufc_demand", 

520 I_: r"^(00|01)00[0-9A-F]{2}(00|10|20)", # (00|10|20|FF)??? 

521 }, 

522 Code._3120: { # unknown_3120 - Error Report? 

523 SZ_NAME: "message_3120", 

524 I_: r"^00[0-9A-F]{10}FF$", # only ever: 34:/0070B0000000FF 

525 RQ: r"^00$", # 20: will RP an RQ? 

526 # RP: r"^00[0-9A-F]{10}FF$", # only ever: 20:/0070B000009CFF 

527 }, 

528 Code._313E: { # unknown_313e, HVAC, NB: no I 

529 SZ_NAME: "unknown_313e", 

530 RQ: r"^00$", 

531 RP: r"^00[0-9A-F]{20}$", 

532 }, 

533 Code._313F: { # datetime (time report) 

534 SZ_NAME: "datetime", 

535 I_: r"^00[0-9A-F]{16}$", # NOTE: RP is same 

536 RQ: r"^00$", 

537 W_: r"^00[0-9A-F]{16}$", 

538 SZ_LIFESPAN: td(seconds=3), 

539 }, 

540 Code._3150: { # heat_demand, also fans with preheat 

541 SZ_NAME: "heat_demand", 

542 I_: r"^((0[0-9A-F])[0-9A-F]{2}|FC[0-9A-F]{2})+$", 

543 SZ_LIFESPAN: td(minutes=20), 

544 }, 

545 Code._31D9: { # fan_state 

546 SZ_NAME: "fan_state", 

547 # I_: r"^(00|21)[0-9A-F]{32}$", 

548 # I_: r"^(00|01|21)[0-9A-F]{4}((00|FE)(00|20){12}(00|08))?$", 

549 I_: r"^(00|01|15|16|17|21)[0-9A-F]{4}(([0-9A-F]{2})(00|20){0,12}(00|01|04|08)?)?$", # 00-0004-FE 

550 RQ: r"^(00|01|15|16|17|21)$", 

551 }, 

552 Code._31DA: { # hvac_state (fan_state_extended) 

553 SZ_NAME: "hvac_state", 

554 I_: r"^(00|01|15|16|17|21)[0-9A-F]{56}(00|20)?$", 

555 RQ: r"^(00|01|15|16|17|21)$", 

556 # RQ --- 32:168090 30:082155 --:------ 31DA 001 21 

557 }, 

558 Code._31E0: { # fan_demand 

559 # 10:15:42.712 077 I --- 29:146052 32:023459 --:------ 31E0 003 0000C8 

560 # 10:21:18.549 078 I --- 29:146052 32:023459 --:------ 31E0 003 000000 

561 # 07:56:50.522 095 I --- --:------ --:------ 07:044315 31E0 004 00006E00 

562 SZ_NAME: "fan_demand", 

563 I_: r"^00([0-9A-F]{4}){1,3}(00|FF)?$", 

564 }, 

565 Code._3200: { # boiler (or CV?) output temp 

566 SZ_NAME: "boiler_output", 

567 I_: r"^00[0-9A-F]{4}$", 

568 RQ: r"^00$", 

569 }, 

570 Code._3210: { # boiler (or CV?) return temp 

571 SZ_NAME: "boiler_return", 

572 RQ: r"^00$", 

573 RP: r"^00[0-9A-F]{4}$", 

574 }, 

575 Code._3220: { # opentherm_msg 

576 SZ_NAME: "opentherm_msg", 

577 RQ: r"^00[0-9A-F]{8}$", 

578 RP: r"^00[0-9A-F]{8}$", 

579 }, 

580 Code._3221: { # unknown_3221, from OTB 

581 SZ_NAME: "message_3221", 

582 RQ: r"^00$", 

583 RP: r"^00", 

584 }, 

585 Code._3222: { # unknown_3222, HVAC, NB: no I 

586 SZ_NAME: "unknown_3222", 

587 RQ: r"^00$", 

588 RP: r"^00[0-9A-F]{4,24}$", 

589 }, 

590 Code._3223: { # unknown_3223, from OTB 

591 SZ_NAME: "message_3223", 

592 RQ: r"^00$", 

593 RP: r"^00", 

594 }, 

595 Code._3B00: { # actuator_sync, NOTE: no RQ 

596 SZ_NAME: "actuator_sync", 

597 I_: r"^(00|FC)(00|C8)$", 

598 }, 

599 Code._3EF0: { # actuator_state 

600 SZ_NAME: "actuator_state", 

601 # .I --- 13:106039 --:------ 13:106039 3EF0 003 00-C8FF 

602 # .I --- 21:038634 --:------ 21:038634 3EF0 006 00-0000-0A0200 # # Itho spIDer 

603 # .I --- 10:030051 --:------ 10:030051 3EF0 009 00-0010-000000-020A64 

604 # .I --- 08:031043 31:077159 --:------ 3EF0 020 00-1191A72044399D2A50DE43F920478AF7185F3F # # Jasper BLOB 

605 I_: r"^..((00|C8)FF|[0-9A-F]{10}|[0-9A-F]{16}|[0-9A-F]{38})$", 

606 RQ: r"^00(00)?$", 

607 RP: r"^00((00|C8)FF|[0-9A-F]{10}|[0-9A-F]{16})$", 

608 }, 

609 Code._3EF1: { # actuator_cycle 

610 SZ_NAME: "actuator_cycle", 

611 # RQ --- 31:004811 13:077615 --:------ 3EF1 001 00 

612 # RP --- 13:077615 31:004811 --:------ 3EF1 007 00024D001300FF 

613 # RQ --- 22:068154 13:031208 --:------ 3EF1 002 0000 

614 # RP --- 13:031208 22:068154 --:------ 3EF1 007 00024E00E000FF 

615 # RQ --- 31:074182 08:026984 --:------ 3EF1 012 0005D1341DA39B8C7DAFD4C1 

616 # RP --- 08:026984 31:074182 --:------ 3EF1 018 001396A7E087922FA77794280B66BE16A975 

617 RQ: r"^00((00)?|[0-9A-F]{22})$", # NOTE: latter is Japser 

618 RP: r"^00([0-9A-F]{12}|[0-9A-F]{34})$", # NOTE: latter is Japser 

619 }, 

620 Code._4401: { # unknown_4401 - HVAC 

621 SZ_NAME: "unknown_4401", 

622 I_: r"^[0-9A-F]{40}$", 

623 RP: r"^00$", 

624 RQ: r"^[0-9A-F]{40}$", 

625 W_: r"^[0-9A-F]{40}$", 

626 }, 

627 Code._4E01: { # xxx (HVAC) - Itho Spider 

628 SZ_NAME: "hvac_4e01", 

629 I_: r"^00([0-9A-F]{4}){3,12}00$", 

630 }, 

631 Code._4E02: { # xxx (HVAC) - Itho Spider 

632 SZ_NAME: "hvac_4e02", 

633 I_: r"^00([0-9A-F]{4}){3,12}(02|03|04|05)([0-9A-F]{4}){3,12}$", 

634 }, 

635 Code._4E04: { # xxx (HVAC) - Itho Spider 

636 SZ_NAME: "hvac_4e04", 

637 I_: r"^00(00|01|02)[0-9A-F]{2}$", 

638 W_: r"^00(00|01|02)[0-9A-F]{2}$", 

639 }, 

640 Code._4E0D: { # xxx (HVAC) - Itho Spider 

641 SZ_NAME: "hvac_4e0d", 

642 I_: r"^(01|02)(00|01)$", 

643 }, 

644 Code._4E15: { # xxx (HVAC) - Itho Spider 

645 SZ_NAME: "hvac_4e15", 

646 I_: r"^000[0-7]$", 

647 }, 

648 Code._4E16: { # xxx (HVAC) - Itho Spider 

649 SZ_NAME: "hvac_4e16", 

650 I_: r"^00(00){6}$", 

651 }, 

652 Code._PUZZ: { 

653 SZ_NAME: "puzzle_packet", 

654 I_: r"^00(([0-9A-F]){2})+$", 

655 }, 

656} 

657CODE_NAME_LOOKUP = {k: v["name"] for k, v in CODES_SCHEMA.items()} 

658 

659 

660for code in CODES_SCHEMA.values(): # map any (missing) RPs to I_s 

661 if RQ in code and RP not in code and I_ in code: 

662 code[RP] = code[I_] 

663# 

664# .I --- 01:210309 --:------ 01:210309 0009 006 FC00FFF900FF 

665CODES_WITH_ARRAYS: dict[Code, list[int | tuple[str, ...]]] = { # 000C/1FC9 are special 

666 Code._0005: [4, ("34",)], 

667 Code._0009: [3, ("01", "12", "22")], 

668 Code._000A: [6, ("01", "12", "22")], # single element I after a W 

669 Code._2309: [3, ("01", "12", "22")], 

670 Code._30C9: [3, ("01", "12", "22")], 

671 Code._2249: [7, ("23",)], 

672 Code._22C9: [6, ("02",)], 

673 Code._3150: [2, ("02",)], 

674} # TODO dex: element_length, src.type(s) (and dst.type too) 

675# 

676RQ_IDX_COMPLEX: list[Code] = [ 

677 Code._0005, # context: zone_type 

678 Code._000A, # optional payload 

679 Code._000C, # context: index, zone_type 

680 Code._0016, # optional payload 

681 Code._0100, # optional payload 

682 Code._0404, # context: index, fragment_idx (fragment_header) 

683 Code._0418, # context: index 

684 Code._10A0, # optional payload 

685 Code._1100, # optional payload 

686 Code._2309, # optional payload 

687 Code._2349, # optional payload 

688 Code._3220, # context: msg_id, and payload 

689] 

690RQ_NO_PAYLOAD: list[Code] = [ 

691 k 

692 for k, v in CODES_SCHEMA.items() 

693 if v.get(RQ) 

694 in (r"^FF$", r"^00$", r"^00(00)?$", r"^0[0-9A-F](00)?$", r"^0[0-9A-F]00$") 

695] 

696RQ_NO_PAYLOAD.extend((Code._0418,)) 

697 

698 

699######################################################################################## 

700# IDX:_xxxxxx: index (and context) 

701 

702# all known codes should be in only one of IDX_COMPLEX, IDX_NONE, IDX_SIMPLE 

703 

704# IDX_COMPLEX - *usually has* a context, but doesn't satisfy criteria for IDX_SIMPLE: 

705CODE_IDX_ARE_COMPLEX: set[Code] = { 

706 Code._0005, 

707 Code._000C, # idx = fx(payload[0:4]) 

708 # Code._0404, # use "HW" for idx if payload[4:6] == "23" # TODO: should be used 

709 Code._0418, # log_idx (payload[4:6]) # null RPs are missing an idx 

710 Code._1100, 

711 Code._3220, # data_id (payload[4:6]) 

712} # TODO: 0005 to ..._NONE? 

713 

714# IDX_SIMPLE - *can have* a context, but sometimes not (usu. 00): only ever payload[:2], 

715# either a zone_idx, domain_id or (UFC) circuit_idx (or array of such, i.e. seqx[:2]) 

716 

717_SIMPLE_IDX = ("^0[0-9A-F]", "^(0[0-9A-F]", "^((0[0-9A-F]", "^(00|01)") 

718CODE_IDX_ARE_SIMPLE: set[Code] = { 

719 k 

720 for k, v in CODES_SCHEMA.items() 

721 for verb in (RQ, I_) 

722 if k not in CODE_IDX_ARE_COMPLEX and v.get(verb, "").startswith(_SIMPLE_IDX) 

723} 

724CODE_IDX_ARE_SIMPLE |= { 

725 Code._22D0, 

726 Code._2411, 

727 Code._31D9, 

728 Code._31DA, 

729 Code._3B00, 

730 Code._4E0D, 

731} 

732 

733# IDX_NONE - *never has* a context: most payloads start 00, but no context even if the 

734# payload starts with something else (e.g. 2E04) 

735CODE_IDX_ARE_NONE: set[Code] = { 

736 k 

737 for k, v in CODES_SCHEMA.items() 

738 if k not in CODE_IDX_ARE_COMPLEX | CODE_IDX_ARE_SIMPLE 

739 and ((RQ in v and v[RQ][:3] == "^00") or (I_ in v and v[I_][:3] == "^00")) 

740} 

741CODE_IDX_ARE_NONE |= {Code._22F3, Code._2389, Code._2E04, Code._4401} 

742 

743# CODE_IDX_DOMAIN - NOTE: not necc. mutex with other 3 

744CODE_IDX_DOMAIN: dict[Code, str] = { 

745 Code._0001: "^F[ACF])", 

746 Code._0008: "^F[9AC]", 

747 Code._0009: "^F[9AC]", 

748 Code._1100: "^FC", 

749 Code._1FC9: "^F[9ABCF]", 

750 Code._3150: "^FC", 

751 Code._3B00: "^FC", 

752} 

753 

754 

755# 

756######################################################################################## 

757# CODES_BY_DEV_SLUG - HEAT (CH/DHW) vs HVAC (ventilation) 

758# TODO: 34: can 3220 - split out RND from THM/STA 

759_DEV_KLASSES_HEAT: dict[str, dict[Code, dict[VerbT, Any]]] = { 

760 DevType.RFG: { # RFG100: RF to Internet gateway (and others) 

761 Code._0002: {RQ: {}}, 

762 Code._0004: {I_: {}, RQ: {}}, 

763 Code._0005: {RQ: {}}, 

764 Code._0006: {RQ: {}}, 

765 Code._000A: {RQ: {}}, 

766 Code._000C: {RQ: {}}, 

767 Code._000E: {W_: {}}, 

768 Code._0016: {RP: {}}, 

769 Code._0404: {RQ: {}, W_: {}}, 

770 Code._0418: {RQ: {}}, 

771 Code._10A0: {RQ: {}}, 

772 Code._10E0: {I_: {}, RQ: {}, RP: {}}, 

773 Code._1260: {RQ: {}}, 

774 Code._1290: {I_: {}}, 

775 Code._1F41: {RQ: {}}, 

776 Code._1FC9: {RP: {}, W_: {}}, 

777 Code._22D9: {RQ: {}}, 

778 Code._2309: {I_: {}}, 

779 Code._2349: {RQ: {}, RP: {}, W_: {}}, 

780 Code._2E04: {RQ: {}, I_: {}, W_: {}}, 

781 Code._30C9: {RQ: {}}, 

782 Code._313F: {RQ: {}, RP: {}, W_: {}}, 

783 Code._3220: {RQ: {}}, 

784 Code._3EF0: {RQ: {}}, 

785 }, 

786 DevType.CTL: { # e.g. ATC928: Evohome Colour Controller 

787 Code._0001: {W_: {}}, 

788 Code._0002: {I_: {}, RP: {}}, 

789 Code._0004: {I_: {}, RP: {}}, 

790 Code._0005: {I_: {}, RP: {}}, 

791 Code._0006: {RP: {}}, 

792 Code._0008: {I_: {}}, 

793 Code._0009: {I_: {}}, 

794 Code._000A: {I_: {}, RP: {}}, 

795 Code._000C: {RP: {}}, 

796 Code._0016: {RQ: {}, RP: {}}, 

797 Code._0100: {RP: {}}, 

798 Code._01D0: {I_: {}}, 

799 Code._01E9: {I_: {}}, 

800 Code._0404: {I_: {}, RP: {}}, 

801 Code._0418: {I_: {}, RP: {}}, 

802 Code._1030: {I_: {}}, 

803 Code._10A0: {I_: {}, RP: {}}, 

804 Code._10E0: {RP: {}}, 

805 Code._1100: {I_: {}, RQ: {}, RP: {}, W_: {}}, 

806 Code._1260: {RP: {}}, 

807 Code._1290: {RP: {}}, 

808 Code._12B0: {I_: {}, RP: {}}, 

809 Code._1F09: {I_: {}, RP: {}, W_: {}}, 

810 Code._1FC9: {I_: {}, RQ: {}, RP: {}, W_: {}}, 

811 Code._1F41: {I_: {}, RP: {}}, 

812 Code._2249: {I_: {}}, # Hometronics, not Evohome 

813 Code._22D9: {RQ: {}}, 

814 Code._2309: {I_: {}, RP: {}}, 

815 Code._2349: {I_: {}, RP: {}}, 

816 Code._2D49: {I_: {}}, 

817 Code._2E04: {I_: {}, RP: {}}, 

818 Code._30C9: {I_: {}, RP: {}}, 

819 Code._313F: {I_: {}, RP: {}, W_: {}}, 

820 Code._3150: {I_: {}}, 

821 Code._3220: {RQ: {}}, 

822 Code._3B00: {I_: {}}, 

823 Code._3EF0: {RQ: {}}, 

824 }, 

825 DevType.PRG: { # e.g. HCF82/HCW82: Room Temperature Sensor 

826 Code._0009: {I_: {}}, 

827 Code._1090: {RP: {}}, 

828 Code._10A0: {RP: {}}, 

829 Code._1100: {I_: {}}, 

830 Code._1F09: {I_: {}}, 

831 Code._2249: {I_: {}}, 

832 Code._2309: {I_: {}}, 

833 Code._30C9: {I_: {}}, 

834 Code._3B00: {I_: {}}, 

835 Code._3EF1: {RP: {}}, 

836 }, 

837 DevType.THM: { # e.g. Generic Thermostat 

838 Code._0001: {W_: {}}, 

839 Code._0005: {I_: {}}, 

840 Code._0008: {I_: {}}, 

841 Code._0009: {I_: {}}, 

842 Code._000A: {I_: {}, RQ: {}, W_: {}}, 

843 Code._000C: {I_: {}}, 

844 Code._000E: {I_: {}}, 

845 Code._0016: {RQ: {}}, 

846 Code._01FF: {I_: {}, RQ: {}}, 

847 Code._042F: {I_: {}}, 

848 Code._1030: {I_: {}}, 

849 Code._1060: {I_: {}}, 

850 Code._1090: {RQ: {}}, 

851 Code._10E0: {I_: {}}, 

852 Code._1100: {I_: {}}, 

853 Code._12C0: {I_: {}}, 

854 Code._1F09: {I_: {}}, 

855 Code._1FC9: {I_: {}}, 

856 Code._22C9: {W_: {}}, # DT4R 

857 Code._22D0: {W_: {}}, # Spider master THM 

858 Code._2309: {I_: {}, RQ: {}, W_: {}}, 

859 Code._2349: {RQ: {}, W_: {}}, 

860 Code._30C9: {I_: {}}, 

861 Code._3110: {I_: {}}, # Spider THM 

862 Code._3120: {I_: {}}, 

863 Code._313F: { 

864 I_: {} 

865 }, # .W --- 30:253184 34:010943 --:------ 313F 009 006000070E0... 

866 Code._3220: {RP: {}}, # RND (using OT) 

867 Code._3B00: {I_: {}}, 

868 Code._3EF0: {RQ: {}}, # when bound direct to a 13: 

869 Code._3EF1: {RQ: {}}, # when bound direct to a 13: 

870 }, 

871 DevType.UFC: { # e.g. HCE80/HCC80: Underfloor Heating Controller 

872 Code._0001: {RP: {}, W_: {}}, # TODO: Ix RP 

873 Code._0005: {RP: {}}, 

874 Code._0008: {I_: {}}, 

875 Code._000A: {RP: {}}, 

876 Code._000C: {RP: {}}, 

877 Code._1FC9: {I_: {}}, 

878 Code._1FD4: {I_: {}}, # Spider Autotemp, slave 'ticker' 

879 Code._10E0: {I_: {}, RP: {}}, 

880 Code._22C9: {I_: {}}, # NOTE: No RP 

881 Code._22D0: {I_: {}, RP: {}}, 

882 Code._2309: {RP: {}}, 

883 Code._3110: {I_: {}}, # Spider Autotemp 

884 Code._3150: {I_: {}}, 

885 Code._4E01: {I_: {}}, # Spider Autotemp Zone controller 

886 Code._4E04: {I_: {}}, # idem 

887 }, 

888 DevType.TRV: { # e.g. HR92/HR91: Radiator Controller 

889 Code._0001: {W_: {r"^0[0-9A-F]"}}, 

890 Code._0004: {RQ: {r"^0[0-9A-F]00$"}}, 

891 Code._0016: {RQ: {}, RP: {}}, 

892 Code._0100: {RQ: {r"^00"}}, 

893 Code._01D0: {W_: {}}, 

894 Code._01E9: {W_: {}}, 

895 Code._1060: {I_: {r"^0[0-9A-F]{3}0[01]$"}}, 

896 Code._10E0: {I_: {r"^00[0-9A-F]{30,}$"}}, 

897 Code._12B0: {I_: {r"^0[0-9A-F]{3}00$"}}, # sends every 1h 

898 Code._1F09: {RQ: {r"^00$"}}, 

899 Code._1FC9: {I_: {}, W_: {}}, 

900 Code._2309: {I_: {r"^0[0-9A-F]{5}$"}}, 

901 Code._30C9: {I_: {r"^0[0-9A-F]"}}, 

902 Code._313F: {RQ: {r"^00$"}}, 

903 Code._3150: {I_: {r"^0[0-9A-F]{3}$"}}, 

904 }, 

905 DevType.DHW: { # e.g. CS92: (DHW) Cylinder Thermostat 

906 Code._0016: {RQ: {}}, 

907 Code._1060: {I_: {}}, 

908 Code._10A0: {RQ: {}}, # This RQ/07/10A0 includes a payload 

909 Code._1260: {I_: {}}, 

910 Code._1FC9: {I_: {}}, 

911 }, 

912 DevType.OTB: { # e.g. R8810/R8820: OpenTherm Bridge 

913 Code._0009: {I_: {}}, # 1/24h for a R8820 (not an R8810) 

914 Code._0150: {RP: {}}, # R8820A only? 

915 Code._042F: {I_: {}, RP: {}}, 

916 Code._1081: {RP: {}}, # R8820A only? 

917 Code._1098: {RP: {}}, # R8820A only? 

918 Code._10A0: {RP: {}}, 

919 Code._10B0: {RP: {}}, # R8820A only? 

920 Code._10E0: {I_: {}, RP: {}}, 

921 Code._10E1: {RP: {}}, # R8820A only? 

922 Code._1260: {RP: {}}, 

923 Code._1290: {RP: {}}, 

924 Code._12F0: {RP: {}}, # R8820A only? 

925 Code._1300: {RP: {}}, # R8820A only? 

926 Code._1FC9: {I_: {}, W_: {}}, 

927 Code._1FD0: {RP: {}}, # R8820A only? 

928 Code._1FD4: {I_: {}}, # 2/min for R8810, every ~210 sec for R8820 

929 Code._22D9: {RP: {}}, 

930 Code._2400: {RP: {}}, # R8820A only? 

931 Code._2401: {RP: {}}, # R8820A only? 

932 Code._2410: {RP: {}}, # R8820A only? 

933 Code._2420: {RP: {}}, # R8820A only? 

934 Code._3150: {I_: {}}, 

935 Code._3200: {RP: {}}, # R8820A only? 

936 Code._3210: {RP: {}}, # R8820A only? 

937 Code._3220: {RP: {}}, 

938 Code._3221: {RP: {}}, # R8820A only? 

939 Code._3223: {RP: {}}, # R8820A only? 

940 Code._3EF0: {I_: {}, RP: {}}, 

941 Code._3EF1: {RP: {}}, 

942 }, # see: https://www.opentherm.eu/request-details/?post_ids=2944 

943 DevType.BDR: { # e.g. BDR91A/BDR91T: Wireless Relay Box 

944 Code._0008: {RP: {}}, # doesn't RP/0009 

945 Code._0016: {RP: {}}, 

946 # Code._10E0: {}, # 13: will not RP/10E0 # TODO: how to indicate that fact here 

947 Code._1100: {I_: {}, RP: {}}, 

948 Code._11F0: {I_: {}}, # BDR91T in heatpump mode 

949 Code._1FC9: {RP: {}, W_: {}}, 

950 Code._2D49: {I_: {}}, # BDR91T in heatpump mode 

951 Code._3B00: {I_: {}}, 

952 Code._3EF0: {I_: {}}, 

953 # RP: {}, # RQ --- 01:145038 13:237335 --:------ 3EF0 001 00 

954 Code._3EF1: {RP: {}}, 

955 }, 

956 DevType.OUT: { 

957 Code._0002: {I_: {}}, 

958 Code._1FC9: {I_: {}}, 

959 }, # i.e. HB85 (ext. temperature/luminosity(lux)), HB95 (+ wind speed) 

960 # 

961 DevType.JIM: { # Jasper Interface Module, 08 

962 Code._0008: {RQ: {}}, 

963 Code._10E0: {I_: {}}, 

964 Code._1100: {I_: {}}, 

965 Code._3EF0: {I_: {}}, 

966 Code._3EF1: {RP: {}}, 

967 }, 

968 DevType.JST: { # Jasper Stat, 31 

969 Code._0008: {I_: {}}, 

970 Code._10E0: {I_: {}}, 

971 Code._3EF1: {RQ: {}, RP: {}}, 

972 }, 

973 # DevType.RND: { # e.g. TR87RF: Single (round) Zone Thermostat 

974 # Code._0005: {I_: {}}, 

975 # Code._0008: {I_: {}}, 

976 # Code._000A: {I_: {}, RQ: {}}, 

977 # Code._000C: {I_: {}}, 

978 # Code._000E: {I_: {}}, 

979 # Code._042F: {I_: {}}, 

980 # Code._1060: {I_: {}}, 

981 # Code._10E0: {I_: {}}, 

982 # Code._12C0: {I_: {}}, 

983 # Code._1FC9: {I_: {}}, 

984 # Code._1FD4: {I_: {}}, 

985 # Code._2309: {I_: {}, RQ: {}, W_: {}}, 

986 # Code._2349: {RQ: {}}, 

987 # Code._30C9: {I_: {}}, 

988 # Code._3120: {I_: {}}, 

989 # Code._313F: {I_: {}}, # W --- 30:253184 34:010943 --:------ 313F 009 006000070E0... 

990 # Code._3EF0: {I_: {}, RQ: {}}, # when bound direct to a 13: 

991 # Code._3EF1: {RQ: {}}, # when bound direct to a 13: 

992 # }, 

993 # DevType.DTS: { # e.g. DTS92(E) 

994 # Code._0001: {W_: {}}, 

995 # Code._0008: {I_: {}}, 

996 # Code._0009: {I_: {}}, 

997 # Code._000A: {I_: {}, RQ: {}, W_: {}}, 

998 # Code._0016: {RQ: {}}, 

999 # # "0B04": {I_: {}}, 

1000 # Code._1030: {I_: {}}, 

1001 # Code._1060: {I_: {}}, 

1002 # Code._1090: {RQ: {}}, 

1003 # Code._1100: {I_: {}}, 

1004 # Code._1F09: {I_: {}}, 

1005 # Code._1FC9: {I_: {}}, 

1006 # Code._2309: {I_: {}, RQ: {}, W_: {}}, 

1007 # Code._2349: {RQ: {}, W_: {}}, 

1008 # Code._30C9: {I_: {}}, 

1009 # Code._313F: {I_: {}}, 

1010 # Code._3B00: {I_: {}}, 

1011 # Code._3EF1: {RQ: {}}, 

1012 # }, 

1013 # DevType.HCW: { # e.g. HCF82/HCW82: Room Temperature Sensor 

1014 # Code._0001: {W_: {}}, 

1015 # Code._0002: {I_: {}}, 

1016 # Code._0008: {I_: {}}, 

1017 # Code._0009: {I_: {}}, 

1018 # Code._1060: {I_: {}}, 

1019 # Code._1100: {I_: {}}, 

1020 # Code._1F09: {I_: {}}, 

1021 # Code._1FC9: {I_: {}}, 

1022 # Code._2309: {I_: {}}, 

1023 # Code._2389: {I_: {}}, 

1024 # Code._30C9: {I_: {}}, 

1025 # }, 

1026} 

1027# TODO: add 1FC9 everywhere? 

1028_DEV_KLASSES_HVAC: dict[str, dict[Code, dict[VerbT, Any]]] = { 

1029 DevType.DIS: { # Orcon RF15 Display: ?a superset of a REM 

1030 Code._0001: {RQ: {}}, 

1031 Code._042F: {I_: {}}, 

1032 Code._10E0: {I_: {}, RQ: {}}, 

1033 Code._1470: {RQ: {}}, 

1034 Code._1FC9: {I_: {}}, 

1035 Code._1F70: {I_: {}}, 

1036 Code._22F1: {I_: {}}, 

1037 Code._22F3: {I_: {}}, 

1038 Code._22F7: {RQ: {}, W_: {}}, 

1039 Code._22B0: {W_: {}}, 

1040 Code._2411: {RQ: {}, W_: {}}, 

1041 Code._313F: {RQ: {}}, 

1042 Code._31DA: {RQ: {}}, 

1043 }, 

1044 DevType.RFS: { # Itho spIDer: RF to Internet gateway (like a RFG100) 

1045 Code._1060: {I_: {}}, 

1046 Code._10E0: {I_: {}, RP: {}}, 

1047 Code._12C0: {I_: {}}, 

1048 Code._22C9: {I_: {}}, 

1049 Code._22F1: {I_: {}}, 

1050 Code._22F3: {I_: {}}, 

1051 Code._2E10: {I_: {}}, 

1052 Code._30C9: {I_: {}}, 

1053 Code._3110: {I_: {}}, 

1054 Code._3120: {I_: {}}, 

1055 Code._31D9: {RQ: {}}, 

1056 Code._31DA: {RQ: {}}, 

1057 Code._3EF0: {I_: {}}, 

1058 }, 

1059 DevType.FAN: { 

1060 Code._0001: {RP: {}}, 

1061 Code._0002: {I_: {}}, 

1062 Code._042F: {I_: {}}, 

1063 Code._10D0: {I_: {}, RP: {}}, 

1064 Code._10E0: {I_: {}, RP: {}}, 

1065 Code._1298: {I_: {}}, 

1066 Code._12A0: {I_: {}}, 

1067 Code._12C8: {I_: {}}, 

1068 Code._1470: {RP: {}}, 

1069 Code._1F09: {I_: {}, RP: {}}, 

1070 Code._1FC9: {I_: {}, W_: {}}, 

1071 Code._2210: {I_: {}, RP: {}}, 

1072 Code._22E0: {RP: {}}, 

1073 Code._22E5: {RP: {}}, 

1074 Code._22E9: {RP: {}}, 

1075 Code._22F1: {RP: {}}, 

1076 Code._22F2: {I_: {}, RP: {}}, 

1077 Code._22F3: {}, 

1078 Code._22F4: {I_: {}, RP: {}}, 

1079 Code._22F7: {I_: {}, RP: {}}, 

1080 Code._2411: {I_: {}, RP: {}}, 

1081 Code._2E10: {I_: {}}, 

1082 Code._3120: {I_: {}}, 

1083 Code._3150: {I_: {}}, 

1084 Code._313E: {RP: {}}, 

1085 Code._313F: {I_: {}, RP: {}}, 

1086 Code._31D9: {I_: {}, RP: {}}, 

1087 Code._31DA: {I_: {}, RP: {}}, 

1088 # Code._31E0: {I_: {}}, 

1089 Code._3200: {I_: {}}, 

1090 Code._3222: {RP: {}}, 

1091 }, 

1092 DevType.CO2: { 

1093 Code._042F: {I_: {}}, 

1094 Code._10E0: {I_: {}, RP: {}}, 

1095 Code._1298: {I_: {}, RP: {}}, 

1096 Code._1FC9: {I_: {}}, 

1097 Code._22F1: {RQ: {}}, 

1098 Code._2411: {RQ: {}}, 

1099 Code._2E10: {I_: {}}, 

1100 Code._3120: {I_: {}}, 

1101 Code._31DA: {RQ: {}}, 

1102 Code._31E0: {I_: {}}, 

1103 }, 

1104 DevType.HUM: { 

1105 Code._042F: {I_: {}}, 

1106 Code._1060: {I_: {}}, 

1107 Code._10E0: {I_: {}}, 

1108 Code._12A0: {I_: {}}, 

1109 Code._1FC9: {I_: {}}, 

1110 Code._31DA: {RQ: {}}, 

1111 Code._31E0: {I_: {}}, 

1112 }, 

1113 DevType.REM: { # HVAC: two-way switch; also an "06/22F1"? 

1114 Code._0001: {RQ: {}}, # from a VMI (only?) 

1115 Code._042F: {I_: {}}, # from a VMI (only?) 

1116 Code._1060: {I_: {}}, 

1117 Code._10D0: { 

1118 RP: {}, 

1119 RQ: {}, 

1120 W_: {}, 

1121 }, # RQ/RP Orcon HRC, W=reset filter count from REM 

1122 Code._10E0: {I_: {}, RQ: {}}, # RQ from a VMI (only?) 

1123 Code._1470: {RQ: {}}, # from a VMI (only?) 

1124 Code._1FC9: {I_: {}}, 

1125 Code._22F1: {I_: {}}, 

1126 Code._22F3: {I_: {}}, 

1127 Code._22F7: {RQ: {}, W_: {}}, # from a VMI (only?) 

1128 Code._2411: {RQ: {}, W_: {}}, # from a VMI (only?) 

1129 Code._313F: {RQ: {}, W_: {}}, # from a VMI (only?) 

1130 Code._31DA: {RQ: {}}, # to a VMI (only?) 

1131 # Code._31E0: {I_: {}}, 

1132 }, # https://www.ithodaalderop.nl/nl-NL/professional/product/536-0124 

1133 # None: { # unknown, TODO: make generic HVAC 

1134 # _4401: {I_: {}}, 

1135 # }, 

1136} 

1137 

1138CODES_BY_DEV_SLUG: dict[str, dict[Code, dict[VerbT, Any]]] = { 

1139 DevType.HGI: { # HGI80: RF to (USB) serial gateway interface 

1140 Code._PUZZ: {I_: {}, RQ: {}, W_: {}}, 

1141 }, # HGI80s can do what they like 

1142 **{k: v for k, v in _DEV_KLASSES_HVAC.items() if k is not None}, 

1143 **{k: v for k, v in _DEV_KLASSES_HEAT.items() if k is not None}, 

1144} 

1145 

1146CODES_OF_HEAT_DOMAIN: tuple[Code] = sorted( # type: ignore[assignment] 

1147 tuple({c for k in _DEV_KLASSES_HEAT.values() for c in k}) + (Code._0B04, Code._2389) 

1148) 

1149CODES_OF_HVAC_DOMAIN: tuple[Code] = sorted( # type: ignore[assignment] 

1150 tuple({c for k in _DEV_KLASSES_HVAC.values() for c in k}) 

1151 + (Code._22F8, Code._4401, Code._4E01, Code._4E02, Code._4E04) 

1152) 

1153CODES_OF_HEAT_DOMAIN_ONLY: tuple[Code, ...] = tuple( 

1154 c for c in sorted(CODES_OF_HEAT_DOMAIN) if c not in CODES_OF_HVAC_DOMAIN 

1155) 

1156CODES_OF_HVAC_DOMAIN_ONLY: tuple[Code, ...] = tuple( 

1157 c for c in sorted(CODES_OF_HVAC_DOMAIN) if c not in CODES_OF_HEAT_DOMAIN 

1158) 

1159_CODES_OF_BOTH_DOMAINS: tuple[Code, ...] = tuple( 

1160 sorted(set(CODES_OF_HEAT_DOMAIN) & set(CODES_OF_HVAC_DOMAIN)) 

1161) 

1162_CODES_OF_EITHER_DOMAIN: tuple[Code, ...] = tuple( 

1163 sorted(set(CODES_OF_HEAT_DOMAIN) | set(CODES_OF_HVAC_DOMAIN)) 

1164) 

1165_CODES_OF_NO_DOMAIN: tuple[Code, ...] = tuple( 

1166 c for c in CODES_SCHEMA if c not in _CODES_OF_EITHER_DOMAIN 

1167) 

1168 

1169_CODE_FROM_NON_CTL: tuple[Code, ...] = tuple( 

1170 dict.fromkeys( 

1171 c 

1172 for k, v1 in CODES_BY_DEV_SLUG.items() 

1173 for c, v2 in v1.items() 

1174 if k != DevType.CTL and (I_ in v2 or RP in v2) 

1175 ) 

1176) 

1177_CODE_FROM_CTL = _DEV_KLASSES_HEAT[DevType.CTL].keys() 

1178 

1179_CODE_ONLY_FROM_CTL: tuple[Code, ...] = tuple( 

1180 c for c in _CODE_FROM_CTL if c not in _CODE_FROM_NON_CTL 

1181) 

1182CODES_ONLY_FROM_CTL: tuple[Code, ...] = ( 

1183 Code._1030, 

1184 Code._1F09, 

1185 # Code._22D0, # also _W from the Spider master THM! issue #340 

1186 Code._313F, 

1187) # I packets, TODO: 31Dx too? not 31D9/31DA! 

1188 

1189# 

1190######################################################################################## 

1191# Other Stuff 

1192 

1193# ### WIP: 

1194# _result = {} 

1195# for domain in (_DEV_KLASSES_HVAC, ): 

1196# for klass, kv in domain.items(): 

1197# if klass in (DEV_TYPE.DIS, DEV_TYPE.RFS): 

1198# continue 

1199# for code, cv in kv.items(): 

1200# for verb in cv: 

1201# _result.update({(verb, code): _result.get((verb, code), 0) + 1}) 

1202 

1203# _HVAC_VC_PAIR_BY_CLASS = { 

1204# (v, c): k 

1205# for c, cv in kv.items() 

1206# for v in cv 

1207# for k, kv in _DEV_KLASSES_HVAC.items() 

1208# if (v, c) in [k for k, v in _result.items() if v == 1] 

1209# } 

1210 

1211 

1212_HVAC_VC_PAIR_BY_CLASS: dict[DevType, tuple[tuple[VerbT, Code], ...]] = { 

1213 DevType.CO2: ((I_, Code._1298),), 

1214 DevType.FAN: ((I_, Code._31D9), (I_, Code._31DA), (RP, Code._31DA)), 

1215 DevType.HUM: ((I_, Code._12A0),), 

1216 DevType.REM: ((I_, Code._22F1), (I_, Code._22F3)), 

1217} 

1218HVAC_KLASS_BY_VC_PAIR: dict[tuple[VerbT, Code], DevType] = { 

1219 t: k for k, v in _HVAC_VC_PAIR_BY_CLASS.items() for t in v 

1220} 

1221 

1222 

1223SZ_DESCRIPTION: Final = "description" 

1224SZ_MIN_VALUE: Final = "min_value" 

1225SZ_MAX_VALUE: Final = "max_value" 

1226SZ_PRECISION: Final = "precision" 

1227SZ_DATA_TYPE: Final = "data_type" 

1228SZ_DATA_UNIT: Final = "data_unit" 

1229 

1230_22F1_MODE_ITHO: dict[str, str] = { 

1231 "00": "off", # not seen 

1232 "01": "trickle", # not seen 

1233 "02": "low", 

1234 "03": "medium", 

1235 "04": "high", # aka boost with 22F3 

1236} 

1237 

1238_22F1_MODE_NUAIRE: dict[str, str] = { 

1239 "02": "normal", 

1240 "03": "boost", # aka purge 

1241 "09": "heater_off", 

1242 "0A": "heater_auto", 

1243} # DRI-ECO-2S (normal/boost only), DRI-ECO-4S 

1244 

1245_22F1_MODE_ORCON: dict[str, str] = { 

1246 "00": "away", 

1247 "01": "low", 

1248 "02": "medium", 

1249 "03": "high", # # The order of the next two may be swapped 

1250 "04": "auto", # # economy, as per RH and CO2 <= 1150 ppm (unsure which is which) 

1251 "05": "auto_alt", # comfort, as per RH and CO2 <= 950 ppm (unsure which is which) 

1252 "06": "boost", 

1253 "07": "off", 

1254} 

1255 

1256_22F1_MODE_VASCO: dict[str, str] = { # for VASCO D60 and ClimaRad Minibox remotes 

1257 "00": "off", 

1258 "01": "away", # 000106 minimum 

1259 "02": "low", # 000206 

1260 "03": "medium", # 000306 

1261 "04": "high", # 000406, aka boost with 22F3 

1262 "05": "auto", 

1263} 

1264 

1265_22F1_SCHEMES: dict[str, dict[str, str]] = { 

1266 "itho": _22F1_MODE_ITHO, 

1267 "nuaire": _22F1_MODE_NUAIRE, 

1268 "orcon": _22F1_MODE_ORCON, 

1269 "vasco": _22F1_MODE_VASCO, 

1270} 

1271 

1272# unclear if true for only Orcon/*all* models 

1273_2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = { 

1274 "01": { # all? 

1275 SZ_DESCRIPTION: "Support", 

1276 SZ_MIN_VALUE: 0xFF, # None? 

1277 SZ_MAX_VALUE: 0xFF, 

1278 SZ_PRECISION: 1, 

1279 SZ_DATA_TYPE: "00", 

1280 SZ_DATA_UNIT: "", 

1281 }, 

1282 "31": { # slot 09 (FANs produced after 2021) 

1283 SZ_DESCRIPTION: "Time to change filter (days)", 

1284 SZ_MIN_VALUE: 0, 

1285 SZ_MAX_VALUE: 1800, 

1286 SZ_PRECISION: 30, 

1287 SZ_DATA_TYPE: "10", 

1288 SZ_DATA_UNIT: "days", 

1289 }, 

1290 "3D": { # slot 00 

1291 SZ_DESCRIPTION: "Away mode Supply fan rate (%)", 

1292 SZ_MIN_VALUE: 0.0, 

1293 SZ_MAX_VALUE: 0.4, 

1294 SZ_PRECISION: 0.005, 

1295 SZ_DATA_TYPE: "0F", 

1296 SZ_DATA_UNIT: "%", 

1297 }, 

1298 "3E": { # slot 01 

1299 SZ_DESCRIPTION: "Away mode Exhaust fan rate (%)", 

1300 SZ_MIN_VALUE: 0.0, 

1301 SZ_MAX_VALUE: 0.4, 

1302 SZ_PRECISION: 0.005, 

1303 SZ_DATA_TYPE: "0F", 

1304 SZ_DATA_UNIT: "%", 

1305 }, 

1306 "3F": { # slot 02 

1307 SZ_DESCRIPTION: "Low mode Supply fan rate (%)", 

1308 SZ_MIN_VALUE: 0.0, 

1309 SZ_MAX_VALUE: 0.8, 

1310 SZ_PRECISION: 0.005, 

1311 SZ_DATA_TYPE: "0F", 

1312 SZ_DATA_UNIT: "%", 

1313 }, 

1314 "40": { # slot 03 

1315 SZ_DESCRIPTION: "Low mode Exhaust fan rate (%)", 

1316 SZ_MIN_VALUE: 0.0, 

1317 SZ_MAX_VALUE: 0.8, 

1318 SZ_PRECISION: 0.005, 

1319 SZ_DATA_TYPE: "0F", 

1320 SZ_DATA_UNIT: "%", 

1321 }, 

1322 "41": { # slot 04 

1323 SZ_DESCRIPTION: "Medium mode Supply fan rate (%)", 

1324 SZ_MIN_VALUE: 0.1, # Orcon FAN responds with 0.0, but I guess this should be the same as for "42" 

1325 SZ_MAX_VALUE: 1.0, 

1326 SZ_PRECISION: 0.005, 

1327 SZ_DATA_TYPE: "0F", 

1328 SZ_DATA_UNIT: "%", 

1329 }, 

1330 "42": { # slot 05 

1331 SZ_DESCRIPTION: "Medium mode Exhaust fan rate (%)", 

1332 SZ_MIN_VALUE: 0.1, 

1333 SZ_MAX_VALUE: 1.0, 

1334 SZ_PRECISION: 0.005, 

1335 SZ_DATA_TYPE: "0F", 

1336 SZ_DATA_UNIT: "%", 

1337 }, 

1338 "43": { # slot 06 

1339 SZ_DESCRIPTION: "High mode Supply fan rate (%)", 

1340 SZ_MIN_VALUE: 0.1, 

1341 SZ_MAX_VALUE: 1.0, 

1342 SZ_PRECISION: 0.005, 

1343 SZ_DATA_TYPE: "0F", 

1344 SZ_DATA_UNIT: "%", 

1345 }, 

1346 "44": { # slot 07 

1347 SZ_DESCRIPTION: "High mode Exhaust fan rate (%)", 

1348 SZ_MIN_VALUE: 0.1, 

1349 SZ_MAX_VALUE: 1.0, 

1350 SZ_PRECISION: 0.005, 

1351 SZ_DATA_TYPE: "0F", 

1352 SZ_DATA_UNIT: "%", 

1353 }, 

1354 "4B": { # slot 09 (FANs produced before 2021) Also check code 22F7 

1355 SZ_DESCRIPTION: "(Test) Bypass Valve (0=auto, 1=open, 2=closed)", 

1356 SZ_MIN_VALUE: 0, 

1357 SZ_MAX_VALUE: 2, 

1358 SZ_PRECISION: 1, 

1359 SZ_DATA_TYPE: "00", 

1360 SZ_DATA_UNIT: "", 

1361 }, 

1362 "4E": { # slot 0A 

1363 SZ_DESCRIPTION: "Moisture scenario position (0=medium, 1=high)", 

1364 SZ_MIN_VALUE: 0, 

1365 SZ_MAX_VALUE: 1, 

1366 SZ_PRECISION: 1, 

1367 SZ_DATA_TYPE: "00", 

1368 SZ_DATA_UNIT: "", 

1369 }, 

1370 "52": { # slot 0B 

1371 SZ_DESCRIPTION: "Sensor sensitivity (%)", 

1372 SZ_MIN_VALUE: 0, 

1373 SZ_MAX_VALUE: 25.0, 

1374 SZ_PRECISION: 0.1, 

1375 SZ_DATA_TYPE: "01", 

1376 SZ_DATA_UNIT: "%", 

1377 }, 

1378 "54": { # slot 0C 

1379 SZ_DESCRIPTION: "Moisture sensor overrun time (mins)", 

1380 SZ_MIN_VALUE: 15, 

1381 SZ_MAX_VALUE: 60, 

1382 SZ_PRECISION: 1, 

1383 SZ_DATA_TYPE: "00", 

1384 SZ_DATA_UNIT: "min", 

1385 }, 

1386 "75": { # slot 0D 

1387 SZ_DESCRIPTION: "Comfort temperature (°C)", 

1388 SZ_MIN_VALUE: 0.0, 

1389 SZ_MAX_VALUE: 30.0, 

1390 SZ_PRECISION: 0.01, 

1391 SZ_DATA_TYPE: "92", 

1392 SZ_DATA_UNIT: "°C", 

1393 }, 

1394 "95": { # slot 08 

1395 SZ_DESCRIPTION: "Boost mode Supply/exhaust fan rate (%)", 

1396 SZ_MIN_VALUE: 0.0, 

1397 SZ_MAX_VALUE: 1.0, 

1398 SZ_PRECISION: 0.005, 

1399 SZ_DATA_TYPE: "0F", 

1400 SZ_DATA_UNIT: "%", 

1401 }, 

1402} 

1403 

1404# ventilation speed description 

1405_31D9_FAN_INFO_VASCO: dict[int, str] = { 

1406 0x00: "off", 

1407 0x01: "1 (trickle)", # aka low 

1408 0x02: "2 (low)", # aka medium 

1409 0x03: "3 (medium)", # aka high 

1410 0x04: "4 (boost)", 

1411 0x05: "auto", 

1412 0xC8: "III (boost)", # same code sent for speed II and III, mode manual 

1413 0x50: "I (low)", 

1414 0x1E: "0 (very low)", 

1415} 

1416 

1417# ventilation speed 

1418_31DA_FAN_INFO: dict[int, str] = { 

1419 0x00: "off", 

1420 0x01: "speed 1, low", # aka low 

1421 0x02: "speed 2, medium", # aka medium 

1422 0x03: "speed 3, high", # aka high 

1423 0x04: "speed 4", 

1424 0x05: "speed 5", 

1425 0x06: "speed 6", 

1426 0x07: "speed 7", 

1427 0x08: "speed 8", 

1428 0x09: "speed 9", 

1429 0x0A: "speed 10", 

1430 0x0B: "speed 1 temporary override", # timer 

1431 0x0C: "speed 2 temporary override", # timer 

1432 0x0D: "speed 3 temporary override", # timer/boost? (timer 1, 2, 3) 

1433 0x0E: "speed 4 temporary override", 

1434 0x0F: "speed 5 temporary override", 

1435 0x10: "speed 6 temporary override", 

1436 0x11: "speed 7 temporary override", 

1437 0x12: "speed 8 temporary override", 

1438 0x13: "speed 9 temporary override", 

1439 0x14: "speed 10 temporary override", 

1440 0x15: "away", # absolute minimum speed 

1441 0x16: "absolute minimum", # trickle? 

1442 0x17: "boost", # absolute maximum", # boost? 

1443 0x18: "auto", 

1444 0x19: "auto_night", 

1445 0x1A: "-unknown 0x1A-", 

1446 0x1B: "-unknown 0x1B-", 

1447 0x1C: "-unknown 0x1C-", 

1448 0x1D: "-unknown 0x1D-", 

1449 0x1E: "-unknown 0x1E-", 

1450 0x1F: "-unknown 0x1F-", # static field, used as filter in parser_31da so keep same 

1451} 

1452 

1453# 

1454######################################################################################## 

1455# CODES_BY_ZONE_TYPE 

1456# 

1457# RAMSES_ZONES: dict[str, str] = { 

1458# "ALL": { 

1459# Code._0004: {I_: {}, RP: {}}, 

1460# Code._000C: {RP: {}}, 

1461# Code._000A: {I_: {}, RP: {}}, 

1462# Code._2309: {I_: {}, RP: {}}, 

1463# Code._2349: {I_: {}, RP: {}}, 

1464# Code._30C9: {I_: {}, RP: {}}, 

1465# }, 

1466# ZON_ROLE.RAD: { 

1467# Code._12B0: {I_: {}, RP: {}}, 

1468# "3150a": {}, 

1469# }, 

1470# ZON_ROLE.ELE: { 

1471# Code._0008: {I_: {}}, 

1472# Code._0009: {I_: {}}, 

1473# }, 

1474# ZON_ROLE.VAL: { 

1475# Code._0008: {I_: {}}, 

1476# Code._0009: {I_: {}}, 

1477# "3150a": {}, 

1478# }, 

1479# ZON_ROLE.UFH: { 

1480# Code._3150: {I_: {}}, 

1481# }, 

1482# ZON_ROLE.MIX: { 

1483# Code._0008: {I_: {}}, 

1484# "3150a": {}, 

1485# }, 

1486# ZON_ROLE.DHW: { 

1487# Code._10A0: {RQ: {}, RP: {}}, 

1488# Code._1260: {I_: {}}, 

1489# Code._1F41: {I_: {}}, 

1490# }, 

1491# } 

1492# RAMSES_ZONES_ALL = RAMSES_ZONES.pop("ALL") 

1493# RAMSES_ZONES_DHW = RAMSES_ZONES[ZON_ROLE.DHW] 

1494# [RAMSES_ZONES[k].update(RAMSES_ZONES_ALL) for k in RAMSES_ZONES if k != ZON_ROLE.DHW] 

1495 

1496__all__ = [ 

1497 "CODES_BY_DEV_SLUG", 

1498 "CODES_SCHEMA", 

1499 "CODE_NAME_LOOKUP", 

1500 "CODES_BY_DEV_SLUG", 

1501 "CODES_SCHEMA", 

1502 "HVAC_KLASS_BY_VC_PAIR", 

1503 "_2411_PARAMS_SCHEMA", 

1504 "SZ_DESCRIPTION", 

1505 "SZ_MIN_VALUE", 

1506 "SZ_MAX_VALUE", 

1507 "SZ_PRECISION", 

1508 "SZ_DATA_TYPE", 

1509 "SZ_DATA_UNIT", 

1510]