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_rf/protocol/const.py
DELETED
|
@@ -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"
|