ramses-rf 0.22.2__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 (72) 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 +378 -514
  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.2.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.2.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_tx/parsers.py +2957 -0
  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 -1561
  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/parsers.py +0 -2673
  66. ramses_rf/protocol/protocol.py +0 -613
  67. ramses_rf/protocol/transport.py +0 -1011
  68. ramses_rf/protocol/version.py +0 -10
  69. ramses_rf/system/hvac.py +0 -82
  70. ramses_rf-0.22.2.dist-info/METADATA +0 -64
  71. ramses_rf-0.22.2.dist-info/RECORD +0 -42
  72. ramses_rf-0.22.2.dist-info/top_level.txt +0 -1
@@ -1,697 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- #
4
- """RAMSES RF - a RAMSES-II protocol decoder & analyser."""
5
- from __future__ import annotations
6
-
7
- import re
8
- from types import SimpleNamespace
9
-
10
- from .backports import StrEnum
11
-
12
- __dev_mode__ = False
13
- DEV_MODE = __dev_mode__
14
-
15
- # used by tansport QoS...
16
- SZ_BACKOFF = "backoff"
17
- SZ_DISABLE_BACKOFF = "disable_backoff"
18
- SZ_PRIORITY = "priority"
19
- SZ_QOS = "qos"
20
- SZ_RETRIES = "retries"
21
- SZ_TIMEOUT = "timeout"
22
-
23
- SZ_CALLBACK = "callback"
24
- SZ_DAEMON = "daemon"
25
- SZ_EXPIRED = "expired"
26
- SZ_EXPIRES = "expires"
27
- SZ_FUNC = "func"
28
- SZ_ARGS = "args"
29
-
30
- # used by schedule.py...
31
- SZ_FRAGMENT = "fragment"
32
- SZ_FRAG_NUMBER = "frag_number"
33
- SZ_FRAG_LENGTH = "frag_length"
34
- SZ_TOTAL_FRAGS = "total_frags"
35
-
36
- SZ_SCHEDULE = "schedule"
37
- SZ_CHANGE_COUNTER = "change_counter"
38
-
39
- Priority = SimpleNamespace(LOWEST=4, LOW=2, DEFAULT=0, HIGH=-2, HIGHEST=-4)
40
-
41
-
42
- # used by 31DA
43
- SZ_AIR_QUALITY = "air_quality"
44
- SZ_AIR_QUALITY_BASE = "air_quality_base"
45
- SZ_BOOST_TIMER = "boost_timer"
46
- SZ_BYPASS_POSITION = "bypass_position"
47
- SZ_CO2_LEVEL = "co2_level"
48
- SZ_EXHAUST_FAN_SPEED = "exhaust_fan_speed"
49
- SZ_EXHAUST_FLOW = "exhaust_flow"
50
- SZ_EXHAUST_TEMPERATURE = "exhaust_temperature"
51
- SZ_FAN_INFO = "fan_info"
52
- SZ_FAN_MODE = "fan_mode"
53
- SZ_FAN_RATE = "fan_rate"
54
- SZ_INDOOR_HUMIDITY = "indoor_humidity"
55
- SZ_INDOOR_TEMPERATURE = "indoor_temperature"
56
- SZ_OUTDOOR_HUMIDITY = "outdoor_humidity"
57
- SZ_OUTDOOR_TEMPERATURE = "outdoor_temperature"
58
- SZ_POST_HEAT = "post_heat"
59
- SZ_PRE_HEAT = "pre_heat"
60
- SZ_REMAINING_TIME = "remaining_time"
61
- SZ_SUPPLY_FAN_SPEED = "supply_fan_speed"
62
- SZ_SUPPLY_FLOW = "supply_flow"
63
- SZ_SUPPLY_TEMPERATURE = "supply_temperature"
64
- SZ_SPEED_CAP = "speed_cap"
65
-
66
- SZ_PRESENCE_DETECTED = "presence_detected"
67
-
68
-
69
- def slug(string: str) -> str:
70
- """Convert a string to snake_case."""
71
- return re.sub(r"[\W_]+", "_", string.lower())
72
-
73
-
74
- def _alt_slugify_string(key: str) -> str:
75
- """Convert a string to snake_case."""
76
- string = re.sub(r"[\-\.\s]", "_", str(key))
77
- return (string[0]).lower() + re.sub(
78
- r"[A-Z]", lambda matched: f"_{matched.group(0).lower()}", string[1:] # type: ignore[str-bytes-safe]
79
- )
80
-
81
-
82
- class AttrDict(dict):
83
- _SZ_AKA_SLUG = "_root_slug"
84
- _SZ_DEFAULT = "_default"
85
- _SZ_SLUGS = "SLUGS"
86
-
87
- @classmethod
88
- def __readonly(cls, *args, **kwargs):
89
- raise TypeError(f"'{cls.__class__.__name__}' object is read only")
90
-
91
- __delitem__ = __readonly
92
- __setitem__ = __readonly
93
- clear = __readonly
94
- pop = __readonly # type:ignore[assignment]
95
- popitem = __readonly # type:ignore[assignment]
96
- setdefault = __readonly # type:ignore[assignment]
97
- update = __readonly # type:ignore[assignment]
98
-
99
- del __readonly
100
-
101
- def __init__(self, main_table, attr_table=None):
102
- self._main_table: dict = main_table
103
- self._attr_table: dict = attr_table
104
- self._attr_table[self._SZ_SLUGS] = tuple(sorted(main_table.keys()))
105
-
106
- self._slug_lookup: dict = {
107
- None: slug
108
- for slug, table in main_table.items()
109
- for k in table.values()
110
- if isinstance(k, str) and table.get(self._SZ_DEFAULT)
111
- } # i.e. {None: 'HEA'}
112
- self._slug_lookup.update(
113
- {
114
- k: table.get(self._SZ_AKA_SLUG, slug)
115
- for slug, table in main_table.items()
116
- for k in table.keys()
117
- if isinstance(k, str) and len(k) == 2
118
- } # e.g. {'00': 'TRV', '01': 'CTL', '04': 'TRV', ...}
119
- )
120
- self._slug_lookup.update(
121
- {
122
- k: slug
123
- for slug, table in main_table.items()
124
- for k in table.values()
125
- if isinstance(k, str) and table.get(self._SZ_AKA_SLUG) is None
126
- } # e.g. {'heat_device':'HEA', 'dhw_sensor':'DHW', ...}
127
- )
128
-
129
- self._forward = {
130
- k: v
131
- for table in main_table.values()
132
- for k, v in table.items()
133
- if isinstance(k, str) and k[:1] != "_"
134
- } # e.g. {'00': 'radiator_valve', '01': 'controller', ...}
135
- self._reverse = {
136
- v: k
137
- for table in main_table.values()
138
- for k, v in table.items()
139
- if isinstance(k, str) and k[:1] != "_" and self._SZ_AKA_SLUG not in table
140
- } # e.g. {'radiator_valve': '00', 'controller': '01', ...}
141
- self._forward = dict(sorted(self._forward.items(), key=lambda item: item[0]))
142
-
143
- super().__init__(self._forward)
144
-
145
- def __getitem__(self, key):
146
- if key in self._main_table: # map[ZON_ROLE.DHW] -> "dhw_sensor"
147
- return list(self._main_table[key].values())[0]
148
- # if key in self._forward: # map["0D"] -> "dhw_sensor"
149
- # return self._forward.__getitem__(key)
150
- if key in self._reverse: # map["dhw_sensor"] -> "0D"
151
- return self._reverse.__getitem__(key)
152
- return super().__getitem__(key)
153
-
154
- def __getattr__(self, name):
155
- if name in self._main_table: # map.DHW -> "0D" (using slug)
156
- if (result := list(self._main_table[name].keys())[0]) is not None:
157
- return result
158
- elif name in self._attr_table: # bespoke attrs
159
- return self._attr_table[name]
160
- elif len(name) and name[1:] in self._forward: # map._0D -> "dhw_sensor"
161
- return self._forward[name[1:]]
162
- elif name.isupper() and name.lower() in self._reverse: # map.DHW_SENSOR -> "0D"
163
- return self[name.lower()]
164
- return self.__getattribute__(name)
165
-
166
- def _hex(self, key) -> str:
167
- """Return the key/ID (2-byte hex string) of the two-way dict (e.g. '04')."""
168
- if key in self._main_table:
169
- return list(self._main_table[key].keys())[0]
170
- if key in self._reverse:
171
- return self._reverse[key]
172
- raise KeyError(key)
173
-
174
- def _str(self, key) -> str:
175
- """Return the value (string) of the two-way dict (e.g. 'radiator_valve')."""
176
- if key in self._main_table:
177
- return list(self._main_table[key].values())[0]
178
- if key in self:
179
- return self[key]
180
- raise KeyError(key)
181
-
182
- # def values(self):
183
- # return {k: k for k in super().values()}.values()
184
-
185
- def slug(self, key) -> str:
186
- """WIP: Return master slug for a hex key/ID (e.g. 00 -> 'TRV', not 'TR0')."""
187
- slug_ = self._slug_lookup[key]
188
- # if slug_ in self._attr_table["_TRANSFORMS"]:
189
- # return self._attr_table["_TRANSFORMS"][slug_]
190
- return slug_
191
-
192
- def slugs(self) -> tuple:
193
- """Return the slugs from the main table."""
194
- return self._attr_table[self._SZ_SLUGS]
195
-
196
-
197
- def attr_dict_factory(main_table, attr_table=None) -> AttrDict: # is: SlottedAttrDict
198
- if attr_table is None:
199
- attr_table = {}
200
-
201
- class SlottedAttrDict(AttrDict):
202
- pass # TODO: low priority
203
- # __slots__ = (
204
- # list(main_table.keys())
205
- # + [
206
- # f"_{k}"
207
- # for t in main_table.values()
208
- # for k in t.keys()
209
- # if isinstance(k, str) and len(k) == 2
210
- # ]
211
- # + [v for t in main_table.values() for v in t.values()]
212
- # + list(attr_table.keys())
213
- # + [AttrDict._SZ_AKA_SLUG, AttrDict._SZ_SLUGS]
214
- # )
215
-
216
- return SlottedAttrDict(main_table, attr_table=attr_table)
217
-
218
-
219
- # slugs for device/zone entity klasses, used by 0005/000C
220
- DEV_ROLE = SimpleNamespace(
221
- #
222
- # Generic device/zone classes
223
- ACT="ACT", # Generic heating zone actuator group
224
- SEN="SEN", # Generic heating zone sensor group
225
- #
226
- # Standard device/zone classes
227
- ELE="ELE", # BDRs (no heat demand)
228
- MIX="MIX", # HM8s
229
- RAD="RAD", # TRVs
230
- UFH="UFH", # UFC (circuits)
231
- VAL="VAL", # BDRs
232
- #
233
- # DHW device/zone classes
234
- DHW="DHW", # DHW sensor (a zone, but not a heating zone)
235
- HTG="HTG", # BDR (DHW relay, HTG relay)
236
- HT1="HT1", # BDR (HTG relay)
237
- #
238
- # Other device/zone classes
239
- OUT="OUT", # OUT (external weather sensor)
240
- RFG="RFG", # RFG
241
- APP="APP", # BDR/OTB (appliance relay)
242
- )
243
- DEV_ROLE_MAP = attr_dict_factory(
244
- {
245
- DEV_ROLE.ACT: {"00": "zone_actuator"},
246
- DEV_ROLE.SEN: {"04": "zone_sensor"},
247
- DEV_ROLE.RAD: {"08": "rad_actuator"},
248
- DEV_ROLE.UFH: {"09": "ufh_actuator"},
249
- DEV_ROLE.VAL: {"0A": "val_actuator"},
250
- DEV_ROLE.MIX: {"0B": "mix_actuator"},
251
- DEV_ROLE.OUT: {"0C": "out_sensor"},
252
- DEV_ROLE.DHW: {"0D": "dhw_sensor"},
253
- DEV_ROLE.HTG: {"0E": "hotwater_valve"}, # payload[:4] == 000E
254
- DEV_ROLE.HT1: {None: "heating_valve"}, # payload[:4] == 010E
255
- DEV_ROLE.APP: {"0F": "appliance_control"}, # the heat/cool source
256
- DEV_ROLE.RFG: {"10": "remote_gateway"},
257
- DEV_ROLE.ELE: {"11": "ele_actuator"}, # ELE(VAL) - no RP from older evos
258
- }, # 03, 05, 06, 07: & >11 - no response from an 01:
259
- {
260
- "HEAT_DEVICES": ("00", "04", "08", "09", "0A", "0B", "11"),
261
- "DHW_DEVICES": ("0D", "0E"),
262
- "SENSORS": ("04", "0C", "0D"),
263
- },
264
- )
265
-
266
-
267
- # slugs for device entity types, used in device_ids
268
- DEV_TYPE = SimpleNamespace(
269
- #
270
- # Promotable/Generic devices
271
- DEV="DEV", # xx: Promotable device
272
- HEA="HEA", # xx: Promotable Heat device, aka CH/DHW device
273
- HVC="HVC", # xx: Promotable HVAC device
274
- THM="THM", # xx: Generic thermostat
275
- #
276
- # Heat (CH/DHW) devices
277
- BDR="BDR", # 13: Electrical relay
278
- CTL="CTL", # 01: Controller (zoned)
279
- DHW="DHW", # 07: DHW sensor
280
- DTS="DTS", # 12: Thermostat, DTS92(E)
281
- DT2="DT2", # 22: Thermostat, DTS92(E)
282
- HCW="HCW", # 03: Thermostat - don't use STA
283
- HGI="HGI", # 18: Gateway interface (RF to USB), HGI80
284
- # 8="HM8", # xx: HM80 mixer valve (Rx-only, does not Tx)
285
- OTB="OTB", # 10: OpenTherm bridge
286
- OUT="OUT", # 17: External weather sensor
287
- PRG="PRG", # 23: Programmer
288
- RFG="RFG", # 30: RF gateway (RF to ethernet), RFG100
289
- RND="RND", # 34: Thermostat, TR87RF
290
- TRV="TRV", # 04: Thermostatic radiator valve
291
- TR0="TR0", # 00: Thermostatic radiator valve
292
- UFC="UFC", # 02: UFH controller
293
- #
294
- # Honeywell Jasper, other Heat devices
295
- JIM="JIM", # 08: Jasper Interface Module (EIM?)
296
- JST="JST", # 31: Jasper Stat
297
- #
298
- # HVAC devices, these are more like classes (i.e. no reliable device type)
299
- RFS="RFS", # ??: HVAC spIDer gateway
300
- FAN="FAN", # ??: HVAC fan, 31D[9A]: 20|29|30|37 (some, e.g. 29: only 31D9)
301
- CO2="CO2", # ??: HVAC CO2 sensor
302
- HUM="HUM", # ??: HVAC humidity sensor, 1260: 32
303
- PIR="PIR", # ??: HVAC pesence sensor, 2E10
304
- REM="REM", # ??: HVAC switch, 22F[13]: 02|06|20|32|39|42|49|59 (no 20: are both)
305
- SW2="SW2", # ??: HVAC switch, Orcon variant
306
- DIS="DIS", # ??: HVAC switch with display
307
- )
308
- DEV_TYPE_MAP = attr_dict_factory(
309
- {
310
- # Generic devices (would be promoted)
311
- DEV_TYPE.DEV: {None: "generic_device"}, # , AttrDict._SZ_DEFAULT: True},
312
- DEV_TYPE.HEA: {None: "heat_device"},
313
- DEV_TYPE.HVC: {None: "hvac_device"},
314
- # HGI80
315
- DEV_TYPE.HGI: {"18": "gateway_interface"}, # HGI80
316
- # Heat (CH/DHW) devices
317
- DEV_TYPE.TR0: {"00": "radiator_valve", AttrDict._SZ_AKA_SLUG: DEV_TYPE.TRV},
318
- DEV_TYPE.CTL: {"01": "controller"},
319
- DEV_TYPE.UFC: {"02": "ufh_controller"},
320
- DEV_TYPE.HCW: {"03": "analog_thermostat"},
321
- DEV_TYPE.THM: {None: "thermostat"},
322
- DEV_TYPE.TRV: {"04": "radiator_valve"},
323
- DEV_TYPE.DHW: {"07": "dhw_sensor"},
324
- DEV_TYPE.OTB: {"10": "opentherm_bridge"},
325
- DEV_TYPE.DTS: {"12": "digital_thermostat"},
326
- DEV_TYPE.BDR: {"13": "electrical_relay"},
327
- DEV_TYPE.OUT: {"17": "outdoor_sensor"},
328
- DEV_TYPE.DT2: {"22": "digital_thermostat", AttrDict._SZ_AKA_SLUG: DEV_TYPE.DTS},
329
- DEV_TYPE.PRG: {"23": "programmer"},
330
- DEV_TYPE.RFG: {"30": "rf_gateway"}, # RFG100
331
- DEV_TYPE.RND: {"34": "round_thermostat"},
332
- # Other (jasper) devices
333
- DEV_TYPE.JIM: {"08": "jasper_interface"},
334
- DEV_TYPE.JST: {"31": "jasper_thermostat"},
335
- # Ventilation devices
336
- DEV_TYPE.CO2: {None: "co2_sensor"},
337
- DEV_TYPE.DIS: {None: "switch_display"},
338
- DEV_TYPE.FAN: {None: "ventilator"}, # Both Fans and HRUs
339
- DEV_TYPE.HUM: {None: "rh_sensor"},
340
- DEV_TYPE.PIR: {None: "presence_sensor"},
341
- DEV_TYPE.RFS: {None: "hvac_gateway"}, # Spider
342
- DEV_TYPE.REM: {None: "switch"},
343
- DEV_TYPE.SW2: {None: "switch_variant"},
344
- },
345
- {
346
- "HEAT_DEVICES": (
347
- "00",
348
- "01",
349
- "02",
350
- "03",
351
- "04",
352
- "07",
353
- "10",
354
- "12",
355
- "13",
356
- "17",
357
- "22",
358
- "30",
359
- "34",
360
- ), # CH/DHW devices instead of HVAC/other
361
- "HEAT_ZONE_SENSORS": ("00", "01", "03", "04", "12", "22", "34"),
362
- "HEAT_ZONE_ACTUATORS": ("00", "02", "04", "13"),
363
- "THM_DEVICES": ("03", "12", "22", "34"),
364
- "TRV_DEVICES": ("00", "04"),
365
- "CONTROLLERS": ("01", "12", "22", "23", "34"), # potentially controllers
366
- "PROMOTABLE_SLUGS": (DEV_TYPE.DEV, DEV_TYPE.HEA, DEV_TYPE.HVC),
367
- "HVAC_SLUGS": {
368
- DEV_TYPE.CO2: "co2_sensor",
369
- DEV_TYPE.FAN: "ventilator", # Both Fans and HRUs
370
- DEV_TYPE.HUM: "rh_sensor",
371
- DEV_TYPE.RFS: "hvac_gateway", # Spider
372
- DEV_TYPE.REM: "switch",
373
- },
374
- },
375
- )
376
-
377
- # slugs for zone entity klasses, used by 0005/000C
378
- ZON_ROLE = SimpleNamespace(
379
- #
380
- # Generic device/zone classes
381
- ACT="ACT", # Generic heating zone actuator group
382
- SEN="SEN", # Generic heating zone sensor group
383
- #
384
- # Standard device/zone classes
385
- ELE="ELE", # heating zone with BDRs (no heat demand)
386
- MIX="MIX", # heating zone with HM8s
387
- RAD="RAD", # heating zone with TRVs
388
- UFH="UFH", # heating zone with UFC circuits
389
- VAL="VAL", # zheating one with BDRs
390
- # Standard device/zone classes *not a heating zone)
391
- DHW="DHW", # DHW zone with BDRs
392
- )
393
- ZON_ROLE_MAP = attr_dict_factory(
394
- {
395
- ZON_ROLE.ACT: {"00": "heating_zone"}, # any actuator
396
- ZON_ROLE.SEN: {"04": "heating_zone"}, # any sensor
397
- ZON_ROLE.RAD: {"08": "radiator_valve"}, # TRVs
398
- ZON_ROLE.UFH: {"09": "underfloor_heating"}, # UFCs
399
- ZON_ROLE.VAL: {"0A": "zone_valve"}, # BDRs
400
- ZON_ROLE.MIX: {"0B": "mixing_valve"}, # HM8s
401
- ZON_ROLE.DHW: {"0D": "stored_hotwater"}, # DHWs
402
- # N_CLASS.HTG: {"0E": "stored_hotwater", AttrDict._SZ_AKA_SLUG: ZON_ROLE.DHW},
403
- ZON_ROLE.ELE: {"11": "electric_heat"}, # BDRs
404
- },
405
- {
406
- "HEAT_ZONES": ("08", "09", "0A", "0B", "11"),
407
- },
408
- )
409
-
410
- # Zone modes
411
- ZON_MODE_MAP = attr_dict_factory(
412
- {
413
- "FOLLOW": {"00": "follow_schedule"},
414
- "ADVANCED": {"01": "advanced_override"}, # . until the next scheduled setpoint
415
- "PERMANENT": {"02": "permanent_override"}, # indefinitely, until auto_reset
416
- "COUNTDOWN": {"03": "countdown_override"}, # for x mins (duration, max 1,215?)
417
- "TEMPORARY": {"04": "temporary_override"}, # until a given date/time (until)
418
- }
419
- )
420
-
421
- # System modes
422
- SYS_MODE_MAP = attr_dict_factory(
423
- {
424
- "au_00": {"00": "auto"}, # . indef (only)
425
- "ho_01": {"01": "heat_off"}, # . indef (only)
426
- "eb_02": {"02": "eco_boost"}, # . indef/<=24h: is either Eco, *or* Boost
427
- "aw_03": {"03": "away"}, # . indef/<=99d (0d = end of today, 00:00)
428
- "do_04": {"04": "day_off"}, # . indef/<=99d: rounded down to 00:00 by CTL
429
- "de_05": {"05": "day_off_eco"}, # . indef/<=99d: set to Eco when DayOff ends
430
- "ar_06": {"06": "auto_with_reset"}, # indef (only)
431
- "cu_07": {"07": "custom"}, # . indef/<=99d
432
- }
433
- )
434
-
435
-
436
- SZ_ACTUATOR = "actuator"
437
- SZ_ACTUATORS = "actuators"
438
- SZ_BINDINGS = "bindings"
439
- SZ_DATETIME = "datetime"
440
- SZ_DEVICE_CLASS = "device_class" # used in 0418 only?
441
- SZ_DEVICE_ID = "device_id"
442
- SZ_DEVICE_ROLE = "device_role"
443
- SZ_DEVICES = "devices"
444
- SZ_DHW_IDX = "dhw_idx"
445
- SZ_DOMAIN_ID = "domain_id"
446
- SZ_DURATION = "duration"
447
- SZ_HEAT_DEMAND = "heat_demand"
448
- SZ_IS_DST = "is_dst"
449
- SZ_LANGUAGE = "language"
450
- SZ_LOG_IDX = "log_idx"
451
- SZ_MODE = "mode"
452
- SZ_NAME = "name"
453
- SZ_PAYLOAD = "payload"
454
- SZ_PRESSURE = "pressure"
455
- SZ_RELAY_DEMAND = "relay_demand"
456
- SZ_RELAY_FAILSAFE = "relay_failsafe"
457
- SZ_SENSOR = "sensor"
458
- SZ_SETPOINT = "setpoint"
459
- SZ_SLUG = "_SLUG"
460
- SZ_SYSTEM_MODE = "system_mode"
461
- SZ_TEMPERATURE = "temperature"
462
- SZ_UFH_IDX = "ufh_idx"
463
- SZ_UNKNOWN = "unknown"
464
- SZ_UNTIL = "until"
465
- SZ_VALUE = "value"
466
- SZ_WINDOW_OPEN = "window_open"
467
- SZ_ZONE_CLASS = "zone_class"
468
- SZ_ZONE_IDX = "zone_idx"
469
- SZ_ZONE_MASK = "zone_mask"
470
- SZ_ZONE_TYPE = "zone_type"
471
- SZ_ZONES = "zones"
472
-
473
-
474
- DEFAULT_MAX_ZONES = 16 if DEV_MODE else 12
475
- # Evohome: 12 (0-11), older/initial version was 8
476
- # Hometronics: 16 (0-15), or more?
477
- # Sundial RF2: 2 (0-1), usually only one, but ST9520C can do two zones
478
-
479
-
480
- DEVICE_ID_REGEX = SimpleNamespace(
481
- ANY=re.compile(r"^[0-9]{2}:[0-9]{6}$"),
482
- BDR=re.compile(r"^13:[0-9]{6}$"),
483
- CTL=re.compile(r"^(01|23):[0-9]{6}$"),
484
- DHW=re.compile(r"^07:[0-9]{6}$"),
485
- HGI=re.compile(r"^18:[0-9]{6}$"),
486
- APP=re.compile(r"^(10|13):[0-9]{6}$"),
487
- UFC=re.compile(r"^02:[0-9]{6}$"),
488
- SEN=re.compile(r"^(01|03|04|12|22|34):[0-9]{6}$"),
489
- )
490
-
491
- # Domains
492
- F6, F7, F8, F9, FA, FB, FC, FD, FE, FF = (f"{x:02X}" for x in range(0xF6, 0x100))
493
-
494
- DOMAIN_TYPE_MAP = {
495
- F6: "cooling_valve", # cooling
496
- F7: "domain_f7",
497
- F8: "domain_f8",
498
- F9: DEV_ROLE_MAP[DEV_ROLE.HT1], # Heating Valve
499
- FA: DEV_ROLE_MAP[DEV_ROLE.HTG], # HW Valve (or UFH loop if src.type == UFC?)
500
- FB: "domain_fb", # also: cooling valve?
501
- FC: DEV_ROLE_MAP[DEV_ROLE.APP], # appliance_control
502
- FD: "domain_fd", # seen with hometronics
503
- # "FE": ???
504
- # FF: "system", # TODO: remove this, is not a domain
505
- } # "21": "Ventilation", "88": ???
506
- DOMAIN_TYPE_LOOKUP = {v: k for k, v in DOMAIN_TYPE_MAP.items() if k != FF}
507
-
508
- DHW_STATE_MAP = {"00": "off", "01": "on"}
509
- DHW_STATE_LOOKUP = {v: k for k, v in DHW_STATE_MAP.items()}
510
-
511
- DTM_LONG_REGEX = re.compile(
512
- r"\d{4}-[01]\d-[0-3]\d(T| )[0-2]\d:[0-5]\d:[0-5]\d\.\d{6} ?"
513
- ) # 2020-11-30T13:15:00.123456
514
- DTM_TIME_REGEX = re.compile(r"[0-2]\d:[0-5]\d:[0-5]\d\.\d{3} ?") # 13:15:00.123
515
-
516
- # Used by packet structure validators
517
- r = r"(-{3}|\d{3}|\.{3})" # RSSI, '...' was used by an older version of evofw3
518
- v = r"( I|RP|RQ| W)" # verb
519
- d = r"(-{2}:-{6}|\d{2}:\d{6})" # device ID
520
- c = r"[0-9A-F]{4}" # code
521
- l = r"\d{3}" # length # noqa: E741
522
- p = r"([0-9A-F]{2}){1,48}" # payload
523
-
524
- # DEVICE_ID_REGEX = re.compile(f"^{d}$")
525
- COMMAND_REGEX = re.compile(f"^{v} {r} {d} {d} {d} {c} {l} {p}$")
526
- MESSAGE_REGEX = re.compile(f"^{r} {v} {r} {d} {d} {d} {c} {l} {p}$")
527
-
528
-
529
- # Used by 0418/system_fault parser
530
- FAULT_DEVICE_CLASS = {
531
- "00": "controller",
532
- "01": "sensor",
533
- "02": "setpoint",
534
- "04": "actuator", # if domain is FC, then "boiler_relay"
535
- "05": "dhw_sensor",
536
- "06": "rf_gateway",
537
- }
538
- FAULT_STATE = {
539
- "00": "fault",
540
- "40": "restore",
541
- "C0": "unknown_c0", # C0s do not appear in the evohome UI
542
- }
543
- FAULT_TYPE = {
544
- "01": "system_fault",
545
- "03": "mains_low",
546
- "04": "battery_low",
547
- "05": "battery_error", # actually: 'evotouch_battery_error'
548
- "06": "comms_fault",
549
- "07": "sensor_fault", # seen with zone sensor
550
- "0A": "sensor_error",
551
- "??": "bad_value",
552
- }
553
-
554
- SystemType = SimpleNamespace(
555
- CHRONOTHERM="chronotherm",
556
- EVOHOME="evohome",
557
- HOMETRONICS="hometronics",
558
- PROGRAMMER="programmer",
559
- SUNDIAL="sundial",
560
- GENERIC="generic",
561
- )
562
-
563
-
564
- # used by 22Fx parser, and FanSwitch devices
565
- # SZ_BOOST_TIMER = "boost_timer" # minutes, e.g. 10, 20, 30 minutes
566
- HEATER_MODE = "heater_mode" # e.g. auto, off
567
- FAN_MODE = "fan_mode" # e.g. low. high
568
- FAN_RATE = "fan_rate" # percentage, 0.0 - 1.0
569
-
570
-
571
- # RP --- 01:054173 18:006402 --:------ 0005 004 00100000 # before adding RFG100
572
- # .I --- 01:054173 --:------ 01:054173 1FC9 012 0010E004D39D001FC904D39D
573
- # .W --- 30:248208 01:054173 --:------ 1FC9 012 0010E07BC9900012907BC990
574
- # .I --- 01:054173 30:248208 --:------ 1FC9 006 00FFFF04D39D
575
-
576
- # RP --- 01:054173 18:006402 --:------ 0005 004 00100100 # after adding RFG100
577
- # RP --- 01:054173 18:006402 --:------ 000C 006 0010007BC990 # 30:082155
578
- # RP --- 01:054173 18:006402 --:------ 0005 004 00100100 # before deleting RFG from CTL
579
- # .I --- 01:054173 --:------ 01:054173 0005 004 00100000 # when the RFG was deleted
580
- # RP --- 01:054173 18:006402 --:------ 0005 004 00100000 # after deleting the RFG
581
-
582
- # RP|zone_devices | 000E0... || {'domain_id': 'FA', 'device_role': 'dhw_valve', 'devices': ['13:081807']} # noqa: E501
583
- # RP|zone_devices | 010E0... || {'domain_id': 'FA', 'device_role': 'htg_valve', 'devices': ['13:106039']} # noqa: E501
584
-
585
- # Example of:
586
- # - Sundial RF2 Pack 3: 23:(ST9420C), 07:(CS92), and 22:(DTS92(E))
587
-
588
- # HCW80 has option of being wired (normally wireless)
589
- # ST9420C has battery back-up (as does evohome)
590
-
591
-
592
- I_ = " I"
593
- RQ = "RQ"
594
- RP = "RP"
595
- W_ = " W"
596
-
597
-
598
- class Code(StrEnum):
599
- _0001 = "0001"
600
- _0002 = "0002"
601
- _0004 = "0004"
602
- _0005 = "0005"
603
- _0006 = "0006"
604
- _0008 = "0008"
605
- _0009 = "0009"
606
- _000A = "000A"
607
- _000C = "000C"
608
- _000E = "000E"
609
- _0016 = "0016"
610
- _0100 = "0100"
611
- _0150 = "0150"
612
- _01D0 = "01D0"
613
- _01E9 = "01E9"
614
- _0404 = "0404"
615
- _0418 = "0418"
616
- _042F = "042F"
617
- _0B04 = "0B04"
618
- _1030 = "1030"
619
- _1060 = "1060"
620
- _1081 = "1081"
621
- _1090 = "1090"
622
- _1098 = "1098"
623
- _10A0 = "10A0"
624
- _10B0 = "10B0"
625
- _10D0 = "10D0" # Orcon
626
- _10E0 = "10E0"
627
- _10E1 = "10E1"
628
- _10E2 = "10E2"
629
- _1100 = "1100"
630
- _11F0 = "11F0"
631
- _1260 = "1260"
632
- _1280 = "1280"
633
- _1290 = "1290"
634
- _1298 = "1298"
635
- _12A0 = "12A0"
636
- _12B0 = "12B0"
637
- _12C0 = "12C0"
638
- _12C8 = "12C8"
639
- _12F0 = "12F0"
640
- _1300 = "1300"
641
- _1470 = "1470" # Orcon
642
- _1F09 = "1F09"
643
- _1F41 = "1F41"
644
- _1F70 = "1F70"
645
- _1FC9 = "1FC9"
646
- _1FCA = "1FCA"
647
- _1FD0 = "1FD0"
648
- _1FD4 = "1FD4"
649
- _2210 = "2210" # Orcon
650
- _2249 = "2249"
651
- _22C9 = "22C9"
652
- _22D0 = "22D0"
653
- _22D9 = "22D9"
654
- _22E0 = "22E0" # Orcon
655
- _22E5 = "22E5" # Orcon
656
- _22E9 = "22E9" # Orcon
657
- _22F1 = "22F1"
658
- _22F2 = "22F2" # Orcon
659
- _22F3 = "22F3"
660
- _22F4 = "22F4" # Orcon
661
- _22F7 = "22F7" # Orcon
662
- _22F8 = "22F8" # Orcon
663
- _22B0 = "22B0"
664
- _2309 = "2309"
665
- _2349 = "2349"
666
- _2389 = "2389"
667
- _2400 = "2400"
668
- _2401 = "2401"
669
- _2410 = "2410"
670
- _2411 = "2411"
671
- _2420 = "2420"
672
- _2D49 = "2D49"
673
- _2E04 = "2E04"
674
- _2E10 = "2E10"
675
- _30C9 = "30C9"
676
- _3110 = "3110"
677
- _3120 = "3120"
678
- _313E = "313E" # Orcon
679
- _313F = "313F"
680
- _3150 = "3150"
681
- _31D9 = "31D9"
682
- _31DA = "31DA"
683
- _31E0 = "31E0"
684
- _3200 = "3200"
685
- _3210 = "3210"
686
- _3220 = "3220"
687
- _3221 = "3221"
688
- _3222 = "3222" # Orcon
689
- _3223 = "3223"
690
- _3B00 = "3B00"
691
- _3EF0 = "3EF0"
692
- _3EF1 = "3EF1"
693
- _4401 = "4401"
694
- _4E01 = "4E01"
695
- _4E02 = "4E02"
696
- _4E04 = "4E04"
697
- _PUZZ = "7FFF"