ramses-rf 0.22.40__py3-none-any.whl → 0.51.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. ramses_cli/__init__.py +18 -0
  2. ramses_cli/client.py +597 -0
  3. ramses_cli/debug.py +20 -0
  4. ramses_cli/discovery.py +405 -0
  5. ramses_cli/utils/cat_slow.py +17 -0
  6. ramses_cli/utils/convert.py +60 -0
  7. ramses_rf/__init__.py +31 -10
  8. ramses_rf/binding_fsm.py +787 -0
  9. ramses_rf/const.py +124 -105
  10. ramses_rf/database.py +297 -0
  11. ramses_rf/device/__init__.py +69 -39
  12. ramses_rf/device/base.py +187 -376
  13. ramses_rf/device/heat.py +540 -552
  14. ramses_rf/device/hvac.py +286 -171
  15. ramses_rf/dispatcher.py +153 -177
  16. ramses_rf/entity_base.py +478 -361
  17. ramses_rf/exceptions.py +82 -0
  18. ramses_rf/gateway.py +377 -513
  19. ramses_rf/helpers.py +57 -19
  20. ramses_rf/py.typed +0 -0
  21. ramses_rf/schemas.py +148 -194
  22. ramses_rf/system/__init__.py +16 -23
  23. ramses_rf/system/faultlog.py +363 -0
  24. ramses_rf/system/heat.py +295 -302
  25. ramses_rf/system/schedule.py +312 -198
  26. ramses_rf/system/zones.py +318 -238
  27. ramses_rf/version.py +2 -8
  28. ramses_rf-0.51.1.dist-info/METADATA +72 -0
  29. ramses_rf-0.51.1.dist-info/RECORD +55 -0
  30. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.1.dist-info}/WHEEL +1 -2
  31. ramses_rf-0.51.1.dist-info/entry_points.txt +2 -0
  32. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.1.dist-info/licenses}/LICENSE +1 -1
  33. ramses_tx/__init__.py +160 -0
  34. {ramses_rf/protocol → ramses_tx}/address.py +65 -59
  35. ramses_tx/command.py +1454 -0
  36. ramses_tx/const.py +903 -0
  37. ramses_tx/exceptions.py +92 -0
  38. {ramses_rf/protocol → ramses_tx}/fingerprints.py +56 -15
  39. {ramses_rf/protocol → ramses_tx}/frame.py +132 -131
  40. ramses_tx/gateway.py +338 -0
  41. ramses_tx/helpers.py +883 -0
  42. {ramses_rf/protocol → ramses_tx}/logger.py +67 -53
  43. {ramses_rf/protocol → ramses_tx}/message.py +155 -191
  44. ramses_tx/opentherm.py +1260 -0
  45. ramses_tx/packet.py +210 -0
  46. {ramses_rf/protocol → ramses_tx}/parsers.py +1266 -1003
  47. ramses_tx/protocol.py +801 -0
  48. ramses_tx/protocol_fsm.py +672 -0
  49. ramses_tx/py.typed +0 -0
  50. {ramses_rf/protocol → ramses_tx}/ramses.py +262 -185
  51. {ramses_rf/protocol → ramses_tx}/schemas.py +150 -133
  52. ramses_tx/transport.py +1471 -0
  53. ramses_tx/typed_dicts.py +492 -0
  54. ramses_tx/typing.py +181 -0
  55. ramses_tx/version.py +4 -0
  56. ramses_rf/discovery.py +0 -398
  57. ramses_rf/protocol/__init__.py +0 -59
  58. ramses_rf/protocol/backports.py +0 -42
  59. ramses_rf/protocol/command.py +0 -1576
  60. ramses_rf/protocol/const.py +0 -697
  61. ramses_rf/protocol/exceptions.py +0 -111
  62. ramses_rf/protocol/helpers.py +0 -390
  63. ramses_rf/protocol/opentherm.py +0 -1170
  64. ramses_rf/protocol/packet.py +0 -235
  65. ramses_rf/protocol/protocol.py +0 -613
  66. ramses_rf/protocol/transport.py +0 -1011
  67. ramses_rf/protocol/version.py +0 -10
  68. ramses_rf/system/hvac.py +0 -82
  69. ramses_rf-0.22.40.dist-info/METADATA +0 -64
  70. ramses_rf-0.22.40.dist-info/RECORD +0 -42
  71. ramses_rf-0.22.40.dist-info/top_level.txt +0 -1
ramses_tx/const.py ADDED
@@ -0,0 +1,903 @@
1
+ #!/usr/bin/env python3
2
+ """RAMSES RF - a RAMSES-II protocol decoder & analyser."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ from enum import EnumCheck, IntEnum, StrEnum, verify
8
+ from types import SimpleNamespace
9
+ from typing import Any, Final, Literal, NoReturn
10
+
11
+ __dev_mode__ = False # NOTE: this is const.py
12
+ DEV_MODE = __dev_mode__
13
+
14
+ # used by protocol QoS FSM (echo tout is different? for MQTT)...
15
+ DEFAULT_DISABLE_QOS: Final[bool | None] = None
16
+ DEFAULT_WAIT_FOR_REPLY: Final[bool | None] = None
17
+
18
+ DEFAULT_ECHO_TIMEOUT: Final[float] = 0.50 # waiting for echo pkt after cmd sent
19
+ DEFAULT_RPLY_TIMEOUT: Final[float] = 0.50 # waiting for reply pkt after echo pkt rcvd
20
+ DEFAULT_BUFFER_SIZE: Final[int] = 32
21
+
22
+ DEFAULT_SEND_TIMEOUT: Final[float] = 20.0 # total waiting for successful send: FIXME
23
+ MAX_SEND_TIMEOUT: Final[float] = 20.0 # for a command to be sent, incl. queuing time
24
+
25
+ MAX_RETRY_LIMIT: Final[int] = 3 # for a command to be re-sent (not incl. 1st send)
26
+
27
+ MIN_INTER_WRITE_GAP: Final[float] = 0.05 # seconds
28
+ DEFAULT_GAP_DURATION: Final[float] = MIN_INTER_WRITE_GAP
29
+ DEFAULT_MAX_RETRIES: Final[int] = 3
30
+ DEFAULT_NUM_REPEATS: Final[int] = 0
31
+
32
+ SZ_QOS: Final = "qos"
33
+
34
+ SZ_CALLBACK: Final = "callback"
35
+ SZ_GAP_DURATION: Final = "gap_duration"
36
+ SZ_MAX_RETRIES: Final = "max_retries"
37
+ SZ_NUM_REPEATS: Final = "num_repeats"
38
+ SZ_PRIORITY: Final = "priority"
39
+ SZ_TIMEOUT: Final = "timeout"
40
+
41
+
42
+ # used by transport...
43
+ SZ_ACTIVE_HGI: Final = "active_gwy"
44
+ SZ_SIGNATURE: Final = "signature"
45
+ SZ_IS_EVOFW3: Final = "is_evofw3"
46
+
47
+ # default values for transmit rate governers...
48
+ DUTY_CYCLE_DURATION = 60 # time window (seconds) where rate limiting occurs
49
+ MAX_DUTY_CYCLE_RATE = 0.01 # % bandwidth used per cycle
50
+ MAX_TRANSMIT_RATE_TOKENS = 80 # transmits per cycle
51
+
52
+
53
+ # used by schedule.py...
54
+ SZ_FRAGMENT: Final = "fragment"
55
+ SZ_FRAG_NUMBER: Final = "frag_number"
56
+ SZ_FRAG_LENGTH: Final = "frag_length"
57
+ SZ_TOTAL_FRAGS: Final = "total_frags"
58
+
59
+ SZ_SCHEDULE: Final = "schedule"
60
+ SZ_CHANGE_COUNTER: Final = "change_counter"
61
+
62
+ SZ_SENSOR_FAULT: Final = "sensor_fault"
63
+
64
+
65
+ # used by 31DA
66
+ SZ_AIR_QUALITY: Final = "air_quality"
67
+ SZ_AIR_QUALITY_BASIS: Final = "air_quality_basis"
68
+ SZ_BOOST_TIMER: Final = "boost_timer"
69
+ SZ_BYPASS_MODE: Final = "bypass_mode"
70
+ SZ_BYPASS_POSITION: Final = "bypass_position"
71
+ SZ_BYPASS_STATE: Final = "bypass_state"
72
+ SZ_CO2_LEVEL: Final = "co2_level"
73
+ SZ_DEWPOINT_TEMP: Final = "dewpoint_temp"
74
+ SZ_EXHAUST_FAN_SPEED: Final = "exhaust_fan_speed"
75
+ SZ_EXHAUST_FLOW: Final = "exhaust_flow"
76
+ SZ_EXHAUST_TEMP: Final = "exhaust_temp"
77
+ SZ_FAN_INFO: Final = "fan_info"
78
+ SZ_FAN_MODE: Final = "fan_mode"
79
+ SZ_FAN_RATE: Final = "fan_rate"
80
+ SZ_FILTER_REMAINING: Final = "filter_remaining"
81
+ SZ_INDOOR_HUMIDITY: Final = "indoor_humidity"
82
+ SZ_INDOOR_TEMP: Final = "indoor_temp"
83
+ SZ_OUTDOOR_HUMIDITY: Final = "outdoor_humidity"
84
+ SZ_OUTDOOR_TEMP: Final = "outdoor_temp"
85
+ SZ_POST_HEAT: Final = "post_heat"
86
+ SZ_PRE_HEAT: Final = "pre_heat"
87
+ SZ_REL_HUMIDITY: Final = "rel_humidity"
88
+ SZ_REMAINING_DAYS: Final = "days_remaining"
89
+ SZ_REMAINING_MINS: Final = "remaining_mins"
90
+ SZ_REMAINING_PERCENT: Final = "percent_remaining"
91
+ SZ_SUPPLY_FAN_SPEED: Final = "supply_fan_speed"
92
+ SZ_SUPPLY_FLOW: Final = "supply_flow"
93
+ SZ_SUPPLY_TEMP: Final = "supply_temp"
94
+ SZ_SPEED_CAPABILITIES: Final = "speed_capabilities"
95
+
96
+ SZ_PRESENCE_DETECTED: Final = "presence_detected"
97
+
98
+
99
+ # used by OTB
100
+ SZ_BURNER_HOURS: Final = "burner_hours"
101
+ SZ_BURNER_STARTS: Final = "burner_starts"
102
+ SZ_BURNER_FAILED_STARTS: Final = "burner_failed_starts"
103
+ SZ_CH_PUMP_HOURS: Final = "ch_pump_hours"
104
+ SZ_CH_PUMP_STARTS: Final = "ch_pump_starts"
105
+ SZ_DHW_BURNER_HOURS: Final = "dhw_burner_hours"
106
+ SZ_DHW_BURNER_STARTS: Final = "dhw_burner_starts"
107
+ SZ_DHW_PUMP_HOURS: Final = "dhw_pump_hours"
108
+ SZ_DHW_PUMP_STARTS: Final = "dhw_pump_starts"
109
+ SZ_FLAME_SIGNAL_LOW: Final = "flame_signal_low"
110
+
111
+ SZ_BOILER_OUTPUT_TEMP: Final = "boiler_output_temp"
112
+ SZ_BOILER_RETURN_TEMP: Final = "boiler_return_temp"
113
+ SZ_BOILER_SETPOINT: Final = "boiler_setpoint"
114
+ SZ_CH_MAX_SETPOINT: Final = "ch_max_setpoint"
115
+ SZ_CH_SETPOINT: Final = "ch_setpoint"
116
+ SZ_CH_WATER_PRESSURE: Final = "ch_water_pressure"
117
+ SZ_DHW_FLOW_RATE: Final = "dhw_flow_rate"
118
+ SZ_DHW_SETPOINT: Final = "dhw_setpoint"
119
+ SZ_DHW_TEMP: Final = "dhw_temp"
120
+ SZ_MAX_REL_MODULATION: Final = "max_rel_modulation"
121
+ # SZ_OEM_CODE:Final[str] = "oem_code"
122
+ SZ_OUTSIDE_TEMP: Final = "outside_temp"
123
+ SZ_REL_MODULATION_LEVEL: Final = "rel_modulation_level"
124
+
125
+ SZ_CH_ACTIVE: Final = "ch_active"
126
+ SZ_CH_ENABLED: Final = "ch_enabled"
127
+ SZ_COOLING_ACTIVE: Final = "cooling_active"
128
+ SZ_COOLING_ENABLED: Final = "cooling_enabled"
129
+ SZ_DHW_ACTIVE: Final = "dhw_active"
130
+ SZ_DHW_BLOCKING: Final = "dhw_blocking"
131
+ SZ_DHW_ENABLED: Final = "dhw_enabled"
132
+ SZ_FAULT_PRESENT: Final = "fault_present"
133
+ SZ_FLAME_ACTIVE: Final = "flame_active"
134
+ SZ_SUMMER_MODE: Final = "summer_mode"
135
+ SZ_OTC_ACTIVE: Final = "otc_active"
136
+
137
+
138
+ @verify(EnumCheck.UNIQUE)
139
+ class Priority(IntEnum):
140
+ LOWEST = 4
141
+ LOW = 2
142
+ DEFAULT = 0
143
+ HIGH = -2
144
+ HIGHEST = -4
145
+
146
+
147
+ def slug(string: str) -> str:
148
+ """Convert a string to snake_case."""
149
+ return re.sub(r"[\W_]+", "_", string.lower())
150
+
151
+
152
+ # TODO: FIXME: This is a mess - needs converting to StrEnum
153
+ class AttrDict(dict): # type: ignore[type-arg]
154
+ _SZ_AKA_SLUG: Final = "_root_slug"
155
+ _SZ_DEFAULT: Final = "_default"
156
+ _SZ_SLUGS: Final = "SLUGS"
157
+
158
+ @classmethod
159
+ def __readonly(cls, *args: Any, **kwargs: Any) -> NoReturn:
160
+ raise TypeError(f"'{cls.__class__.__name__}' object is read only")
161
+
162
+ __delitem__ = __readonly
163
+ __setitem__ = __readonly
164
+ clear = __readonly
165
+ pop = __readonly
166
+ popitem = __readonly
167
+ setdefault = __readonly
168
+ update = __readonly
169
+
170
+ del __readonly
171
+
172
+ def __init__(self, main_table: dict[str, dict], attr_table: dict[str, Any]) -> None: # type: ignore[type-arg]
173
+ self._main_table = main_table
174
+ self._attr_table = attr_table
175
+ self._attr_table[self._SZ_SLUGS] = tuple(sorted(main_table.keys()))
176
+
177
+ self._slug_lookup: dict = { # type: ignore[type-arg]
178
+ None: slug # noqa: B035
179
+ for slug, table in main_table.items()
180
+ for k in table.values()
181
+ if isinstance(k, str) and table.get(self._SZ_DEFAULT)
182
+ } # i.e. {None: 'HEA'}
183
+ self._slug_lookup.update(
184
+ {
185
+ k: table.get(self._SZ_AKA_SLUG, slug)
186
+ for slug, table in main_table.items()
187
+ for k in table
188
+ if isinstance(k, str) and len(k) == 2
189
+ } # e.g. {'00': 'TRV', '01': 'CTL', '04': 'TRV', ...}
190
+ )
191
+ self._slug_lookup.update(
192
+ {
193
+ k: slug
194
+ for slug, table in main_table.items()
195
+ for k in table.values()
196
+ if isinstance(k, str) and table.get(self._SZ_AKA_SLUG) is None
197
+ } # e.g. {'heat_device':'HEA', 'dhw_sensor':'DHW', ...}
198
+ )
199
+
200
+ self._forward = {
201
+ k: v
202
+ for table in main_table.values()
203
+ for k, v in table.items()
204
+ if isinstance(k, str) and k[:1] != "_"
205
+ } # e.g. {'00': 'radiator_valve', '01': 'controller', ...}
206
+ self._reverse = {
207
+ v: k
208
+ for table in main_table.values()
209
+ for k, v in table.items()
210
+ if isinstance(k, str) and k[:1] != "_" and self._SZ_AKA_SLUG not in table
211
+ } # e.g. {'radiator_valve': '00', 'controller': '01', ...}
212
+ self._forward = dict(sorted(self._forward.items(), key=lambda item: item[0]))
213
+
214
+ super().__init__(self._forward)
215
+
216
+ def __getitem__(self, key: str) -> Any:
217
+ if key in self._main_table: # map[ZON_ROLE.DHW] -> "dhw_sensor"
218
+ return list(self._main_table[key].values())[0]
219
+ # if key in self._forward: # map["0D"] -> "dhw_sensor"
220
+ # return self._forward.__getitem__(key)
221
+ if key in self._reverse: # map["dhw_sensor"] -> "0D"
222
+ return self._reverse.__getitem__(key)
223
+ return super().__getitem__(key)
224
+
225
+ def __getattr__(self, name: str) -> Any:
226
+ if name in self._main_table: # map.DHW -> "0D" (using slug)
227
+ if (result := list(self._main_table[name].keys())[0]) is not None:
228
+ return result
229
+ elif name in self._attr_table: # bespoke attrs
230
+ return self._attr_table[name]
231
+ elif len(name) and name[1:] in self._forward: # map._0D -> "dhw_sensor"
232
+ return self._forward[name[1:]]
233
+ elif name.isupper() and name.lower() in self._reverse: # map.DHW_SENSOR -> "0D"
234
+ return self[name.lower()]
235
+ return self.__getattribute__(name)
236
+
237
+ def _hex(self, key: str) -> str:
238
+ """Return the key/ID (2-byte hex string) of the two-way dict (e.g. '04')."""
239
+ if key in self._main_table:
240
+ return list(self._main_table[key].keys())[0] # type: ignore[no-any-return]
241
+ if key in self._reverse:
242
+ return self._reverse[key]
243
+ raise KeyError(key)
244
+
245
+ def _str(self, key: str) -> str:
246
+ """Return the value (string) of the two-way dict (e.g. 'radiator_valve')."""
247
+ if key in self._main_table:
248
+ return list(self._main_table[key].values())[0] # type: ignore[no-any-return]
249
+ if key in self:
250
+ return self[key] # type: ignore[no-any-return]
251
+ raise KeyError(key)
252
+
253
+ # def values(self):
254
+ # return {k: k for k in super().values()}.values()
255
+
256
+ def slug(self, key: str) -> str:
257
+ """WIP: Return master slug for a hex key/ID (e.g. 00 -> 'TRV', not 'TR0')."""
258
+ slug_ = self._slug_lookup[key]
259
+ # if slug_ in self._attr_table["_TRANSFORMS"]:
260
+ # return self._attr_table["_TRANSFORMS"][slug_]
261
+ return slug_ # type: ignore[no-any-return]
262
+
263
+ def slugs(self) -> tuple[str]:
264
+ """Return the slugs from the main table."""
265
+ return self._attr_table[self._SZ_SLUGS] # type: ignore[no-any-return]
266
+
267
+
268
+ def attr_dict_factory(
269
+ main_table: dict[str, dict], # type: ignore[type-arg]
270
+ attr_table: dict | None = None, # type: ignore[type-arg]
271
+ ) -> AttrDict: # is: SlottedAttrDict
272
+ if attr_table is None:
273
+ attr_table = {}
274
+
275
+ class SlottedAttrDict(AttrDict):
276
+ pass # TODO: low priority
277
+ # __slots__ = (
278
+ # list(main_table.keys())
279
+ # + [
280
+ # f"_{k}"
281
+ # for t in main_table.values()
282
+ # for k in t.keys()
283
+ # if isinstance(k, str) and len(k) == 2
284
+ # ]
285
+ # + [v for t in main_table.values() for v in t.values()]
286
+ # + list(attr_table.keys())
287
+ # + [AttrDict._SZ_AKA_SLUG, AttrDict._SZ_SLUGS]
288
+ # )
289
+
290
+ return SlottedAttrDict(main_table, attr_table=attr_table)
291
+
292
+
293
+ # slugs for device/zone entity klasses, used by 0005/000C
294
+ @verify(EnumCheck.UNIQUE)
295
+ class DevRole(StrEnum):
296
+ #
297
+ # Generic device/zone classes
298
+ ACT = "ACT" # Generic heating zone actuator group
299
+ SEN = "SEN" # Generic heating zone sensor group
300
+ #
301
+ # Standard device/zone classes
302
+ ELE = "ELE" # BDRs (no heat demand)
303
+ MIX = "MIX" # HM8s
304
+ RAD = "RAD" # TRVs
305
+ UFH = "UFH" # UFC (circuits)
306
+ VAL = "VAL" # BDRs
307
+ #
308
+ # DHW device/zone classes
309
+ DHW = "DHW" # DHW sensor (a zone, but not a heating zone)
310
+ HTG = "HTG" # BDR (DHW relay, HTG relay)
311
+ HT1 = "HT1" # BDR (HTG relay)
312
+ #
313
+ # Other device/zone classes
314
+ OUT = "OUT" # OUT (external weather sensor)
315
+ RFG = "RFG" # RFG
316
+ APP = "APP" # BDR/OTB (appliance relay)
317
+
318
+
319
+ DEV_ROLE_MAP = attr_dict_factory(
320
+ {
321
+ DevRole.ACT: {"00": "zone_actuator"},
322
+ DevRole.SEN: {"04": "zone_sensor"},
323
+ DevRole.RAD: {"08": "rad_actuator"},
324
+ DevRole.UFH: {"09": "ufh_actuator"},
325
+ DevRole.VAL: {"0A": "val_actuator"},
326
+ DevRole.MIX: {"0B": "mix_actuator"},
327
+ DevRole.OUT: {"0C": "out_sensor"},
328
+ DevRole.DHW: {"0D": "dhw_sensor"},
329
+ DevRole.HTG: {"0E": "hotwater_valve"}, # payload[:4] == 000E
330
+ DevRole.HT1: {None: "heating_valve"}, # payload[:4] == 010E
331
+ DevRole.APP: {"0F": "appliance_control"}, # the heat/cool source
332
+ DevRole.RFG: {"10": "remote_gateway"},
333
+ DevRole.ELE: {"11": "ele_actuator"}, # ELE(VAL) - no RP from older evos
334
+ }, # 03, 05, 06, 07: & >11 - no response from an 01:
335
+ {
336
+ "HEAT_DEVICES": ("00", "04", "08", "09", "0A", "0B", "11"),
337
+ "DHW_DEVICES": ("0D", "0E"),
338
+ "SENSORS": ("04", "0C", "0D"),
339
+ },
340
+ )
341
+
342
+
343
+ # slugs for device entity types, used in device_ids
344
+ @verify(EnumCheck.UNIQUE)
345
+ class DevType(StrEnum):
346
+ #
347
+ # Promotable/Generic devices
348
+ DEV = "DEV" # xx: Promotable device
349
+ HEA = "HEA" # xx: Promotable Heat device, aka CH/DHW device
350
+ HVC = "HVC" # xx: Promotable HVAC device
351
+ THM = "THM" # xx: Generic thermostat
352
+ #
353
+ # Heat (CH/DHW) devices
354
+ BDR = "BDR" # 13: Electrical relay
355
+ CTL = "CTL" # 01: Controller (zoned)
356
+ DHW = "DHW" # 07: DHW sensor
357
+ DTS = "DTS" # 12: Thermostat, DTS92(E)
358
+ DT2 = "DT2" # 22: Thermostat, DTS92(E)
359
+ HCW = "HCW" # 03: Thermostat - don't use STA
360
+ HGI = "HGI" # 18: Gateway interface (RF to USB), HGI80
361
+ # 8 = "HM8" # xx: HM80 mixer valve (Rx-only, does not Tx)
362
+ OTB = "OTB" # 10: OpenTherm bridge
363
+ OUT = "OUT" # 17: External weather sensor
364
+ PRG = "PRG" # 23: Programmer
365
+ RFG = "RFG" # 30: RF gateway (RF to ethernet), RFG100
366
+ RND = "RND" # 34: Thermostat, TR87RF
367
+ TRV = "TRV" # 04: Thermostatic radiator valve
368
+ TR0 = "TR0" # 00: Thermostatic radiator valve
369
+ UFC = "UFC" # 02: UFH controller
370
+ #
371
+ # Honeywell Jasper, other Heat devices
372
+ JIM = "JIM" # 08: Jasper Interface Module (EIM?)
373
+ JST = "JST" # 31: Jasper Stat
374
+ #
375
+ # HVAC devices, these are more like classes (i.e. no reliable device type)
376
+ RFS = "RFS" # ??: HVAC spIDer gateway
377
+ FAN = "FAN" # ??: HVAC fan, 31D[9A]: 20|29|30|37 (some, e.g. 29: only 31D9)
378
+ CO2 = "CO2" # ??: HVAC CO2 sensor
379
+ HUM = "HUM" # ??: HVAC humidity sensor, 1260: 32
380
+ PIR = "PIR" # ??: HVAC pesence sensor, 2E10
381
+ REM = "REM" # ??: HVAC switch, 22F[13]: 02|06|20|32|39|42|49|59 (no 20: are both)
382
+ SW2 = "SW2" # ??: HVAC switch, Orcon variant
383
+ DIS = "DIS" # ??: HVAC switch with display
384
+
385
+
386
+ DEV_TYPE_MAP = attr_dict_factory(
387
+ {
388
+ # Generic devices (would be promoted)
389
+ DevType.DEV: {None: "generic_device"}, # , AttrDict._SZ_DEFAULT: True},
390
+ DevType.HEA: {None: "heat_device"},
391
+ DevType.HVC: {None: "hvac_device"},
392
+ # HGI80
393
+ DevType.HGI: {"18": "gateway_interface"}, # HGI80
394
+ # Heat (CH/DHW) devices
395
+ DevType.TR0: {"00": "radiator_valve", AttrDict._SZ_AKA_SLUG: DevType.TRV},
396
+ DevType.CTL: {"01": "controller"},
397
+ DevType.UFC: {"02": "ufh_controller"},
398
+ DevType.HCW: {"03": "analog_thermostat"},
399
+ DevType.THM: {None: "thermostat"},
400
+ DevType.TRV: {"04": "radiator_valve"},
401
+ DevType.DHW: {"07": "dhw_sensor"},
402
+ DevType.OTB: {"10": "opentherm_bridge"},
403
+ DevType.DTS: {"12": "digital_thermostat"},
404
+ DevType.BDR: {"13": "electrical_relay"},
405
+ DevType.OUT: {"17": "outdoor_sensor"},
406
+ DevType.DT2: {"22": "digital_thermostat", AttrDict._SZ_AKA_SLUG: DevType.DTS},
407
+ DevType.PRG: {"23": "programmer"},
408
+ DevType.RFG: {"30": "rf_gateway"}, # RFG100
409
+ DevType.RND: {"34": "round_thermostat"},
410
+ # Other (jasper) devices
411
+ DevType.JIM: {"08": "jasper_interface"},
412
+ DevType.JST: {"31": "jasper_thermostat"},
413
+ # Ventilation devices
414
+ DevType.CO2: {None: "co2_sensor"},
415
+ DevType.DIS: {None: "switch_display"},
416
+ DevType.FAN: {None: "ventilator"}, # Both Fans and HRUs
417
+ DevType.HUM: {None: "rh_sensor"},
418
+ DevType.PIR: {None: "presence_sensor"},
419
+ DevType.RFS: {None: "hvac_gateway"}, # Spider
420
+ DevType.REM: {None: "switch"},
421
+ DevType.SW2: {None: "switch_variant"},
422
+ },
423
+ {
424
+ "HEAT_DEVICES": (
425
+ "00",
426
+ "01",
427
+ "02",
428
+ "03",
429
+ "04",
430
+ "07",
431
+ "10",
432
+ "12",
433
+ "13",
434
+ "17",
435
+ "22",
436
+ "30",
437
+ "34",
438
+ ), # CH/DHW devices instead of HVAC/other
439
+ "HEAT_ZONE_SENSORS": ("00", "01", "03", "04", "12", "22", "34"),
440
+ "HEAT_ZONE_ACTUATORS": ("00", "02", "04", "13"),
441
+ "THM_DEVICES": ("03", "12", "22", "34"),
442
+ "TRV_DEVICES": ("00", "04"),
443
+ "CONTROLLERS": ("01", "12", "22", "23", "34"), # potentially controllers
444
+ "PROMOTABLE_SLUGS": (DevType.DEV, DevType.HEA, DevType.HVC),
445
+ "HVAC_SLUGS": {
446
+ DevType.CO2: "co2_sensor",
447
+ DevType.FAN: "ventilator", # Both Fans and HRUs
448
+ DevType.HUM: "rh_sensor",
449
+ DevType.RFS: "hvac_gateway", # Spider
450
+ DevType.REM: "switch",
451
+ },
452
+ },
453
+ )
454
+
455
+
456
+ # slugs for zone entity klasses, used by 0005/000C
457
+ class ZoneRole(StrEnum):
458
+ #
459
+ # Generic device/zone classes
460
+ ACT = "ACT" # Generic heating zone actuator group
461
+ SEN = "SEN" # Generic heating zone sensor group
462
+ #
463
+ # Standard device/zone classes
464
+ ELE = "ELE" # heating zone with BDRs (no heat demand)
465
+ MIX = "MIX" # heating zone with HM8s
466
+ RAD = "RAD" # heating zone with TRVs
467
+ UFH = "UFH" # heating zone with UFC circuits
468
+ VAL = "VAL" # zheating one with BDRs
469
+ # Standard device/zone classes *not a heating zone)
470
+ DHW = "DHW" # DHW zone with BDRs
471
+
472
+
473
+ ZON_ROLE_MAP = attr_dict_factory(
474
+ {
475
+ ZoneRole.ACT: {"00": "heating_zone"}, # any actuator
476
+ ZoneRole.SEN: {"04": "heating_zone"}, # any sensor
477
+ ZoneRole.RAD: {"08": "radiator_valve"}, # TRVs
478
+ ZoneRole.UFH: {"09": "underfloor_heating"}, # UFCs
479
+ ZoneRole.VAL: {"0A": "zone_valve"}, # BDRs
480
+ ZoneRole.MIX: {"0B": "mixing_valve"}, # HM8s
481
+ ZoneRole.DHW: {"0D": "stored_hotwater"}, # DHWs
482
+ # N_CLASS.HTG: {"0E": "stored_hotwater", AttrDict._SZ_AKA_SLUG: ZON_ROLE.DHW},
483
+ ZoneRole.ELE: {"11": "electric_heat"}, # BDRs
484
+ },
485
+ {
486
+ "HEAT_ZONES": ("08", "09", "0A", "0B", "11"),
487
+ },
488
+ )
489
+
490
+ # Zone modes
491
+ ZON_MODE_MAP = attr_dict_factory(
492
+ {
493
+ "FOLLOW": {"00": "follow_schedule"},
494
+ "ADVANCED": {"01": "advanced_override"}, # . until the next scheduled setpoint
495
+ "PERMANENT": {"02": "permanent_override"}, # indefinitely, until auto_reset
496
+ "COUNTDOWN": {"03": "countdown_override"}, # for x mins (duration, max 1,215?)
497
+ "TEMPORARY": {"04": "temporary_override"}, # until a given date/time (until)
498
+ }
499
+ )
500
+
501
+ # System modes
502
+ SYS_MODE_MAP = attr_dict_factory(
503
+ {
504
+ "au_00": {"00": "auto"}, # . indef (only)
505
+ "ho_01": {"01": "heat_off"}, # . indef (only)
506
+ "eb_02": {"02": "eco_boost"}, # . indef/<=24h: is either Eco, *or* Boost
507
+ "aw_03": {"03": "away"}, # . indef/<=99d (0d = end of today, 00:00)
508
+ "do_04": {"04": "day_off"}, # . indef/<=99d: rounded down to 00:00 by CTL
509
+ "de_05": {"05": "day_off_eco"}, # . indef/<=99d: set to Eco when DayOff ends
510
+ "ar_06": {"06": "auto_with_reset"}, # indef (only)
511
+ "cu_07": {"07": "custom"}, # . indef/<=99d
512
+ }
513
+ )
514
+
515
+
516
+ SZ_ACTIVE: Final = "active"
517
+ SZ_ACTUATOR: Final = "actuator"
518
+ SZ_ACTUATORS: Final = "actuators"
519
+ SZ_BINDINGS: Final = "bindings"
520
+ SZ_CONFIG: Final = "config"
521
+ SZ_DATETIME: Final = "datetime"
522
+ SZ_DEMAND: Final = "demand"
523
+ SZ_DEVICE_ID: Final = "device_id"
524
+ SZ_DEVICE_ROLE: Final = "device_role"
525
+ SZ_DEVICES: Final = "devices"
526
+ SZ_DHW_IDX: Final = "dhw_idx"
527
+ SZ_DOMAIN_ID: Final = "domain_id"
528
+ SZ_DURATION: Final = "duration"
529
+ SZ_HEAT_DEMAND: Final = "heat_demand"
530
+ SZ_IS_DST: Final = "is_dst"
531
+ SZ_LANGUAGE: Final = "language"
532
+ SZ_LOCAL_OVERRIDE: Final = "local_override"
533
+ SZ_MAX_TEMP: Final = "max_temp"
534
+ SZ_MIN_TEMP: Final = "min_temp"
535
+ SZ_MIX_CONFIG: Final = "mix_config"
536
+ SZ_MODE: Final = "mode"
537
+ SZ_MULTIROOM_MODE: Final = "multiroom_mode"
538
+ SZ_NAME: Final = "name"
539
+ SZ_OEM_CODE: Final = "oem_code"
540
+ SZ_OPENWINDOW_FUNCTION: Final = "openwindow_function"
541
+ SZ_PAYLOAD: Final = "payload"
542
+ SZ_PERCENTAGE: Final = "percentage"
543
+ SZ_PRESSURE: Final = "pressure"
544
+ SZ_RELAY_DEMAND: Final = "relay_demand"
545
+ SZ_RELAY_FAILSAFE: Final = "relay_failsafe"
546
+ SZ_SENSOR: Final = "sensor"
547
+ SZ_SETPOINT: Final = "setpoint"
548
+ SZ_SETPOINT_BOUNDS: Final = "setpoint_bounds"
549
+ SZ_SLUG: Final = "_SLUG"
550
+ SZ_SYSTEM_MODE: Final = "system_mode"
551
+ SZ_TEMPERATURE: Final = "temperature"
552
+ SZ_UFH_IDX: Final = "ufh_idx"
553
+ SZ_UNKNOWN: Final = "unknown"
554
+ SZ_UNTIL: Final = "until"
555
+ SZ_VALUE: Final = "value"
556
+ SZ_WINDOW_OPEN: Final = "window_open"
557
+ SZ_ZONE_CLASS: Final = "zone_class"
558
+ SZ_ZONE_IDX: Final = "zone_idx"
559
+ SZ_ZONE_MASK: Final = "zone_mask"
560
+ SZ_ZONE_TYPE: Final = "zone_type"
561
+ SZ_ZONES: Final = "zones"
562
+
563
+ # used in 0418 only?
564
+ SZ_DEVICE_CLASS: Final = "device_class"
565
+ # _DEVICE_ID: Final = "device_id"
566
+ SZ_DOMAIN_IDX: Final = "domain_idx"
567
+ SZ_FAULT_STATE: Final = "fault_state"
568
+ SZ_FAULT_TYPE: Final = "fault_type"
569
+ SZ_LOG_ENTRY: Final = "log_entry"
570
+ SZ_LOG_IDX: Final = "log_idx"
571
+ SZ_TIMESTAMP: Final = "timestamp"
572
+
573
+ # used in 1FC9
574
+ SZ_OFFER: Final = "offer"
575
+ SZ_ACCEPT: Final = "accept"
576
+ SZ_CONFIRM: Final = "confirm"
577
+ SZ_PHASE: Final = "phase"
578
+
579
+
580
+ DEFAULT_MAX_ZONES: Final = 16 if DEV_MODE else 12
581
+ # Evohome: 12 (0-11), older/initial version was 8
582
+ # Hometronics: 16 (0-15), or more?
583
+ # Sundial RF2: 2 (0-1), usually only one, but ST9520C can do two zones
584
+
585
+
586
+ DEVICE_ID_REGEX = SimpleNamespace(
587
+ ANY=re.compile(r"^[0-9]{2}:[0-9]{6}$"),
588
+ BDR=re.compile(r"^13:[0-9]{6}$"),
589
+ CTL=re.compile(r"^(01|23):[0-9]{6}$"),
590
+ DHW=re.compile(r"^07:[0-9]{6}$"),
591
+ HGI=re.compile(r"^18:[0-9]{6}$"),
592
+ APP=re.compile(r"^(10|13):[0-9]{6}$"),
593
+ UFC=re.compile(r"^02:[0-9]{6}$"),
594
+ SEN=re.compile(r"^(01|03|04|12|22|34):[0-9]{6}$"),
595
+ )
596
+
597
+ # Domains
598
+ F6: Final = "F6"
599
+ F7: Final = "F7"
600
+ F8: Final = "F8"
601
+ F9: Final = "F9"
602
+ FA: Final = "FA"
603
+ FB: Final = "FB"
604
+ FC: Final = "FC"
605
+ FD: Final = "FD"
606
+ FE: Final = "FE"
607
+ FF: Final = "FF"
608
+
609
+ DOMAIN_TYPE_MAP: dict[str, str] = {
610
+ F6: "cooling_valve", # cooling
611
+ F7: "domain_f7",
612
+ F8: "domain_f8",
613
+ F9: DEV_ROLE_MAP[DevRole.HT1], # Heating Valve
614
+ FA: DEV_ROLE_MAP[DevRole.HTG], # HW Valve (or UFH loop if src.type == UFC?)
615
+ FB: "domain_fb", # also: cooling valve?
616
+ FC: DEV_ROLE_MAP[DevRole.APP], # appliance_control
617
+ FD: "domain_fd", # seen with hometronics
618
+ # "FE": ???
619
+ # FF: "system", # TODO: remove this, is not a domain
620
+ } # "21": "Ventilation", "88": ???
621
+ DOMAIN_TYPE_LOOKUP = {v: k for k, v in DOMAIN_TYPE_MAP.items() if k != FF}
622
+
623
+ DHW_STATE_MAP: dict[str, str] = {"00": "off", "01": "on"}
624
+ DHW_STATE_LOOKUP = {v: k for k, v in DHW_STATE_MAP.items()}
625
+
626
+ DTM_LONG_REGEX = re.compile(
627
+ r"\d{4}-[01]\d-[0-3]\d(T| )[0-2]\d:[0-5]\d:[0-5]\d\.\d{6} ?"
628
+ ) # 2020-11-30T13:15:00.123456
629
+ DTM_TIME_REGEX = re.compile(r"[0-2]\d:[0-5]\d:[0-5]\d\.\d{3} ?") # 13:15:00.123
630
+
631
+ # Used by packet structure validators
632
+ r = r"(-{3}|\d{3}|\.{3})" # RSSI, '...' was used by an older version of evofw3
633
+ v = r"( I|RP|RQ| W)" # verb
634
+ d = r"(-{2}:-{6}|\d{2}:\d{6})" # device ID
635
+ c = r"[0-9A-F]{4}" # code
636
+ l = r"\d{3}" # length # noqa: E741
637
+ p = r"([0-9A-F]{2}){1,48}" # payload
638
+
639
+ # DEVICE_ID_REGEX = re.compile(f"^{d}$")
640
+ COMMAND_REGEX = re.compile(f"^{v} {r} {d} {d} {d} {c} {l} {p}$")
641
+ MESSAGE_REGEX = re.compile(f"^{r} {v} {r} {d} {d} {d} {c} {l} {p}$")
642
+
643
+
644
+ # Used by 0418/system_fault parser
645
+ class FaultDeviceClass(StrEnum):
646
+ CONTROLLER = "controller"
647
+ SENSOR = "sensor"
648
+ SETPOINT = "setpoint"
649
+ ACTUATOR = "actuator" # if domain is FC, then "boiler_relay"
650
+ DHW_ACTUATOR = "dhw_sensor"
651
+ RF_GATEWAY = "rf_gateway"
652
+ BOILER_RELAY = "boiler_relay"
653
+ UNKNOWN = "unknown"
654
+
655
+
656
+ FAULT_DEVICE_CLASS: Final[dict[str, FaultDeviceClass]] = {
657
+ "00": FaultDeviceClass.CONTROLLER,
658
+ "01": FaultDeviceClass.SENSOR,
659
+ "02": FaultDeviceClass.SETPOINT,
660
+ "04": FaultDeviceClass.ACTUATOR, # if domain is FC, then BOILER_RELAY
661
+ "05": FaultDeviceClass.DHW_ACTUATOR,
662
+ "06": FaultDeviceClass.RF_GATEWAY,
663
+ }
664
+
665
+
666
+ class FaultState(StrEnum):
667
+ FAULT = "fault"
668
+ RESTORE = "restore"
669
+ UNKNOWN_C0 = "unknown_c0"
670
+ UNKNOWN = "unknown"
671
+
672
+
673
+ FAULT_STATE: Final[dict[str, FaultState]] = { # a bitmap?
674
+ "00": FaultState.FAULT,
675
+ "40": FaultState.RESTORE,
676
+ "C0": FaultState.UNKNOWN_C0, # C0s do not appear in the evohome UI
677
+ }
678
+
679
+
680
+ class FaultType(StrEnum):
681
+ SYSTEM_FAULT = "system_fault"
682
+ MAINS_LOW = "mains_low"
683
+ BATTERY_LOW = "battery_low"
684
+ BATTERY_ERROR = "battery_error" # actually: 'evotouch_battery_error'
685
+ COMMS_FAULT = "comms_fault"
686
+ SENSOR_FAULT = "sensor_fault" # seen with zone sensor
687
+ SENSOR_ERROR = "sensor_error"
688
+ BAD_VALUE = "bad_value"
689
+ UNKNOWN = "unknown"
690
+
691
+
692
+ FAULT_TYPE: Final[dict[str, FaultType]] = {
693
+ "01": FaultType.SYSTEM_FAULT,
694
+ "03": FaultType.MAINS_LOW,
695
+ "04": FaultType.BATTERY_LOW,
696
+ "05": FaultType.BATTERY_ERROR, # actually: 'evotouch_battery_error'
697
+ "06": FaultType.COMMS_FAULT,
698
+ "07": FaultType.SENSOR_FAULT, # seen with zone sensor
699
+ "0A": FaultType.SENSOR_ERROR,
700
+ }
701
+
702
+
703
+ class SystemType(StrEnum):
704
+ CHRONOTHERM = "chronotherm"
705
+ EVOHOME = "evohome"
706
+ HOMETRONICS = "hometronics"
707
+ PROGRAMMER = "programmer"
708
+ SUNDIAL = "sundial"
709
+ GENERIC = "generic"
710
+
711
+
712
+ # used by 22Fx parser, and FanSwitch devices
713
+ # SZ_BOOST_TIMER:Final = "boost_timer" # minutes, e.g. 10, 20, 30 minutes
714
+ HEATER_MODE: Final = "heater_mode" # e.g. auto, off
715
+ FAN_MODE: Final = "fan_mode" # e.g. low. high # . deprecated, use SZ_FAN_MODE, to be removed in Q1 2026
716
+ FAN_RATE: Final = "fan_rate" # percentage, 0.0 - 1.0 # deprecated, use SZ_FAN_MODE, to be removed in Q1 2026
717
+
718
+
719
+ # RP --- 01:054173 18:006402 --:------ 0005 004 00100000 # before adding RFG100
720
+ # .I --- 01:054173 --:------ 01:054173 1FC9 012 0010E004D39D001FC904D39D
721
+ # .W --- 30:248208 01:054173 --:------ 1FC9 012 0010E07BC9900012907BC990
722
+ # .I --- 01:054173 30:248208 --:------ 1FC9 006 00FFFF04D39D
723
+
724
+ # RP --- 01:054173 18:006402 --:------ 0005 004 00100100 # after adding RFG100
725
+ # RP --- 01:054173 18:006402 --:------ 000C 006 0010007BC990 # 30:082155
726
+ # RP --- 01:054173 18:006402 --:------ 0005 004 00100100 # before deleting RFG from CTL
727
+ # .I --- 01:054173 --:------ 01:054173 0005 004 00100000 # when the RFG was deleted
728
+ # RP --- 01:054173 18:006402 --:------ 0005 004 00100000 # after deleting the RFG
729
+
730
+ # RP|zone_devices | 000E0... || {'domain_id': 'FA', 'device_role': 'dhw_valve', 'devices': ['13:081807']} # noqa: E501
731
+ # RP|zone_devices | 010E0... || {'domain_id': 'FA', 'device_role': 'htg_valve', 'devices': ['13:106039']} # noqa: E501
732
+
733
+ # Example of:
734
+ # - Sundial RF2 Pack 3: 23:(ST9420C), 07:(CS92), and 22:(DTS92(E))
735
+
736
+ # HCW80 has option of being wired (normally wireless)
737
+ # ST9420C has battery back-up (as does evohome)
738
+
739
+
740
+ # Below, verbs & codes - can use Verb/Code/Index for mypy type checking
741
+ @verify(EnumCheck.UNIQUE)
742
+ class VerbT(StrEnum):
743
+ I_ = " I"
744
+ RQ = "RQ"
745
+ RP = "RP"
746
+ W_ = " W"
747
+
748
+
749
+ I_: Final = VerbT.I_
750
+ RQ: Final = VerbT.RQ
751
+ RP: Final = VerbT.RP
752
+ W_: Final = VerbT.W_
753
+
754
+
755
+ @verify(EnumCheck.UNIQUE)
756
+ class MsgId(StrEnum):
757
+ _00 = "00"
758
+ _03 = "03"
759
+ _06 = "06"
760
+ _01 = "01"
761
+ _05 = "05"
762
+ _0E = "0E"
763
+ _0F = "0F"
764
+ _11 = "11"
765
+ _12 = "12"
766
+ _13 = "13"
767
+ _19 = "19"
768
+ _1A = "1A"
769
+ _1B = "1B"
770
+ _1C = "1C"
771
+ _30 = "30"
772
+ _31 = "31"
773
+ _38 = "38"
774
+ _39 = "39"
775
+ _71 = "71" # unclear if is supported bt OTB
776
+ _72 = "72" # unclear if is supported bt OTB
777
+ _73 = "73"
778
+ _74 = "74" # unclear if is supported bt OTB
779
+ _75 = "75" # unclear if is supported bt OTB
780
+ _76 = "76" # unclear if is supported bt OTB
781
+ _77 = "77" # unclear if is supported bt OTB
782
+ _78 = "78" # unclear if is supported bt OTB
783
+ _79 = "79" # unclear if is supported bt OTB
784
+ _7A = "7A" # unclear if is supported bt OTB
785
+ _7B = "7B" # unclear if is supported bt OTB
786
+ _7F = "7F"
787
+
788
+
789
+ # StrEnum is intended include all known codes, see: test suite, code schema in ramses.py
790
+ @verify(EnumCheck.UNIQUE)
791
+ class Code(StrEnum):
792
+ _0001 = "0001"
793
+ _0002 = "0002"
794
+ _0004 = "0004"
795
+ _0005 = "0005"
796
+ _0006 = "0006"
797
+ _0008 = "0008"
798
+ _0009 = "0009"
799
+ _000A = "000A"
800
+ _000C = "000C"
801
+ _000E = "000E"
802
+ _0016 = "0016"
803
+ _0100 = "0100"
804
+ _0150 = "0150"
805
+ _01D0 = "01D0"
806
+ _01E9 = "01E9"
807
+ _01FF = "01FF"
808
+ _0404 = "0404"
809
+ _0418 = "0418"
810
+ _042F = "042F"
811
+ _0B04 = "0B04"
812
+ _1030 = "1030"
813
+ _1060 = "1060"
814
+ _1081 = "1081"
815
+ _1090 = "1090"
816
+ _1098 = "1098"
817
+ _10A0 = "10A0"
818
+ _10B0 = "10B0"
819
+ _10D0 = "10D0"
820
+ _10E0 = "10E0"
821
+ _10E1 = "10E1"
822
+ _10E2 = "10E2"
823
+ _1100 = "1100"
824
+ _11F0 = "11F0"
825
+ _1260 = "1260"
826
+ _1280 = "1280"
827
+ _1290 = "1290"
828
+ _1298 = "1298"
829
+ _12A0 = "12A0"
830
+ _12B0 = "12B0"
831
+ _12C0 = "12C0"
832
+ _12C8 = "12C8"
833
+ _12F0 = "12F0"
834
+ _1300 = "1300"
835
+ _1470 = "1470"
836
+ _1F09 = "1F09"
837
+ _1F41 = "1F41"
838
+ _1F70 = "1F70"
839
+ _1FC9 = "1FC9"
840
+ _1FCA = "1FCA"
841
+ _1FD0 = "1FD0"
842
+ _1FD4 = "1FD4"
843
+ _2210 = "2210"
844
+ _2249 = "2249"
845
+ _22C9 = "22C9"
846
+ _22D0 = "22D0"
847
+ _22D9 = "22D9"
848
+ _22E0 = "22E0"
849
+ _22E5 = "22E5"
850
+ _22E9 = "22E9"
851
+ _22F1 = "22F1"
852
+ _22F2 = "22F2"
853
+ _22F3 = "22F3"
854
+ _22F4 = "22F4"
855
+ _22F7 = "22F7"
856
+ _22F8 = "22F8"
857
+ _22B0 = "22B0"
858
+ _2309 = "2309"
859
+ _2349 = "2349"
860
+ _2389 = "2389"
861
+ _2400 = "2400"
862
+ _2401 = "2401"
863
+ _2410 = "2410"
864
+ _2411 = "2411"
865
+ _2420 = "2420"
866
+ _2D49 = "2D49"
867
+ _2E04 = "2E04"
868
+ _2E10 = "2E10"
869
+ _30C9 = "30C9"
870
+ _3110 = "3110"
871
+ _3120 = "3120"
872
+ _313E = "313E"
873
+ _313F = "313F"
874
+ _3150 = "3150"
875
+ _31D9 = "31D9"
876
+ _31DA = "31DA"
877
+ _31E0 = "31E0"
878
+ _3200 = "3200"
879
+ _3210 = "3210"
880
+ _3220 = "3220"
881
+ _3221 = "3221"
882
+ _3222 = "3222"
883
+ _3223 = "3223"
884
+ _3B00 = "3B00"
885
+ _3EF0 = "3EF0"
886
+ _3EF1 = "3EF1"
887
+ _4401 = "4401"
888
+ _4E01 = "4E01"
889
+ _4E02 = "4E02"
890
+ _4E04 = "4E04"
891
+ _4E0D = "4E0D"
892
+ _4E15 = "4E15"
893
+ _4E16 = "4E16"
894
+ _PUZZ = "7FFF" # for internal use: not to be a RAMSES II code
895
+
896
+
897
+ # fmt: off
898
+ IndexT = Literal[
899
+ "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0A", "0B", "0C", "0D", "0E", "0F",
900
+ "21", # used by Nuaire
901
+ "F0", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "FA", "FB", "FC", "FD", "FE", "FF"
902
+ ]
903
+ # fmt: on