ramses-rf 0.22.40__py3-none-any.whl → 0.51.2__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.
- ramses_cli/__init__.py +18 -0
- ramses_cli/client.py +597 -0
- ramses_cli/debug.py +20 -0
- ramses_cli/discovery.py +405 -0
- ramses_cli/utils/cat_slow.py +17 -0
- ramses_cli/utils/convert.py +60 -0
- ramses_rf/__init__.py +31 -10
- ramses_rf/binding_fsm.py +787 -0
- ramses_rf/const.py +124 -105
- ramses_rf/database.py +297 -0
- ramses_rf/device/__init__.py +69 -39
- ramses_rf/device/base.py +187 -376
- ramses_rf/device/heat.py +540 -552
- ramses_rf/device/hvac.py +279 -171
- ramses_rf/dispatcher.py +153 -177
- ramses_rf/entity_base.py +478 -361
- ramses_rf/exceptions.py +82 -0
- ramses_rf/gateway.py +377 -513
- ramses_rf/helpers.py +57 -19
- ramses_rf/py.typed +0 -0
- ramses_rf/schemas.py +148 -194
- ramses_rf/system/__init__.py +16 -23
- ramses_rf/system/faultlog.py +363 -0
- ramses_rf/system/heat.py +295 -302
- ramses_rf/system/schedule.py +312 -198
- ramses_rf/system/zones.py +318 -238
- ramses_rf/version.py +2 -8
- ramses_rf-0.51.2.dist-info/METADATA +72 -0
- ramses_rf-0.51.2.dist-info/RECORD +55 -0
- {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.2.dist-info}/WHEEL +1 -2
- ramses_rf-0.51.2.dist-info/entry_points.txt +2 -0
- {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.2.dist-info/licenses}/LICENSE +1 -1
- ramses_tx/__init__.py +160 -0
- {ramses_rf/protocol → ramses_tx}/address.py +65 -59
- ramses_tx/command.py +1454 -0
- ramses_tx/const.py +903 -0
- ramses_tx/exceptions.py +92 -0
- {ramses_rf/protocol → ramses_tx}/fingerprints.py +56 -15
- {ramses_rf/protocol → ramses_tx}/frame.py +132 -131
- ramses_tx/gateway.py +338 -0
- ramses_tx/helpers.py +883 -0
- {ramses_rf/protocol → ramses_tx}/logger.py +67 -53
- {ramses_rf/protocol → ramses_tx}/message.py +155 -191
- ramses_tx/opentherm.py +1260 -0
- ramses_tx/packet.py +210 -0
- {ramses_rf/protocol → ramses_tx}/parsers.py +1266 -1003
- ramses_tx/protocol.py +801 -0
- ramses_tx/protocol_fsm.py +672 -0
- ramses_tx/py.typed +0 -0
- {ramses_rf/protocol → ramses_tx}/ramses.py +262 -185
- {ramses_rf/protocol → ramses_tx}/schemas.py +150 -133
- ramses_tx/transport.py +1471 -0
- ramses_tx/typed_dicts.py +492 -0
- ramses_tx/typing.py +181 -0
- ramses_tx/version.py +4 -0
- ramses_rf/discovery.py +0 -398
- ramses_rf/protocol/__init__.py +0 -59
- ramses_rf/protocol/backports.py +0 -42
- ramses_rf/protocol/command.py +0 -1576
- ramses_rf/protocol/const.py +0 -697
- ramses_rf/protocol/exceptions.py +0 -111
- ramses_rf/protocol/helpers.py +0 -390
- ramses_rf/protocol/opentherm.py +0 -1170
- ramses_rf/protocol/packet.py +0 -235
- ramses_rf/protocol/protocol.py +0 -613
- ramses_rf/protocol/transport.py +0 -1011
- ramses_rf/protocol/version.py +0 -10
- ramses_rf/system/hvac.py +0 -82
- ramses_rf-0.22.40.dist-info/METADATA +0 -64
- ramses_rf-0.22.40.dist-info/RECORD +0 -42
- 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
|