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.
- 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 +286 -171
- ramses_rf/dispatcher.py +153 -177
- ramses_rf/entity_base.py +478 -361
- ramses_rf/exceptions.py +82 -0
- ramses_rf/gateway.py +378 -514
- 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.1.dist-info/METADATA +72 -0
- ramses_rf-0.51.1.dist-info/RECORD +55 -0
- {ramses_rf-0.22.2.dist-info → ramses_rf-0.51.1.dist-info}/WHEEL +1 -2
- ramses_rf-0.51.1.dist-info/entry_points.txt +2 -0
- {ramses_rf-0.22.2.dist-info → ramses_rf-0.51.1.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_tx/parsers.py +2957 -0
- 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 -1561
- 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/parsers.py +0 -2673
- 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.2.dist-info/METADATA +0 -64
- ramses_rf-0.22.2.dist-info/RECORD +0 -42
- ramses_rf-0.22.2.dist-info/top_level.txt +0 -1
ramses_rf/protocol/parsers.py
DELETED
|
@@ -1,2673 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
#
|
|
4
|
-
"""RAMSES RF - payload processors."""
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
import re
|
|
9
|
-
from datetime import datetime as dt
|
|
10
|
-
from datetime import timedelta as td
|
|
11
|
-
from typing import Callable
|
|
12
|
-
|
|
13
|
-
from .address import NON_DEV_ADDR, hex_id_to_dev_id
|
|
14
|
-
from .const import (
|
|
15
|
-
DEV_ROLE,
|
|
16
|
-
DEV_ROLE_MAP,
|
|
17
|
-
DEV_TYPE_MAP,
|
|
18
|
-
FAULT_DEVICE_CLASS,
|
|
19
|
-
FAULT_STATE,
|
|
20
|
-
FAULT_TYPE,
|
|
21
|
-
SYS_MODE_MAP,
|
|
22
|
-
SZ_ACTUATOR,
|
|
23
|
-
SZ_AIR_QUALITY,
|
|
24
|
-
SZ_AIR_QUALITY_BASE,
|
|
25
|
-
SZ_BINDINGS,
|
|
26
|
-
SZ_BYPASS_POSITION,
|
|
27
|
-
SZ_CHANGE_COUNTER,
|
|
28
|
-
SZ_CO2_LEVEL,
|
|
29
|
-
SZ_DATETIME,
|
|
30
|
-
SZ_DEVICE_CLASS,
|
|
31
|
-
SZ_DEVICE_ID,
|
|
32
|
-
SZ_DEVICE_ROLE,
|
|
33
|
-
SZ_DEVICES,
|
|
34
|
-
SZ_DOMAIN_ID,
|
|
35
|
-
SZ_DURATION,
|
|
36
|
-
SZ_EXHAUST_FAN_SPEED,
|
|
37
|
-
SZ_EXHAUST_FLOW,
|
|
38
|
-
SZ_EXHAUST_TEMPERATURE,
|
|
39
|
-
SZ_FAN_INFO,
|
|
40
|
-
SZ_FAN_MODE,
|
|
41
|
-
SZ_FRAG_LENGTH,
|
|
42
|
-
SZ_FRAG_NUMBER,
|
|
43
|
-
SZ_FRAGMENT,
|
|
44
|
-
SZ_INDOOR_HUMIDITY,
|
|
45
|
-
SZ_INDOOR_TEMPERATURE,
|
|
46
|
-
SZ_IS_DST,
|
|
47
|
-
SZ_LANGUAGE,
|
|
48
|
-
SZ_MODE,
|
|
49
|
-
SZ_NAME,
|
|
50
|
-
SZ_OUTDOOR_HUMIDITY,
|
|
51
|
-
SZ_OUTDOOR_TEMPERATURE,
|
|
52
|
-
SZ_PAYLOAD,
|
|
53
|
-
SZ_POST_HEAT,
|
|
54
|
-
SZ_PRE_HEAT,
|
|
55
|
-
SZ_PRESSURE,
|
|
56
|
-
SZ_RELAY_DEMAND,
|
|
57
|
-
SZ_REMAINING_TIME,
|
|
58
|
-
SZ_SETPOINT,
|
|
59
|
-
SZ_SPEED_CAP,
|
|
60
|
-
SZ_SUPPLY_FAN_SPEED,
|
|
61
|
-
SZ_SUPPLY_FLOW,
|
|
62
|
-
SZ_SUPPLY_TEMPERATURE,
|
|
63
|
-
SZ_SYSTEM_MODE,
|
|
64
|
-
SZ_TEMPERATURE,
|
|
65
|
-
SZ_TOTAL_FRAGS,
|
|
66
|
-
SZ_UFH_IDX,
|
|
67
|
-
SZ_UNKNOWN,
|
|
68
|
-
SZ_UNTIL,
|
|
69
|
-
SZ_VALUE,
|
|
70
|
-
SZ_WINDOW_OPEN,
|
|
71
|
-
SZ_ZONE_CLASS,
|
|
72
|
-
SZ_ZONE_IDX,
|
|
73
|
-
SZ_ZONE_MASK,
|
|
74
|
-
SZ_ZONE_TYPE,
|
|
75
|
-
ZON_MODE_MAP,
|
|
76
|
-
ZON_ROLE_MAP,
|
|
77
|
-
__dev_mode__,
|
|
78
|
-
)
|
|
79
|
-
from .exceptions import InvalidPayloadError
|
|
80
|
-
from .fingerprints import check_signature
|
|
81
|
-
from .helpers import (
|
|
82
|
-
bool_from_hex,
|
|
83
|
-
date_from_hex,
|
|
84
|
-
double_from_hex,
|
|
85
|
-
dtm_from_hex,
|
|
86
|
-
dts_from_hex,
|
|
87
|
-
flag8_from_hex,
|
|
88
|
-
percent_from_hex,
|
|
89
|
-
str_from_hex,
|
|
90
|
-
temp_from_hex,
|
|
91
|
-
valve_demand,
|
|
92
|
-
)
|
|
93
|
-
from .opentherm import EN, MSG_DESC, MSG_ID, MSG_NAME, MSG_TYPE, OtMsgType, decode_frame
|
|
94
|
-
from .ramses import _31DA_FAN_INFO, _2411_PARAMS_SCHEMA
|
|
95
|
-
from .version import VERSION
|
|
96
|
-
|
|
97
|
-
# Kudos & many thanks to:
|
|
98
|
-
# - Evsdd: 0404 (wow!)
|
|
99
|
-
# - Ierlandfan: 3150, 31D9, 31DA, others
|
|
100
|
-
# - ReneKlootwijk: 3EF0
|
|
101
|
-
# - brucemiranda: 3EF0, others
|
|
102
|
-
# - janvken: 10D0, 1470, 1F70, 22B0, 2411, several others
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# skipcq: PY-W2000
|
|
106
|
-
from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
107
|
-
I_,
|
|
108
|
-
RP,
|
|
109
|
-
RQ,
|
|
110
|
-
W_,
|
|
111
|
-
F6,
|
|
112
|
-
F8,
|
|
113
|
-
F9,
|
|
114
|
-
FA,
|
|
115
|
-
FB,
|
|
116
|
-
FC,
|
|
117
|
-
FF,
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
_2411_TABLE = {k: v["description"] for k, v in _2411_PARAMS_SCHEMA.items()}
|
|
121
|
-
|
|
122
|
-
_INFORM_DEV_MSG = "Support the development of ramses_rf by reporting this packet"
|
|
123
|
-
|
|
124
|
-
LOOKUP_PUZZ = {
|
|
125
|
-
"10": "engine", # . # version str, e.g. v0.14.0
|
|
126
|
-
"11": "impersonating", # pkt header, e.g. 30C9| I|03:123001 (15 characters, packed)
|
|
127
|
-
"12": "message", # . # message only, max len is 16 ascii characters
|
|
128
|
-
"13": "message", # . # message only, but without a timestamp, max len 22 chars
|
|
129
|
-
"7F": "null", # . # packet is null / was nullified: payload to be ignored
|
|
130
|
-
} # "00" is reserved
|
|
131
|
-
|
|
132
|
-
DEV_MODE = __dev_mode__ and False
|
|
133
|
-
|
|
134
|
-
_LOGGER = _PKT_LOGGER = logging.getLogger(__name__)
|
|
135
|
-
if DEV_MODE:
|
|
136
|
-
_LOGGER.setLevel(logging.DEBUG)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def parser_decorator(fnc) -> Callable:
|
|
140
|
-
def wrapper(payload, msg, **kwargs):
|
|
141
|
-
result = fnc(payload, msg, **kwargs)
|
|
142
|
-
if isinstance(result, dict) and msg.seqn.isnumeric(): # 22F1/3
|
|
143
|
-
result["seqx_num"] = msg.seqn
|
|
144
|
-
return result
|
|
145
|
-
|
|
146
|
-
return wrapper
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
@parser_decorator # rf_unknown
|
|
150
|
-
def parser_0001(payload, msg) -> dict:
|
|
151
|
-
# When in test mode, a 12: will send a W every 6 seconds, *on?* the second:
|
|
152
|
-
# 12:39:56.099 061 W --- 12:010740 --:------ 12:010740 0001 005 0000000501
|
|
153
|
-
# 12:40:02.098 061 W --- 12:010740 --:------ 12:010740 0001 005 0000000501
|
|
154
|
-
# 12:40:08.099 058 W --- 12:010740 --:------ 12:010740 0001 005 0000000501
|
|
155
|
-
|
|
156
|
-
# sent by a THM every 5s when is signal strength test mode (0505, except 1st pkt)
|
|
157
|
-
# 13:48:38.518 080 W --- 12:010740 --:------ 12:010740 0001 005 0000000501
|
|
158
|
-
# 13:48:45.518 074 W --- 12:010740 --:------ 12:010740 0001 005 0000000505
|
|
159
|
-
# 13:48:50.518 077 W --- 12:010740 --:------ 12:010740 0001 005 0000000505
|
|
160
|
-
|
|
161
|
-
# sent by a CTL before a rf_check
|
|
162
|
-
# 15:12:47.769 053 W --- 01:145038 --:------ 01:145038 0001 005 FC00000505
|
|
163
|
-
# 15:12:47.869 053 RQ --- 01:145038 13:237335 --:------ 0016 002 00FF
|
|
164
|
-
# 15:12:47.880 053 RP --- 13:237335 01:145038 --:------ 0016 002 0017
|
|
165
|
-
|
|
166
|
-
# 12:30:18.083 047 W --- 01:145038 --:------ 01:145038 0001 005 0800000505
|
|
167
|
-
# 12:30:23.084 049 W --- 01:145038 --:------ 01:145038 0001 005 0800000505
|
|
168
|
-
|
|
169
|
-
# 15:03:33.187 054 W --- 01:145038 --:------ 01:145038 0001 005 FC00000505
|
|
170
|
-
# 15:03:38.188 063 W --- 01:145038 --:------ 01:145038 0001 005 FC00000505
|
|
171
|
-
# 15:03:43.188 064 W --- 01:145038 --:------ 01:145038 0001 005 FC00000505
|
|
172
|
-
# 15:13:19.757 053 W --- 01:145038 --:------ 01:145038 0001 005 FF00000505
|
|
173
|
-
# 15:13:24.758 054 W --- 01:145038 --:------ 01:145038 0001 005 FF00000505
|
|
174
|
-
# 15:13:29.758 068 W --- 01:145038 --:------ 01:145038 0001 005 FF00000505
|
|
175
|
-
# 15:13:34.759 063 W --- 01:145038 --:------ 01:145038 0001 005 FF00000505
|
|
176
|
-
|
|
177
|
-
# loopback (not Tx'd) by a HGI80 whenever its button is pressed
|
|
178
|
-
# 00:22:41.540 --- I --- --:------ --:------ --:------ 0001 005 00FFFF02FF
|
|
179
|
-
# 00:22:41.757 --- I --- --:------ --:------ --:------ 0001 005 00FFFF0200
|
|
180
|
-
# 00:22:43.320 --- I --- --:------ --:------ --:------ 0001 005 00FFFF02FF
|
|
181
|
-
# 00:22:43.415 --- I --- --:------ --:------ --:------ 0001 005 00FFFF0200
|
|
182
|
-
|
|
183
|
-
# From a CM927:
|
|
184
|
-
# W/--:/--:/12:/00-0000-0501 = Test transmit
|
|
185
|
-
# W/--:/--:/12:/00-0000-0505 = Field strength
|
|
186
|
-
|
|
187
|
-
if payload[2:6] in ("2000", "8000", "A000"):
|
|
188
|
-
mode = "hvac"
|
|
189
|
-
elif payload[2:6] in ("0000", "FFFF"):
|
|
190
|
-
mode = "heat"
|
|
191
|
-
else:
|
|
192
|
-
mode = "heat"
|
|
193
|
-
|
|
194
|
-
if mode == "hvac":
|
|
195
|
-
assert payload[:2] == "00", payload[:2]
|
|
196
|
-
# assert payload[2:4] in ("20", "80", "A0"), payload[2:4]
|
|
197
|
-
# assert payload[4:6] == "00", payload[4:6]
|
|
198
|
-
assert payload[8:10] in ("00", "04", "10", "20", "FF"), payload[8:10]
|
|
199
|
-
|
|
200
|
-
result = {"payload": payload, "slot_num": payload[6:8]}
|
|
201
|
-
if msg.len >= 6:
|
|
202
|
-
result.update({"param_num": payload[10:12]})
|
|
203
|
-
if msg.len >= 7:
|
|
204
|
-
result.update({"next_slot_num": payload[12:14]})
|
|
205
|
-
if msg.len >= 8:
|
|
206
|
-
result.update(
|
|
207
|
-
{
|
|
208
|
-
"boolean_14": None
|
|
209
|
-
if payload[14:16] == "FF"
|
|
210
|
-
else bool(int(payload[14:16]))
|
|
211
|
-
}
|
|
212
|
-
)
|
|
213
|
-
return result
|
|
214
|
-
|
|
215
|
-
assert payload[:2] in ("00",), payload[:2]
|
|
216
|
-
assert payload[2:6] in ("0000", "FFFF"), payload[2:6]
|
|
217
|
-
assert payload[8:10] in ("00", "02", "05"), payload[8:10]
|
|
218
|
-
|
|
219
|
-
return {
|
|
220
|
-
SZ_PAYLOAD: "-".join((payload[:2], payload[2:6], payload[6:8], payload[8:])),
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
@parser_decorator # outdoor_sensor (outdoor_weather / outdoor_temperature)
|
|
225
|
-
def parser_0002(payload, msg) -> dict:
|
|
226
|
-
# seen with: 03:125829, 03:196221, 03:196196, 03:052382, 03:201498, 03:201565:
|
|
227
|
-
# .I 000 03:201565 --:------ 03:201565 0002 004 03020105 # no zone_idx, domain_id
|
|
228
|
-
|
|
229
|
-
# is it CODE_IDX_COMPLEX:
|
|
230
|
-
# - 02...... for outside temp?
|
|
231
|
-
# - 03...... for other stuff?
|
|
232
|
-
|
|
233
|
-
if msg.src.type == DEV_TYPE_MAP.HCW: # payload[2:] == DEV_TYPE_MAP.HCW, DEX
|
|
234
|
-
assert payload == "03020105"
|
|
235
|
-
return {f"_{SZ_UNKNOWN}": payload}
|
|
236
|
-
|
|
237
|
-
# if payload[6:] == "02": # msg.src.type == DEV_TYPE_MAP.OUT:
|
|
238
|
-
return {
|
|
239
|
-
SZ_TEMPERATURE: temp_from_hex(payload[2:6]),
|
|
240
|
-
f"_{SZ_UNKNOWN}": payload[6:],
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
@parser_decorator # zone_name
|
|
245
|
-
def parser_0004(payload, msg) -> dict:
|
|
246
|
-
# RQ payload is zz00; limited to 12 chars in evohome UI? if "7F"*20: not a zone
|
|
247
|
-
|
|
248
|
-
return {} if payload[4:] == "7F" * 20 else {SZ_NAME: str_from_hex(payload[4:])}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
@parser_decorator # system_zones (add/del a zone?)
|
|
252
|
-
def parser_0005(payload, msg) -> dict | list[dict]: # TODO: needs a cleanup
|
|
253
|
-
# .I --- 01:145038 --:------ 01:145038 0005 004 00000100
|
|
254
|
-
# RP --- 02:017205 18:073736 --:------ 0005 004 0009001F
|
|
255
|
-
# .I --- 34:064023 --:------ 34:064023 0005 012 000A0000-000F0000-00100000
|
|
256
|
-
|
|
257
|
-
def _parser(seqx) -> dict:
|
|
258
|
-
if msg.src.type == DEV_TYPE_MAP.UFC: # DEX, or use: seqx[2:4] == ...
|
|
259
|
-
zone_mask = flag8_from_hex(seqx[6:8], lsb=True)
|
|
260
|
-
elif msg.len == 3: # ATC928G1000 - 1st gen monochrome model, max 8 zones
|
|
261
|
-
zone_mask = flag8_from_hex(seqx[4:6], lsb=True)
|
|
262
|
-
else:
|
|
263
|
-
zone_mask = flag8_from_hex(seqx[4:6], lsb=True) + flag8_from_hex(
|
|
264
|
-
seqx[6:8], lsb=True
|
|
265
|
-
)
|
|
266
|
-
zone_class = ZON_ROLE_MAP.get(seqx[2:4], DEV_ROLE_MAP[seqx[2:4]])
|
|
267
|
-
return {
|
|
268
|
-
SZ_ZONE_TYPE: seqx[2:4], # TODO: ?remove & keep zone_class?
|
|
269
|
-
SZ_ZONE_MASK: zone_mask,
|
|
270
|
-
SZ_ZONE_CLASS: zone_class, # TODO: ?remove & keep zone_type?
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if msg.verb == RQ: # RQs have a context: zone_type
|
|
274
|
-
return {SZ_ZONE_TYPE: payload[2:4], SZ_ZONE_CLASS: DEV_ROLE_MAP[payload[2:4]]}
|
|
275
|
-
|
|
276
|
-
if msg._has_array:
|
|
277
|
-
assert (
|
|
278
|
-
msg.verb == I_ and msg.src.type == DEV_TYPE_MAP.RND
|
|
279
|
-
), f"{msg!r} # expecting I/{DEV_TYPE_MAP.RND}:" # DEX
|
|
280
|
-
return [_parser(payload[i : i + 8]) for i in range(0, len(payload), 8)]
|
|
281
|
-
|
|
282
|
-
return _parser(payload)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
@parser_decorator # schedule_sync (any changes?)
|
|
286
|
-
def parser_0006(payload, msg) -> dict:
|
|
287
|
-
"""Return the total number of changes to the schedules, including the DHW schedule.
|
|
288
|
-
|
|
289
|
-
An RQ is sent every ~60s by a RFG100, an increase will prompt it to send a run of
|
|
290
|
-
RQ/0404s (it seems to assume only the zones may have changed?).
|
|
291
|
-
"""
|
|
292
|
-
# 16:10:34.288 053 RQ --- 30:071715 01:145038 --:------ 0006 001 00
|
|
293
|
-
# 16:10:34.291 053 RP --- 01:145038 30:071715 --:------ 0006 004 00050008
|
|
294
|
-
|
|
295
|
-
if payload[2:] == "FFFFFF": # RP to an invalid RQ
|
|
296
|
-
return {}
|
|
297
|
-
|
|
298
|
-
assert payload[2:4] == "05"
|
|
299
|
-
|
|
300
|
-
return {
|
|
301
|
-
SZ_CHANGE_COUNTER: int(payload[4:], 16),
|
|
302
|
-
"_header": payload[:4],
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
@parser_decorator # relay_demand (domain/zone/device)
|
|
307
|
-
def parser_0008(payload, msg) -> dict:
|
|
308
|
-
# https://www.domoticaforum.eu/viewtopic.php?f=7&t=5806&start=105#p73681
|
|
309
|
-
# e.g. Electric Heat Zone
|
|
310
|
-
|
|
311
|
-
# .I --- 01:145038 --:------ 01:145038 0008 002 0314
|
|
312
|
-
# .I --- 01:145038 --:------ 01:145038 0008 002 F914
|
|
313
|
-
# .I --- 01:054173 --:------ 01:054173 0008 002 FA00
|
|
314
|
-
# .I --- 01:145038 --:------ 01:145038 0008 002 FC14
|
|
315
|
-
|
|
316
|
-
# RP --- 13:109598 18:199952 --:------ 0008 002 0000
|
|
317
|
-
# RP --- 13:109598 18:199952 --:------ 0008 002 00C8
|
|
318
|
-
|
|
319
|
-
if msg.src.type == DEV_TYPE_MAP.JST and msg.len == 13: # Honeywell Japser, DEX
|
|
320
|
-
assert msg.len == 13, "expecting length 13"
|
|
321
|
-
return {
|
|
322
|
-
"ordinal": f"0x{payload[2:8]}",
|
|
323
|
-
"blob": payload[8:],
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return {SZ_RELAY_DEMAND: percent_from_hex(payload[2:4])} # 3EF0[2:4], 3EF1[10:12]
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
@parser_decorator # relay_failsafe
|
|
330
|
-
def parser_0009(payload, msg) -> dict | list:
|
|
331
|
-
"""The relay failsafe mode.
|
|
332
|
-
|
|
333
|
-
The failsafe mode defines the relay behaviour if the RF communication is lost (e.g.
|
|
334
|
-
when a room thermostat stops communicating due to discharged batteries):
|
|
335
|
-
False (disabled) - if RF comms are lost, relay will be held in OFF position
|
|
336
|
-
True (enabled) - if RF comms are lost, relay will cycle at 20% ON, 80% OFF
|
|
337
|
-
|
|
338
|
-
This setting may need to be enabled to ensure prost protect mode.
|
|
339
|
-
"""
|
|
340
|
-
# can get: 003 or 006, e.g.: FC01FF-F901FF or FC00FF-F900FF
|
|
341
|
-
# .I --- 23:100224 --:------ 23:100224 0009 003 0100FF # 2-zone ST9520C
|
|
342
|
-
# .I --- 10:040239 01:223036 --:------ 0009 003 000000
|
|
343
|
-
|
|
344
|
-
def _parser(seqx) -> dict:
|
|
345
|
-
assert seqx[:2] in (F9, FC) or int(seqx[:2], 16) < 16
|
|
346
|
-
return {
|
|
347
|
-
SZ_DOMAIN_ID if seqx[:1] == "F" else SZ_ZONE_IDX: seqx[:2],
|
|
348
|
-
"failsafe_enabled": {"00": False, "01": True}.get(seqx[2:4]),
|
|
349
|
-
f"{SZ_UNKNOWN}_0": seqx[4:],
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if msg._has_array:
|
|
353
|
-
return [_parser(payload[i : i + 6]) for i in range(0, len(payload), 6)]
|
|
354
|
-
|
|
355
|
-
return {
|
|
356
|
-
"failsafe_enabled": {"00": False, "01": True}.get(payload[2:4]),
|
|
357
|
-
f"{SZ_UNKNOWN}_0": payload[4:],
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
@parser_decorator # zone_params (zone_config)
|
|
362
|
-
def parser_000a(payload, msg) -> dict | list:
|
|
363
|
-
# RQ --- 34:044203 01:158182 --:------ 000A 001 08
|
|
364
|
-
# RP --- 01:158182 34:044203 --:------ 000A 006 081001F409C4
|
|
365
|
-
# RQ --- 22:017139 01:140959 --:------ 000A 006 080001F40DAC
|
|
366
|
-
# RP --- 01:140959 22:017139 --:------ 000A 006 081001F40DAC
|
|
367
|
-
|
|
368
|
-
def _parser(seqx) -> dict: # null_rp: "007FFF7FFF"
|
|
369
|
-
bitmap = int(seqx[2:4], 16)
|
|
370
|
-
return {
|
|
371
|
-
"min_temp": temp_from_hex(seqx[4:8]),
|
|
372
|
-
"max_temp": temp_from_hex(seqx[8:]),
|
|
373
|
-
"local_override": not bool(bitmap & 1),
|
|
374
|
-
"openwindow_function": not bool(bitmap & 2),
|
|
375
|
-
"multiroom_mode": not bool(bitmap & 16),
|
|
376
|
-
f"_{SZ_UNKNOWN}_bitmap": f"0b{bitmap:08b}", # TODO: try W with this
|
|
377
|
-
} # cannot determine zone_type from this information
|
|
378
|
-
|
|
379
|
-
if msg._has_array: # NOTE: these arrays can span 2 pkts!
|
|
380
|
-
return [
|
|
381
|
-
{
|
|
382
|
-
SZ_ZONE_IDX: payload[i : i + 2],
|
|
383
|
-
**_parser(payload[i : i + 12]),
|
|
384
|
-
}
|
|
385
|
-
for i in range(0, len(payload), 12)
|
|
386
|
-
]
|
|
387
|
-
|
|
388
|
-
if msg.verb == RQ and msg.len <= 2: # some RQs have a payload (why?)
|
|
389
|
-
return {}
|
|
390
|
-
|
|
391
|
-
assert msg.len == 6, f"{msg!r} # expecting length 006"
|
|
392
|
-
return _parser(payload)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
@parser_decorator # zone_devices
|
|
396
|
-
def parser_000c(payload, msg) -> dict:
|
|
397
|
-
# .I --- 34:092243 --:------ 34:092243 000C 018 00-0A-7F-FFFFFF 00-0F-7F-FFFFFF 00-10-7F-FFFFFF # noqa: E501
|
|
398
|
-
# RP --- 01:145038 18:013393 --:------ 000C 006 00-00-00-10DAFD
|
|
399
|
-
# RP --- 01:145038 18:013393 --:------ 000C 012 01-00-00-10DAF5 01-00-00-10DAFB
|
|
400
|
-
|
|
401
|
-
def complex_idx(seqx, msg) -> dict: # complex index
|
|
402
|
-
"""domain_id, zone_idx, or ufx_idx|zone_idx."""
|
|
403
|
-
|
|
404
|
-
# TODO: 000C to a UFC should be ufh_ifx, not zone_idx
|
|
405
|
-
if msg.src.type == DEV_TYPE_MAP.UFC: # DEX
|
|
406
|
-
assert int(seqx, 16) < 8, f"invalid ufh_idx: '{seqx}' (0x00)"
|
|
407
|
-
return {
|
|
408
|
-
SZ_UFH_IDX: seqx,
|
|
409
|
-
SZ_ZONE_IDX: None if payload[4:6] == "7F" else payload[4:6],
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if payload[2:4] in (DEV_ROLE_MAP.DHW, DEV_ROLE_MAP.HTG):
|
|
413
|
-
assert (
|
|
414
|
-
int(seqx, 16) < 1 if payload[2:4] == DEV_ROLE_MAP.DHW else 2
|
|
415
|
-
), f"invalid _idx: '{seqx}' (0x01)"
|
|
416
|
-
return {SZ_DOMAIN_ID: FA if payload[:2] == "00" else F9}
|
|
417
|
-
|
|
418
|
-
if payload[2:4] == DEV_ROLE_MAP.APP:
|
|
419
|
-
assert int(seqx, 16) < 1, f"invalid _idx: '{seqx}' (0x02)"
|
|
420
|
-
return {SZ_DOMAIN_ID: FC}
|
|
421
|
-
|
|
422
|
-
assert int(seqx, 16) < 16, f"invalid zone_idx: '{seqx}' (0x03)"
|
|
423
|
-
return {SZ_ZONE_IDX: seqx}
|
|
424
|
-
|
|
425
|
-
def _parser(seqx) -> dict: # TODO: assumption that all id/idx are same is wrong!
|
|
426
|
-
assert (
|
|
427
|
-
seqx[:2] == payload[:2]
|
|
428
|
-
), f"idx != {payload[:2]} (seqx = {seqx}), short={is_short_000C(payload)}"
|
|
429
|
-
assert int(seqx[:2], 16) < 16
|
|
430
|
-
assert seqx[4:6] == "7F" or seqx[6:] != "F" * 6, f"Bad device_id: {seqx[6:]}"
|
|
431
|
-
return {hex_id_to_dev_id(seqx[6:12]): seqx[4:6]}
|
|
432
|
-
|
|
433
|
-
def is_short_000C(payload) -> bool:
|
|
434
|
-
"""Return True if it is a short 000C (element length is 5, not 6)."""
|
|
435
|
-
|
|
436
|
-
if (pkt_len := len(payload)) != 72:
|
|
437
|
-
return pkt_len % 12 != 0
|
|
438
|
-
|
|
439
|
-
# 0608-001099C3 0608-001099C5 0608-001099BF 0608-001099BE 0608-001099BD 0608-001099BC # len(element) = 6
|
|
440
|
-
# 0508-00109901 0800-10990208 0010-99030800 1099-04080010 9905-08001099 0608-00109907 # len(element) = 5
|
|
441
|
-
elif all(payload[i : i + 4] == payload[:4] for i in range(12, pkt_len, 12)):
|
|
442
|
-
return False # len(element) = 6 (12)
|
|
443
|
-
|
|
444
|
-
# 06 08-001099C3 06-08001099 C5-06080010 99-BF060800 10-99BE0608 00-1099BD06 08-001099BC # len(element) = 6
|
|
445
|
-
# 05 08-00109901 08-00109902 08-00109903 08-00109904 08-00109905 08-00109906 08-00109907 # len(element) = 5
|
|
446
|
-
elif all(payload[i : i + 2] == payload[2:4] for i in range(12, pkt_len, 10)):
|
|
447
|
-
return True # len(element) = 5 (10)
|
|
448
|
-
|
|
449
|
-
raise InvalidPayloadError("Unable to determine element length") # return None
|
|
450
|
-
|
|
451
|
-
if payload[2:4] == DEV_ROLE_MAP.HTG and payload[:2] == "01":
|
|
452
|
-
dev_role = DEV_ROLE_MAP[DEV_ROLE.HT1]
|
|
453
|
-
else:
|
|
454
|
-
dev_role = DEV_ROLE_MAP[payload[2:4]]
|
|
455
|
-
|
|
456
|
-
result = {
|
|
457
|
-
SZ_ZONE_TYPE: payload[2:4],
|
|
458
|
-
**complex_idx(payload[:2], msg),
|
|
459
|
-
SZ_DEVICE_ROLE: dev_role,
|
|
460
|
-
}
|
|
461
|
-
if msg.verb == RQ: # RQs have a context: index, zone_type, payload is iitt
|
|
462
|
-
return result
|
|
463
|
-
|
|
464
|
-
# NOTE: Both these are valid! So collision when len = 036!
|
|
465
|
-
# RP --- 01:239474 18:198929 --:------ 000C 012 06-00-00119A99 06-00-00119B21
|
|
466
|
-
# RP --- 01:069616 18:205592 --:------ 000C 011 01-00-00121B54 00-00121B52
|
|
467
|
-
# RP --- 01:239700 18:009874 --:------ 000C 018 07-08-001099C3 07-08-001099C5 07-08-001099BF
|
|
468
|
-
# RP --- 01:059885 18:010642 --:------ 000C 016 00-00-0011EDAA 00-0011ED92 00-0011EDA0
|
|
469
|
-
|
|
470
|
-
devs = (
|
|
471
|
-
[_parser(payload[:2] + payload[i : i + 10]) for i in range(2, len(payload), 10)]
|
|
472
|
-
if is_short_000C(payload)
|
|
473
|
-
else [_parser(payload[i : i + 12]) for i in range(0, len(payload), 12)]
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
return {
|
|
477
|
-
**result,
|
|
478
|
-
SZ_DEVICES: [k for d in devs for k, v in d.items() if v != "7F"],
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
@parser_decorator # unknown_000e, from STA
|
|
483
|
-
def parser_000e(payload, msg) -> dict:
|
|
484
|
-
|
|
485
|
-
assert payload in ("000000", "000014"), _INFORM_DEV_MSG
|
|
486
|
-
|
|
487
|
-
return {
|
|
488
|
-
SZ_PAYLOAD: payload,
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
@parser_decorator # rf_check
|
|
493
|
-
def parser_0016(payload, msg) -> dict:
|
|
494
|
-
# TODO: does 0016 include parent_idx?, but RQ/07:/0000?
|
|
495
|
-
# RQ --- 22:060293 01:078710 --:------ 0016 002 0200
|
|
496
|
-
# RP --- 01:078710 22:060293 --:------ 0016 002 021E
|
|
497
|
-
# RQ --- 12:010740 01:145038 --:------ 0016 002 0800
|
|
498
|
-
# RP --- 01:145038 12:010740 --:------ 0016 002 081E
|
|
499
|
-
# RQ --- 07:031785 01:063844 --:------ 0016 002 0000
|
|
500
|
-
# RP --- 01:063844 07:031785 --:------ 0016 002 002A
|
|
501
|
-
|
|
502
|
-
if msg.verb == RQ: # and msg.len == 1: # TODO: some RQs have a payload
|
|
503
|
-
return {}
|
|
504
|
-
|
|
505
|
-
rf_value = int(payload[2:4], 16)
|
|
506
|
-
return {
|
|
507
|
-
"rf_strength": min(int(rf_value / 5) + 1, 5),
|
|
508
|
-
"rf_value": rf_value,
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
@parser_decorator # language (of device/system)
|
|
513
|
-
def parser_0100(payload, msg) -> dict:
|
|
514
|
-
|
|
515
|
-
if msg.verb == RQ and msg.len == 1: # some RQs have a payload
|
|
516
|
-
return {}
|
|
517
|
-
|
|
518
|
-
return {
|
|
519
|
-
SZ_LANGUAGE: str_from_hex(payload[2:6]),
|
|
520
|
-
f"_{SZ_UNKNOWN}_0": payload[6:],
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
@parser_decorator # unknown_0150, from OTB
|
|
525
|
-
def parser_0150(payload, msg) -> dict:
|
|
526
|
-
|
|
527
|
-
assert payload == "000000", _INFORM_DEV_MSG
|
|
528
|
-
|
|
529
|
-
return {
|
|
530
|
-
SZ_PAYLOAD: payload,
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
@parser_decorator # unknown_01d0, from a HR91 (when its buttons are pushed)
|
|
535
|
-
def parser_01d0(payload, msg) -> dict:
|
|
536
|
-
# 23:57:28.869 045 W --- 04:000722 01:158182 --:------ 01D0 002 0003
|
|
537
|
-
# 23:57:28.931 045 I --- 01:158182 04:000722 --:------ 01D0 002 0003
|
|
538
|
-
# 23:57:31.581 048 W --- 04:000722 01:158182 --:------ 01E9 002 0003
|
|
539
|
-
# 23:57:31.643 045 I --- 01:158182 04:000722 --:------ 01E9 002 0000
|
|
540
|
-
# 23:57:31.749 050 W --- 04:000722 01:158182 --:------ 01D0 002 0000
|
|
541
|
-
# 23:57:31.811 045 I --- 01:158182 04:000722 --:------ 01D0 002 0000
|
|
542
|
-
|
|
543
|
-
assert payload[2:] in ("00", "03"), _INFORM_DEV_MSG
|
|
544
|
-
return {
|
|
545
|
-
f"{SZ_UNKNOWN}_0": payload[2:],
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
@parser_decorator # unknown_01e9, from a HR91 (when its buttons are pushed)
|
|
550
|
-
def parser_01e9(payload, msg) -> dict:
|
|
551
|
-
# 23:57:31.581348 048 W --- 04:000722 01:158182 --:------ 01E9 002 0003
|
|
552
|
-
# 23:57:31.643188 045 I --- 01:158182 04:000722 --:------ 01E9 002 0000
|
|
553
|
-
|
|
554
|
-
assert payload[2:] in ("00", "03"), _INFORM_DEV_MSG
|
|
555
|
-
return {
|
|
556
|
-
f"{SZ_UNKNOWN}_0": payload[2:],
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
@parser_decorator # zone_schedule (fragment)
|
|
561
|
-
def parser_0404(payload, msg) -> dict:
|
|
562
|
-
# Retreival of Zone schedule (NB: 200008)
|
|
563
|
-
# RQ --- 30:185469 01:037519 --:------ 0404 007 00-200008-00-0100
|
|
564
|
-
# RP --- 01:037519 30:185469 --:------ 0404 048 00-200008-29-0103-6E2...
|
|
565
|
-
# RQ --- 30:185469 01:037519 --:------ 0404 007 00-200008-00-0203
|
|
566
|
-
# RP --- 01:037519 30:185469 --:------ 0404 048 00-200008-29-0203-4FD...
|
|
567
|
-
# RQ --- 30:185469 01:037519 --:------ 0404 007 00-200008-00-0303
|
|
568
|
-
# RP --- 01:037519 30:185469 --:------ 0404 038 00-200008-1F-0303-C10...
|
|
569
|
-
|
|
570
|
-
# Retreival of DHW schedule (NB: 230008)
|
|
571
|
-
# RQ --- 30:185469 01:037519 --:------ 0404 007 00-230008-00-0100
|
|
572
|
-
# RP --- 01:037519 30:185469 --:------ 0404 048 00-230008-29-0103-618...
|
|
573
|
-
# RQ --- 30:185469 01:037519 --:------ 0404 007 00-230008-00-0203
|
|
574
|
-
# RP --- 01:037519 30:185469 --:------ 0404 048 00-230008-29-0203-ED6...
|
|
575
|
-
# RQ --- 30:185469 01:037519 --:------ 0404 007 00-230008-00-0303
|
|
576
|
-
# RP --- 01:037519 30:185469 --:------ 0404 014 00-230008-07-0303-13F...
|
|
577
|
-
|
|
578
|
-
# Write a Zone schedule...
|
|
579
|
-
# .W --- 30:042165 01:076010 --:------ 0404 048 08-200808-29-0104-688...
|
|
580
|
-
# .I --- 01:076010 30:042165 --:------ 0404 007 08-200808-29-0104
|
|
581
|
-
# .W --- 30:042165 01:076010 --:------ 0404 048 08-200808-29-0204-007...
|
|
582
|
-
# .I --- 01:076010 30:042165 --:------ 0404 007 08-200808-29-0204
|
|
583
|
-
# .W --- 30:042165 01:076010 --:------ 0404 048 08-200808-29-0304-8DD...
|
|
584
|
-
# .I --- 01:076010 30:042165 --:------ 0404 007 08-200808-29-0304
|
|
585
|
-
# .W --- 30:042165 01:076010 --:------ 0404 048 08-200808-11-0404-970...
|
|
586
|
-
# .I --- 01:076010 30:042165 --:------ 0404 007 08-200808-11-0400
|
|
587
|
-
|
|
588
|
-
# RP --- 01:145038 18:013393 --:------ 0404 007 00-230008-00-01FF # no schedule
|
|
589
|
-
|
|
590
|
-
assert payload[4:6] in ("00", payload[:2]), _INFORM_DEV_MSG
|
|
591
|
-
|
|
592
|
-
if int(payload[8:10], 16) * 2 != (frag_length := len(payload[14:])) and (
|
|
593
|
-
msg.verb != I_ or frag_length != 0
|
|
594
|
-
):
|
|
595
|
-
raise InvalidPayloadError(f"Incorrect fragment length: 0x{payload[8:10]}")
|
|
596
|
-
|
|
597
|
-
if msg.verb == RQ: # have a ctx: idx|frag_idx
|
|
598
|
-
return {
|
|
599
|
-
SZ_FRAG_NUMBER: int(payload[10:12], 16),
|
|
600
|
-
SZ_TOTAL_FRAGS: None if payload[12:14] == "00" else int(payload[12:14], 16),
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if msg.verb == I_: # have a ctx: idx|frag_idx
|
|
604
|
-
return {
|
|
605
|
-
SZ_FRAG_NUMBER: int(payload[10:12], 16),
|
|
606
|
-
SZ_TOTAL_FRAGS: int(payload[12:14], 16),
|
|
607
|
-
SZ_FRAG_LENGTH: None if payload[8:10] == "00" else int(payload[8:10], 16),
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
if payload[12:14] == FF:
|
|
611
|
-
return {
|
|
612
|
-
SZ_FRAG_NUMBER: int(payload[10:12], 16),
|
|
613
|
-
SZ_TOTAL_FRAGS: None,
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return {
|
|
617
|
-
SZ_FRAG_NUMBER: int(payload[10:12], 16),
|
|
618
|
-
SZ_TOTAL_FRAGS: int(payload[12:14], 16),
|
|
619
|
-
SZ_FRAG_LENGTH: None if payload[8:10] == "FF" else int(payload[8:10], 16),
|
|
620
|
-
SZ_FRAGMENT: payload[14:],
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
@parser_decorator # system_fault
|
|
625
|
-
def parser_0418(payload, msg) -> dict:
|
|
626
|
-
# RP --- 01:145038 18:013393 --:------ 0418 022 000000B006F604000000711607697FFFFF7000348A86 # COMMS FAULT, CHANGEOVER
|
|
627
|
-
# RP --- 01:145038 18:013393 --:------ 0418 022 000000B0000000000000000000007FFFFF7000000000 # noqa: E501
|
|
628
|
-
# RP --- 01:145038 18:013393 --:------ 0418 022 000036B0010000000000108000007FFFFF7000000000 # noqa: E501
|
|
629
|
-
# RP --- 01:145038 18:013393 --:------ 0418 022 000000B00401010000008694A3CC7FFFFF70000ECC8A # noqa: E501
|
|
630
|
-
# .I --- 01:037519 --:------ 01:037519 0418 022 000000B0050000000000239581877FFFFF7000000001 # Evotouch Battery Error # noqa: E501
|
|
631
|
-
# RP --- 01:037519 18:140805 --:------ 0418 022 004024B0060006000000CB94A112FFFFFF70007AD47D # noqa: E501
|
|
632
|
-
# 0 0 1 1 3 3
|
|
633
|
-
# 2 8 2 8 0 8
|
|
634
|
-
|
|
635
|
-
# assert int(payload[4:6], 16) < 64, f"Unexpected log_idx: 0x{payload[4:6]}"
|
|
636
|
-
|
|
637
|
-
if dts_from_hex(payload[18:30]) is None: # a null log entry
|
|
638
|
-
return {"log_entry": None}
|
|
639
|
-
|
|
640
|
-
try:
|
|
641
|
-
assert payload[2:4] in FAULT_STATE, f"fault state: {payload[2:4]}"
|
|
642
|
-
assert payload[8:10] in FAULT_TYPE, f"fault type: {payload[8:10]}"
|
|
643
|
-
assert payload[12:14] in FAULT_DEVICE_CLASS, f"device class: {payload[12:14]}"
|
|
644
|
-
# 1C: 'Comms fault, Actuator': seen with boiler relays
|
|
645
|
-
assert int(payload[10:12], 16) < 16 or (
|
|
646
|
-
payload[10:12] in ("1C", F6, F9, FA, FC)
|
|
647
|
-
), f"domain id: {payload[10:12]}"
|
|
648
|
-
except AssertionError as exc:
|
|
649
|
-
_LOGGER.warning(
|
|
650
|
-
f"{msg!r} < {_INFORM_DEV_MSG} ({exc}), with a photo of your fault log"
|
|
651
|
-
)
|
|
652
|
-
|
|
653
|
-
result = {
|
|
654
|
-
"timestamp": dts_from_hex(payload[18:30]),
|
|
655
|
-
"state": FAULT_STATE.get(payload[2:4], payload[2:4]),
|
|
656
|
-
"type": FAULT_TYPE.get(payload[8:10], payload[8:10]),
|
|
657
|
-
SZ_DEVICE_CLASS: FAULT_DEVICE_CLASS.get(payload[12:14], payload[12:14]),
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
if payload[10:12] == FC and result[SZ_DEVICE_CLASS] == SZ_ACTUATOR:
|
|
661
|
-
result[SZ_DEVICE_CLASS] = DEV_ROLE_MAP[DEV_ROLE.APP] # actual evohome UI
|
|
662
|
-
elif payload[10:12] == FA and result[SZ_DEVICE_CLASS] == SZ_ACTUATOR:
|
|
663
|
-
result[SZ_DEVICE_CLASS] = DEV_ROLE_MAP[DEV_ROLE.HTG] # speculative
|
|
664
|
-
elif payload[10:12] == F9 and result[SZ_DEVICE_CLASS] == SZ_ACTUATOR:
|
|
665
|
-
result[SZ_DEVICE_CLASS] = DEV_ROLE_MAP[DEV_ROLE.HT1] # speculative
|
|
666
|
-
|
|
667
|
-
if payload[12:14] != "00": # TODO: Controller
|
|
668
|
-
key_name = SZ_ZONE_IDX if int(payload[10:12], 16) < 16 else SZ_DOMAIN_ID
|
|
669
|
-
result.update({key_name: payload[10:12]})
|
|
670
|
-
|
|
671
|
-
if payload[38:] == "000002": # "00:000002 for Unknown?
|
|
672
|
-
result.update({SZ_DEVICE_ID: None})
|
|
673
|
-
elif payload[38:] not in ("000000", "000001"): # "00:000001 for Controller?
|
|
674
|
-
result.update({SZ_DEVICE_ID: hex_id_to_dev_id(payload[38:])})
|
|
675
|
-
|
|
676
|
-
result.update(
|
|
677
|
-
{
|
|
678
|
-
f"_{SZ_UNKNOWN}_3": payload[6:8], # B0 ?priority
|
|
679
|
-
f"_{SZ_UNKNOWN}_7": payload[14:18], # 0000
|
|
680
|
-
f"_{SZ_UNKNOWN}_15": payload[30:38], # FFFF7000/1/2
|
|
681
|
-
}
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
return {"log_entry": [v for k, v in result.items() if k != "log_idx"]}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
@parser_decorator # unknown_042f, from STA, VMS
|
|
688
|
-
def parser_042f(payload, msg) -> dict:
|
|
689
|
-
# .I --- 34:064023 --:------ 34:064023 042F 008 00-0000-0023-0023-F5
|
|
690
|
-
# .I --- 34:064023 --:------ 34:064023 042F 008 00-0000-0024-0024-F5
|
|
691
|
-
# .I --- 34:064023 --:------ 34:064023 042F 008 00-0000-0025-0025-F5
|
|
692
|
-
# .I --- 34:064023 --:------ 34:064023 042F 008 00-0000-0026-0026-F5
|
|
693
|
-
# .I --- 34:092243 --:------ 34:092243 042F 008 00-0001-0021-0022-01
|
|
694
|
-
# .I 34:011469 --:------ 34:011469 042F 008 00-0001-0003-0004-BC
|
|
695
|
-
|
|
696
|
-
# .I --- 32:168090 --:------ 32:168090 042F 009 00-0000100F00105050
|
|
697
|
-
# .I --- 32:166025 --:------ 32:166025 042F 009 00-050E0B0C00111470
|
|
698
|
-
|
|
699
|
-
return {
|
|
700
|
-
"counter_1": f"0x{payload[2:6]}",
|
|
701
|
-
"counter_3": f"0x{payload[6:10]}",
|
|
702
|
-
"counter_5": f"0x{payload[10:14]}",
|
|
703
|
-
f"{SZ_UNKNOWN}_7": f"0x{payload[14:]}",
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
@parser_decorator # TODO: unknown_0b04, from THM (only when its a CTL?)
|
|
708
|
-
def parser_0b04(payload, msg) -> dict:
|
|
709
|
-
# .I --- --:------ --:------ 12:207082 0B04 002 00C8 # batch of 3, every 24h
|
|
710
|
-
|
|
711
|
-
return {
|
|
712
|
-
f"{SZ_UNKNOWN}_1": payload[2:],
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
@parser_decorator # mixvalve_config (zone), FAN
|
|
717
|
-
def parser_1030(payload, msg) -> dict:
|
|
718
|
-
# .I --- 01:145038 --:------ 01:145038 1030 016 0A-C80137-C9010F-CA0196-CB0100-CC0101
|
|
719
|
-
# .I --- --:------ --:------ 12:144017 1030 016 01-C80137-C9010F-CA0196-CB010F-CC0101
|
|
720
|
-
# RP --- 32:155617 18:005904 --:------ 1030 007 00-200100-21011F
|
|
721
|
-
|
|
722
|
-
def _parser(seqx) -> dict:
|
|
723
|
-
assert seqx[2:4] == "01", seqx[2:4]
|
|
724
|
-
|
|
725
|
-
param_name = {
|
|
726
|
-
"20": "unknown_20", # HVAC
|
|
727
|
-
"21": "unknown_21", # HVAC
|
|
728
|
-
"C8": "max_flow_setpoint", # 55 (0-99) C
|
|
729
|
-
"C9": "min_flow_setpoint", # 15 (0-50) C
|
|
730
|
-
"CA": "valve_run_time", # 150 (0-240) sec, aka actuator_run_time
|
|
731
|
-
"CB": "pump_run_time", # 15 (0-99) sec
|
|
732
|
-
"CC": "boolean_cc", # ?boolean?
|
|
733
|
-
}[seqx[:2]]
|
|
734
|
-
|
|
735
|
-
return {param_name: int(seqx[4:], 16)}
|
|
736
|
-
|
|
737
|
-
assert (msg.len - 1) / 3 in (2, 5), msg.len
|
|
738
|
-
# assert payload[30:] in ("00", "01"), payload[30:]
|
|
739
|
-
|
|
740
|
-
params = [_parser(payload[i : i + 6]) for i in range(2, len(payload), 6)]
|
|
741
|
-
return {k: v for x in params for k, v in x.items()}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
@parser_decorator # device_battery (battery_state)
|
|
745
|
-
def parser_1060(payload, msg) -> dict:
|
|
746
|
-
"""Return the battery state.
|
|
747
|
-
|
|
748
|
-
Some devices (04:) will also report battery level.
|
|
749
|
-
"""
|
|
750
|
-
# 06:48:23.948 049 I --- 12:010740 --:------ 12:010740 1060 003 00FF01
|
|
751
|
-
# 16:18:43.515 051 I --- 12:010740 --:------ 12:010740 1060 003 00FF00
|
|
752
|
-
# 16:14:44.180 054 I --- 04:056057 --:------ 04:056057 1060 003 002800
|
|
753
|
-
# 17:34:35.460 087 I --- 04:189076 --:------ 01:145038 1060 003 026401
|
|
754
|
-
|
|
755
|
-
assert msg.len == 3, msg.len
|
|
756
|
-
assert payload[4:6] in ("00", "01")
|
|
757
|
-
|
|
758
|
-
return {
|
|
759
|
-
"battery_low": payload[4:] == "00",
|
|
760
|
-
"battery_level": percent_from_hex(payload[2:4]),
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
@parser_decorator # max_ch_setpoint (supply high limit)
|
|
765
|
-
def parser_1081(payload, msg) -> dict:
|
|
766
|
-
return {SZ_SETPOINT: temp_from_hex(payload[2:])}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
@parser_decorator # unknown_1090 (non-Evohome, e.g. ST9520C)
|
|
770
|
-
def parser_1090(payload, msg) -> dict:
|
|
771
|
-
# 14:08:05.176 095 RP --- 23:100224 22:219457 --:------ 1090 005 007FFF01F4
|
|
772
|
-
# 18:08:05.809 095 RP --- 23:100224 22:219457 --:------ 1090 005 007FFF01F4
|
|
773
|
-
|
|
774
|
-
# this is an educated guess
|
|
775
|
-
assert msg.len == 5, _INFORM_DEV_MSG
|
|
776
|
-
assert int(payload[:2], 16) < 2, _INFORM_DEV_MSG
|
|
777
|
-
|
|
778
|
-
return {
|
|
779
|
-
f"{SZ_TEMPERATURE}_0": temp_from_hex(payload[2:6]),
|
|
780
|
-
f"{SZ_TEMPERATURE}_1": temp_from_hex(payload[6:10]),
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
@parser_decorator # unknown_1098, from OTB
|
|
785
|
-
def parser_1098(payload, msg) -> dict:
|
|
786
|
-
|
|
787
|
-
assert payload == "00C8", _INFORM_DEV_MSG
|
|
788
|
-
|
|
789
|
-
return {
|
|
790
|
-
f"_{SZ_PAYLOAD}": payload,
|
|
791
|
-
f"_{SZ_VALUE}": {"00": False, "C8": True}.get(
|
|
792
|
-
payload[2:], percent_from_hex(payload[2:])
|
|
793
|
-
),
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
@parser_decorator # dhw (cylinder) params # FIXME: a bit messy
|
|
798
|
-
def parser_10a0(payload, msg) -> dict:
|
|
799
|
-
# RQ --- 07:045960 01:145038 --:------ 10A0 006 00-1087-00-03E4 # RQ/RP, every 24h
|
|
800
|
-
# RP --- 01:145038 07:045960 --:------ 10A0 006 00-109A-00-03E8
|
|
801
|
-
# RP --- 10:048122 18:006402 --:------ 10A0 003 00-1B58
|
|
802
|
-
|
|
803
|
-
# these may not be reliable...
|
|
804
|
-
# RQ --- 01:136410 10:067219 --:------ 10A0 002 0000
|
|
805
|
-
# RQ --- 07:017494 01:078710 --:------ 10A0 006 00-1566-00-03E4
|
|
806
|
-
|
|
807
|
-
# RQ --- 07:045960 01:145038 --:------ 10A0 006 00-31FF-00-31FF # null
|
|
808
|
-
# RQ --- 07:045960 01:145038 --:------ 10A0 006 00-1770-00-03E8
|
|
809
|
-
# RQ --- 07:045960 01:145038 --:------ 10A0 006 00-1374-00-03E4
|
|
810
|
-
# RQ --- 07:030741 01:102458 --:------ 10A0 006 00-181F-00-03E4
|
|
811
|
-
# RQ --- 07:036831 23:100224 --:------ 10A0 006 01-1566-00-03E4 # non-evohome
|
|
812
|
-
|
|
813
|
-
# these from a RFG...
|
|
814
|
-
# RQ --- 30:185469 01:037519 --:------ 0005 002 000E
|
|
815
|
-
# RP --- 01:037519 30:185469 --:------ 0005 004 000E0300 # two DHW valves
|
|
816
|
-
# RQ --- 30:185469 01:037519 --:------ 10A0 001 01 (01 )
|
|
817
|
-
|
|
818
|
-
if msg.verb == RQ and msg.len == 1: # some RQs have a payload (why?)
|
|
819
|
-
# 045 RQ --- 07:045960 01:145038 --:------ 10A0 006 0013740003E4
|
|
820
|
-
# 037 RQ --- 18:013393 01:145038 --:------ 10A0 001 00
|
|
821
|
-
# 054 RP --- 01:145038 18:013393 --:------ 10A0 006 0013880003E8
|
|
822
|
-
return {}
|
|
823
|
-
|
|
824
|
-
assert msg.len in (1, 3, 6), msg.len # OTB uses 3, evohome uses 6
|
|
825
|
-
assert payload[:2] in ("00", "01"), payload[:2] # can be two DHW valves/system
|
|
826
|
-
|
|
827
|
-
result = {}
|
|
828
|
-
if msg.len >= 2:
|
|
829
|
-
setpoint = temp_from_hex(payload[2:6]) # 255 for OTB? iff no DHW?
|
|
830
|
-
result = {SZ_SETPOINT: None if setpoint == 255 else setpoint} # 30.0-85.0 C
|
|
831
|
-
if msg.len >= 4:
|
|
832
|
-
result["overrun"] = int(payload[6:8], 16) # 0-10 minutes
|
|
833
|
-
if msg.len >= 6:
|
|
834
|
-
result["differential"] = temp_from_hex(payload[8:12]) # 1.0-10.0 C
|
|
835
|
-
|
|
836
|
-
return result
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
@parser_decorator # unknown_10b0, from OTB
|
|
840
|
-
def parser_10b0(payload, msg) -> dict:
|
|
841
|
-
|
|
842
|
-
assert payload == "0000", _INFORM_DEV_MSG
|
|
843
|
-
|
|
844
|
-
return {
|
|
845
|
-
f"_{SZ_PAYLOAD}": payload,
|
|
846
|
-
f"_{SZ_VALUE}": {"00": False, "C8": True}.get(
|
|
847
|
-
payload[2:], percent_from_hex(payload[2:])
|
|
848
|
-
),
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
@parser_decorator # filter_change, HVAC
|
|
853
|
-
def parser_10d0(payload, msg) -> dict:
|
|
854
|
-
|
|
855
|
-
# 2022-07-03T22:52:34.571579 045 W --- 37:171871 32:155617 --:------ 10D0 002 00FF
|
|
856
|
-
# 2022-07-03T22:52:34.596526 066 I --- 32:155617 37:171871 --:------ 10D0 006 0047B44F0000
|
|
857
|
-
# then...
|
|
858
|
-
# 2022-07-03T23:14:23.854089 000 RQ --- 37:155617 32:155617 --:------ 10D0 002 0000
|
|
859
|
-
# 2022-07-03T23:14:23.876088 084 RP --- 32:155617 37:155617 --:------ 10D0 006 00B4B4C80000
|
|
860
|
-
|
|
861
|
-
# 00-FF resets the counter, 00-47-B4-4F-0000 is the value (71 180 79).
|
|
862
|
-
# Default is 180 180 200. The returned value is the amount of days (180),
|
|
863
|
-
# total amount of days till change (180), percentage (200)
|
|
864
|
-
|
|
865
|
-
if msg.verb == W_:
|
|
866
|
-
result = {"reset_counter": payload[2:4] == "FF"}
|
|
867
|
-
else:
|
|
868
|
-
result = {"days_remaining": int(payload[2:4], 16)}
|
|
869
|
-
|
|
870
|
-
if msg.len >= 3:
|
|
871
|
-
result.update({"days_lifetime": int(payload[4:6], 16)})
|
|
872
|
-
if msg.len >= 4:
|
|
873
|
-
result.update({"percent_remaining": percent_from_hex(payload[6:8])})
|
|
874
|
-
|
|
875
|
-
return result
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
@parser_decorator # device_info
|
|
879
|
-
def parser_10e0(payload, msg) -> dict:
|
|
880
|
-
if payload == "00": # some HVAC devices wil RP|10E0|00
|
|
881
|
-
return {}
|
|
882
|
-
|
|
883
|
-
assert msg.len in (19, 28, 29, 30, 36, 38), msg.len # >= 19, msg.len
|
|
884
|
-
|
|
885
|
-
payload = re.sub("(00)*$", "", payload) # remove trailing 00s
|
|
886
|
-
assert len(payload) >= 18 * 2
|
|
887
|
-
|
|
888
|
-
# if DEV_MODE: # TODO
|
|
889
|
-
try: # DEX
|
|
890
|
-
check_signature(msg.src.type, payload[2:20])
|
|
891
|
-
except ValueError as exc:
|
|
892
|
-
_LOGGER.warning(
|
|
893
|
-
f"{msg!r} < {_INFORM_DEV_MSG}, with the make/model of device: {msg.src} ({exc})"
|
|
894
|
-
)
|
|
895
|
-
|
|
896
|
-
description, _, unknown = payload[36:].partition("00")
|
|
897
|
-
assert msg.verb == RP or not unknown, f"{unknown}"
|
|
898
|
-
|
|
899
|
-
result = {
|
|
900
|
-
"date_2": date_from_hex(payload[20:28]) or "0000-00-00", # manufactured?
|
|
901
|
-
"date_1": date_from_hex(payload[28:36]) or "0000-00-00", # firmware?
|
|
902
|
-
# "manufacturer_group": payload[2:6], # 0001/0002
|
|
903
|
-
"manufacturer_sub_id": payload[6:8],
|
|
904
|
-
"product_id": payload[8:10], # if CH/DHW: matches device_type (sometimes)
|
|
905
|
-
# "software_ver_id": payload[10:12],
|
|
906
|
-
# "list_ver_id": payload[12:14], # if FF/01 is CH/DHW, then 01/FF
|
|
907
|
-
"oem_code": payload[14:16], # 00/FF is CH/DHW, 01/6x is HVAC
|
|
908
|
-
# # "additional_ver_a": payload[16:18],
|
|
909
|
-
# # "additional_ver_b": payload[18:20],
|
|
910
|
-
"description": bytearray.fromhex(description).decode(),
|
|
911
|
-
}
|
|
912
|
-
if msg.verb == RP and unknown: # TODO: why only OTBs do this?
|
|
913
|
-
result[f"_{SZ_UNKNOWN}"] = unknown
|
|
914
|
-
return result
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
@parser_decorator # device_id
|
|
918
|
-
def parser_10e1(payload, msg) -> dict:
|
|
919
|
-
return {SZ_DEVICE_ID: hex_id_to_dev_id(payload[2:])}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
@parser_decorator # unknown_10e2 - HVAC
|
|
923
|
-
def parser_10e2(payload, msg) -> dict:
|
|
924
|
-
# .I --- --:------ --:------ 20:231151 10E2 003 00AD74 # every 2 minutes
|
|
925
|
-
|
|
926
|
-
assert payload[:2] == "00", _INFORM_DEV_MSG
|
|
927
|
-
assert len(payload) == 6, _INFORM_DEV_MSG
|
|
928
|
-
|
|
929
|
-
return {
|
|
930
|
-
"counter": int(payload[2:], 16),
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
@parser_decorator # tpi_params (domain/zone/device) # FIXME: a bit messy
|
|
935
|
-
def parser_1100(payload, msg) -> dict:
|
|
936
|
-
def complex_idx(seqx) -> dict:
|
|
937
|
-
return {SZ_DOMAIN_ID: seqx} if seqx[:1] == "F" else {} # only FC
|
|
938
|
-
|
|
939
|
-
if msg.src.type == DEV_TYPE_MAP.JIM: # Honeywell Japser, DEX
|
|
940
|
-
assert msg.len == 19, msg.len
|
|
941
|
-
return {
|
|
942
|
-
"ordinal": f"0x{payload[2:8]}",
|
|
943
|
-
"blob": payload[8:],
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
if msg.verb == RQ and msg.len == 1: # some RQs have a payload (why?)
|
|
947
|
-
return complex_idx(payload[:2])
|
|
948
|
-
|
|
949
|
-
assert int(payload[2:4], 16) / 4 in range(1, 13), payload[2:4]
|
|
950
|
-
assert int(payload[4:6], 16) / 4 in range(1, 31), payload[4:6]
|
|
951
|
-
assert int(payload[6:8], 16) / 4 in range(0, 16), payload[6:8]
|
|
952
|
-
|
|
953
|
-
# for: TPI // heatpump
|
|
954
|
-
# - cycle_rate: 6 (3, 6, 9, 12) // ?? (1-9)
|
|
955
|
-
# - min_on_time: 1 (1-5) // ?? (1, 5, 10,...30)
|
|
956
|
-
# - min_off_time: 1 (1-?) // ?? (0, 5, 10, 15)
|
|
957
|
-
|
|
958
|
-
def _parser(seqx) -> dict:
|
|
959
|
-
return {
|
|
960
|
-
"cycle_rate": int(int(payload[2:4], 16) / 4), # cycles/hour
|
|
961
|
-
"min_on_time": int(payload[4:6], 16) / 4, # min
|
|
962
|
-
"min_off_time": int(payload[6:8], 16) / 4, # min
|
|
963
|
-
f"_{SZ_UNKNOWN}_0": payload[8:10], # always 00, FF?
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
result = _parser(payload)
|
|
967
|
-
|
|
968
|
-
if msg.len > 5:
|
|
969
|
-
assert (
|
|
970
|
-
payload[10:14] == "7FFF" or 1.5 <= temp_from_hex(payload[10:14]) <= 3.0
|
|
971
|
-
), f"unexpected value for PBW: {payload[10:14]}"
|
|
972
|
-
|
|
973
|
-
result.update(
|
|
974
|
-
{
|
|
975
|
-
"proportional_band_width": temp_from_hex(payload[10:14]),
|
|
976
|
-
f"_{SZ_UNKNOWN}_1": payload[14:], # always 01?
|
|
977
|
-
}
|
|
978
|
-
)
|
|
979
|
-
|
|
980
|
-
return {
|
|
981
|
-
**complex_idx(payload[:2]),
|
|
982
|
-
**result,
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
@parser_decorator # unknown_11f0, from heatpump relay
|
|
987
|
-
def parser_11f0(payload, msg) -> dict:
|
|
988
|
-
|
|
989
|
-
assert payload == "000009000000000000", _INFORM_DEV_MSG
|
|
990
|
-
|
|
991
|
-
return {
|
|
992
|
-
SZ_PAYLOAD: payload,
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
@parser_decorator # dhw cylinder temperature
|
|
997
|
-
def parser_1260(payload, msg) -> dict:
|
|
998
|
-
return {SZ_TEMPERATURE: temp_from_hex(payload[2:])}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
@parser_decorator # outdoor humidity
|
|
1002
|
-
def parser_1280(payload, msg) -> dict:
|
|
1003
|
-
# educated guess - this packet never seen in the wild
|
|
1004
|
-
|
|
1005
|
-
rh = percent_from_hex(payload[2:4], high_res=False)
|
|
1006
|
-
if msg.len == 2:
|
|
1007
|
-
return {SZ_OUTDOOR_HUMIDITY: rh}
|
|
1008
|
-
|
|
1009
|
-
return {
|
|
1010
|
-
SZ_OUTDOOR_HUMIDITY: rh,
|
|
1011
|
-
SZ_TEMPERATURE: temp_from_hex(payload[4:8]),
|
|
1012
|
-
"dewpoint_temp": temp_from_hex(payload[8:12]),
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
@parser_decorator # outdoor temperature
|
|
1017
|
-
def parser_1290(payload, msg) -> dict:
|
|
1018
|
-
# evohome responds to an RQ
|
|
1019
|
-
return {SZ_TEMPERATURE: temp_from_hex(payload[2:])}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
@parser_decorator # co2_level
|
|
1023
|
-
def parser_1298(payload, msg) -> dict:
|
|
1024
|
-
# .I --- 37:258565 --:------ 37:258565 1298 003 0007D0
|
|
1025
|
-
FAULT_CODES_CO2 = {
|
|
1026
|
-
"80": "sensor short circuit",
|
|
1027
|
-
"81": "sensor open",
|
|
1028
|
-
"83": "sensor value too high",
|
|
1029
|
-
"84": "sensor value too low",
|
|
1030
|
-
"85": "sensor unreliable",
|
|
1031
|
-
}
|
|
1032
|
-
if fault := FAULT_CODES_CO2.get(payload[:2]):
|
|
1033
|
-
return {"sensor_fault": fault}
|
|
1034
|
-
|
|
1035
|
-
return {SZ_CO2_LEVEL: double_from_hex(payload[2:])}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
@parser_decorator # indoor_humidity
|
|
1039
|
-
def parser_12a0(payload, msg) -> dict:
|
|
1040
|
-
|
|
1041
|
-
FAULT_CODES_RHUM = {
|
|
1042
|
-
"EF": "sensor not available",
|
|
1043
|
-
"F0": "sensor short circuit",
|
|
1044
|
-
"F1": "sensor open",
|
|
1045
|
-
"F2": "sensor not available",
|
|
1046
|
-
"F3": "sensor value too high",
|
|
1047
|
-
"F4": "sensor value too low",
|
|
1048
|
-
"F5": "sensor unreliable",
|
|
1049
|
-
} # relative humidity sensor
|
|
1050
|
-
|
|
1051
|
-
assert payload[2:4] in FAULT_CODES_RHUM or int(payload[2:4], 16) <= 100
|
|
1052
|
-
if fault := FAULT_CODES_RHUM.get(payload[2:4]):
|
|
1053
|
-
return {"sensor_fault": fault}
|
|
1054
|
-
|
|
1055
|
-
# FAULT_CODES_TEMP = {
|
|
1056
|
-
# "7F": "sensor not available",
|
|
1057
|
-
# "80": "sensor short circuit",
|
|
1058
|
-
# "81": "sensor open",
|
|
1059
|
-
# "82": "sensor not available",
|
|
1060
|
-
# "83": "sensor value too high",
|
|
1061
|
-
# "84": "sensor value too low",
|
|
1062
|
-
# "85": "sensor unreliable",
|
|
1063
|
-
# }
|
|
1064
|
-
# assert payload[4:6] in FAULT_CODES_TEMP or ...
|
|
1065
|
-
# if (fault := FAULT_CODES_TEMP.get(payload[2:4])):
|
|
1066
|
-
# return {"sensor_fault": fault}
|
|
1067
|
-
|
|
1068
|
-
rh = percent_from_hex(payload[2:4], high_res=False)
|
|
1069
|
-
if msg.len == 2:
|
|
1070
|
-
return {SZ_INDOOR_HUMIDITY: rh}
|
|
1071
|
-
|
|
1072
|
-
return {
|
|
1073
|
-
SZ_INDOOR_HUMIDITY: rh,
|
|
1074
|
-
SZ_TEMPERATURE: temp_from_hex(payload[4:8]),
|
|
1075
|
-
"dewpoint_temp": temp_from_hex(payload[8:12]),
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
@parser_decorator # window_state (of a device/zone)
|
|
1080
|
-
def parser_12b0(payload, msg) -> dict:
|
|
1081
|
-
assert payload[2:] in ("0000", "C800", "FFFF"), payload[2:] # "FFFF" means N/A
|
|
1082
|
-
|
|
1083
|
-
return {
|
|
1084
|
-
SZ_WINDOW_OPEN: bool_from_hex(payload[2:4]),
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
@parser_decorator # displayed temperature (on a TR87RF bound to a RFG100)
|
|
1089
|
-
def parser_12c0(payload, msg) -> dict:
|
|
1090
|
-
|
|
1091
|
-
if payload[2:4] == "80":
|
|
1092
|
-
temp = None
|
|
1093
|
-
elif payload[4:] == "00": # units are 1.0 F
|
|
1094
|
-
temp = int(payload[2:4], 16)
|
|
1095
|
-
else: # if payload[4:] == "01": # units are 0.5 C
|
|
1096
|
-
temp = int(payload[2:4], 16) / 2
|
|
1097
|
-
|
|
1098
|
-
return {
|
|
1099
|
-
SZ_TEMPERATURE: temp,
|
|
1100
|
-
"units": {"00": "Fahrenheit", "01": "Celsius"}[payload[4:6]],
|
|
1101
|
-
f"_{SZ_UNKNOWN}_6": payload[6:],
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
@parser_decorator # air_quality, HVAC
|
|
1106
|
-
def parser_12c8(payload, msg) -> dict:
|
|
1107
|
-
# 04:50:01.616 080 I --- 37:261128 --:------ 37:261128 31DA 029 00A740-05133AEF7FFF7FFF7FFF7FFFF808EF1805000000EFEF7FFF7FFF # noqa: E501
|
|
1108
|
-
# 04:50:01.717 078 I --- 37:261128 --:------ 37:261128 12C8 003 00A740
|
|
1109
|
-
# 04:50:31.443 078 I --- 37:261128 --:------ 37:261128 31DA 029 007A40-05993AEF7FFF7FFF7FFF7FFFF808EF1807000000EFEF7FFF7FFF # noqa: E501
|
|
1110
|
-
# 04:50:31.544 078 I --- 37:261128 --:------ 37:261128 12C8 003 007A40
|
|
1111
|
-
# 04:51:40.262 079 I --- 37:261128 --:------ 37:261128 31DA 029 009540-054B3AEF7FFF7FFF7FFF7FFFF808EF180E000000EFEF7FFF7FFF # noqa: E501
|
|
1112
|
-
# 04:51:41.192 078 I --- 37:261128 --:------ 37:261128 12C8 003 009540
|
|
1113
|
-
|
|
1114
|
-
return {
|
|
1115
|
-
SZ_AIR_QUALITY: percent_from_hex(payload[2:4]), # 31DA[2:4]
|
|
1116
|
-
SZ_AIR_QUALITY_BASE: int(payload[4:6], 16), # 31DA[4:6]
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
@parser_decorator # dhw_flow_rate
|
|
1121
|
-
def parser_12f0(payload, msg) -> dict:
|
|
1122
|
-
return {"dhw_flow_rate": temp_from_hex(payload[2:])}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
@parser_decorator # ch_pressure
|
|
1126
|
-
def parser_1300(payload, msg) -> dict:
|
|
1127
|
-
return {SZ_PRESSURE: temp_from_hex(payload[2:])} # is 2's complement still
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
@parser_decorator # programme_scheme, HVAC
|
|
1131
|
-
def parser_1470(payload, msg) -> dict:
|
|
1132
|
-
# Seen on Orcon: see 1470, 1F70, 22B0
|
|
1133
|
-
|
|
1134
|
-
SCHEDULE_SCHEME = {
|
|
1135
|
-
"9": "one_per_week",
|
|
1136
|
-
"A": "two_per_week", # week_day, week_end
|
|
1137
|
-
"B": "one_each_day", # seven_per_week (default?)
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
assert payload[8:10] == "80", _INFORM_DEV_MSG
|
|
1141
|
-
assert msg.verb == W_ or payload[4:8] == "0E60", _INFORM_DEV_MSG
|
|
1142
|
-
assert msg.verb == W_ or payload[10:] == "2A0108", _INFORM_DEV_MSG
|
|
1143
|
-
assert msg.verb != W_ or payload[4:] == "000080000000", _INFORM_DEV_MSG
|
|
1144
|
-
|
|
1145
|
-
# schedule...
|
|
1146
|
-
# [2:3] - 1, every/all days, 1&6, weekdays/weekends, 1-7, each individual day
|
|
1147
|
-
# [3:4] - # setpoints/day (default 3)
|
|
1148
|
-
assert payload[2:3] in SCHEDULE_SCHEME and (
|
|
1149
|
-
payload[3:4] in ("2", "3", "4", "5", "6")
|
|
1150
|
-
), _INFORM_DEV_MSG
|
|
1151
|
-
|
|
1152
|
-
return {
|
|
1153
|
-
"scheme": SCHEDULE_SCHEME.get(payload[2:3], f"unknown_{payload[2:3]}"),
|
|
1154
|
-
"daily_setpoints": payload[3:4],
|
|
1155
|
-
f"_{SZ_VALUE}_4": payload[4:8],
|
|
1156
|
-
f"_{SZ_VALUE}_8": payload[8:10],
|
|
1157
|
-
f"_{SZ_VALUE}_10": payload[10:],
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
@parser_decorator # system_sync
|
|
1162
|
-
def parser_1f09(payload, msg) -> dict:
|
|
1163
|
-
# 22:51:19.287 067 I --- --:------ --:------ 12:193204 1F09 003 010A69
|
|
1164
|
-
# 22:51:19.318 068 I --- --:------ --:------ 12:193204 2309 003 010866
|
|
1165
|
-
# 22:51:19.321 067 I --- --:------ --:------ 12:193204 30C9 003 0108C3
|
|
1166
|
-
|
|
1167
|
-
assert msg.len == 3, f"length is {msg.len}, expecting 3"
|
|
1168
|
-
assert payload[:2] in ("00", "01", F8, FF) # W/F8
|
|
1169
|
-
|
|
1170
|
-
seconds = int(payload[2:6], 16) / 10
|
|
1171
|
-
next_sync = msg.dtm + td(seconds=seconds)
|
|
1172
|
-
|
|
1173
|
-
return {
|
|
1174
|
-
"remaining_seconds": seconds,
|
|
1175
|
-
"_next_sync": dt.strftime(next_sync, "%H:%M:%S"),
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
@parser_decorator # dhw_mode
|
|
1180
|
-
def parser_1f41(payload, msg) -> dict:
|
|
1181
|
-
# 053 RP --- 01:145038 18:013393 --:------ 1F41 006 00FF00FFFFFF # no stored DHW
|
|
1182
|
-
assert payload[4:6] in ZON_MODE_MAP, f"{payload[4:6]} (0xjj)"
|
|
1183
|
-
assert (
|
|
1184
|
-
payload[4:6] == ZON_MODE_MAP.TEMPORARY or msg.len == 6
|
|
1185
|
-
), f"{msg!r}: expected length 6"
|
|
1186
|
-
assert (
|
|
1187
|
-
payload[4:6] != ZON_MODE_MAP.TEMPORARY or msg.len == 12
|
|
1188
|
-
), f"{msg!r}: expected length 12"
|
|
1189
|
-
assert (
|
|
1190
|
-
payload[6:12] == "FFFFFF"
|
|
1191
|
-
), f"{msg!r}: expected FFFFFF instead of '{payload[6:12]}'"
|
|
1192
|
-
|
|
1193
|
-
result = {SZ_MODE: ZON_MODE_MAP.get(payload[4:6])}
|
|
1194
|
-
if payload[2:4] != "FF":
|
|
1195
|
-
result["active"] = {"00": False, "01": True, "FF": None}[payload[2:4]]
|
|
1196
|
-
# if payload[4:6] == ZON_MODE_MAP.COUNTDOWN:
|
|
1197
|
-
# result[SZ_UNTIL] = dtm_from_hex(payload[6:12])
|
|
1198
|
-
if payload[4:6] == ZON_MODE_MAP.TEMPORARY:
|
|
1199
|
-
result[SZ_UNTIL] = dtm_from_hex(payload[12:24])
|
|
1200
|
-
|
|
1201
|
-
return result
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
@parser_decorator # programme_config, HVAC
|
|
1205
|
-
def parser_1f70(payload, msg) -> dict:
|
|
1206
|
-
# Seen on Orcon: see 1470, 1F70, 22B0
|
|
1207
|
-
|
|
1208
|
-
try:
|
|
1209
|
-
assert payload[:2] == "00", f"expected 00, not {payload[:2]}"
|
|
1210
|
-
assert payload[2:4] in ("00", "01"), f"expected (00|01), not {payload[2:4]}"
|
|
1211
|
-
assert payload[4:8] == "0800", f"expected 0800, not {payload[4:8]}"
|
|
1212
|
-
assert payload[10:14] == "0000", f"expected 0000, not {payload[10:14]}"
|
|
1213
|
-
assert msg.verb in (RQ, W_) or payload[14:16] == "15"
|
|
1214
|
-
assert msg.verb in (I_, RP) or payload[14:16] == "00"
|
|
1215
|
-
assert msg.verb == RQ or payload[22:24] == "60"
|
|
1216
|
-
assert msg.verb != RQ or payload[22:24] == "00"
|
|
1217
|
-
assert msg.verb == RQ or payload[24:26] in ("E4", "E5", "E6"), _INFORM_DEV_MSG
|
|
1218
|
-
assert msg.verb == RP or payload[26:] == "000000"
|
|
1219
|
-
assert msg.verb != RP or payload[26:] == "008000"
|
|
1220
|
-
|
|
1221
|
-
except AssertionError as exc:
|
|
1222
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({exc})")
|
|
1223
|
-
|
|
1224
|
-
# assert int(payload[16:18], 16) < 7, _INFORM_DEV_MSG
|
|
1225
|
-
|
|
1226
|
-
return {
|
|
1227
|
-
"day_idx": payload[16:18], # depends upon 1470[3:4]?
|
|
1228
|
-
"setpoint_idx": payload[8:10], # needs to be mod 1470[3:4]?
|
|
1229
|
-
"start_time": f"{int(payload[18:20], 16):02d}:{int(payload[20:22], 16):02d}",
|
|
1230
|
-
"fan_speed_wip": payload[24:26], # # E4/E5/E6 / 00(RQ)
|
|
1231
|
-
f"_{SZ_VALUE}_02": payload[2:4], # # 00/01 / 00(RQ)
|
|
1232
|
-
f"_{SZ_VALUE}_04": payload[4:8], # # 0800
|
|
1233
|
-
f"_{SZ_VALUE}_10": payload[10:14], # 0000
|
|
1234
|
-
f"_{SZ_VALUE}_14": payload[14:16], # 15(RP,I) / 00(RQ,W)
|
|
1235
|
-
f"_{SZ_VALUE}_22": payload[22:24], # 60 / 00(RQ)
|
|
1236
|
-
f"_{SZ_VALUE}_26": payload[26:], # # 008000(RP) / 000000(I/RQ/W)
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
@parser_decorator # rf_bind
|
|
1241
|
-
def parser_1fc9(payload, msg) -> list:
|
|
1242
|
-
# .I --- 01:145038 --:------ 01:145038 1FC9 012 F6-2D49-06368E F6-1FC9-06368E
|
|
1243
|
-
|
|
1244
|
-
# .I is missing?
|
|
1245
|
-
# .W --- 10:048122 01:145038 --:------ 1FC9 006 003EF028BBFA
|
|
1246
|
-
# .I --- 01:145038 10:048122 --:------ 1FC9 006 00FFFF06368E
|
|
1247
|
-
|
|
1248
|
-
# .I --- 07:045960 --:------ 07:045960 1FC9 012 0012601CB388001FC91CB388
|
|
1249
|
-
# .W --- 01:145038 07:045960 --:------ 1FC9 006 0010A006368E
|
|
1250
|
-
# .I --- 07:045960 01:145038 --:------ 1FC9 006 0012601CB388
|
|
1251
|
-
|
|
1252
|
-
# .I --- 01:145038 --:------ 01:145038 1FC9 018 FA000806368EFC3B0006368EFA1FC906368E
|
|
1253
|
-
# .W --- 13:081807 01:145038 --:------ 1FC9 006 003EF0353F8F
|
|
1254
|
-
# .I --- 01:145038 13:081807 --:------ 1FC9 006 00FFFF06368E
|
|
1255
|
-
|
|
1256
|
-
# this is an array of codes
|
|
1257
|
-
# 049 I --- 01:145038 --:------ 01:145038 1FC9 018 07-0008-06368E FC-3B00-06368E 07-1FC9-06368E # noqa: E501
|
|
1258
|
-
# 047 I --- 01:145038 --:------ 01:145038 1FC9 018 FA-0008-06368E FC-3B00-06368E FA-1FC9-06368E # noqa: E501
|
|
1259
|
-
# 065 I --- 01:145038 --:------ 01:145038 1FC9 024 FC-0008-06368E FC-3150-06368E FB-3150-06368E FC-1FC9-06368E # noqa: E501
|
|
1260
|
-
|
|
1261
|
-
# HW valve binding:
|
|
1262
|
-
# 063 I --- 01:145038 --:------ 01:145038 1FC9 018 FA-0008-06368E FC-3B00-06368E FA-1FC9-06368E # noqa: E501
|
|
1263
|
-
# CH valve binding:
|
|
1264
|
-
# 071 I --- 01:145038 --:------ 01:145038 1FC9 018 F9-0008-06368E FC-3B00-06368E F9-1FC9-06368E # noqa: E501
|
|
1265
|
-
# ZoneValve zone binding
|
|
1266
|
-
# 045 W --- 13:106039 01:145038 --:------ 1FC9 012 00-3EF0-359E37 00-3B00-359E37
|
|
1267
|
-
# DHW binding..
|
|
1268
|
-
# 045 W --- 13:163733 01:145038 --:------ 1FC9 012 00-3EF0-367F95 00-3B00-367F95
|
|
1269
|
-
|
|
1270
|
-
# 049 I --- 01:145038 --:------ 01:145038 1FC9 018 F9-0008-06368E FC-3B00-06368E F9-1FC9-06368E # noqa: E501
|
|
1271
|
-
|
|
1272
|
-
# the new (heatpump-aware) BDR91:
|
|
1273
|
-
# 045 RP --- 13:035462 18:013393 --:------ 1FC9 018 00-3EF0-348A86 00-11F0-348A86 90-7FE1-DD6ABD # noqa: E501
|
|
1274
|
-
|
|
1275
|
-
def _parser(seqx) -> list:
|
|
1276
|
-
if seqx[:2] not in ("90",):
|
|
1277
|
-
assert seqx[6:] == payload[6:12] # all with same controller
|
|
1278
|
-
if seqx[:2] not in (
|
|
1279
|
-
"63",
|
|
1280
|
-
"67",
|
|
1281
|
-
"6C",
|
|
1282
|
-
"90",
|
|
1283
|
-
F6,
|
|
1284
|
-
F9,
|
|
1285
|
-
FA,
|
|
1286
|
-
FB,
|
|
1287
|
-
FC,
|
|
1288
|
-
FF,
|
|
1289
|
-
): # or: not in DOMAIN_TYPE_MAP: ??
|
|
1290
|
-
assert int(seqx[:2], 16) < 16
|
|
1291
|
-
return [seqx[:2], seqx[2:6], hex_id_to_dev_id(seqx[6:])]
|
|
1292
|
-
|
|
1293
|
-
if msg.verb == I_ and msg.src is msg.dst:
|
|
1294
|
-
bind_step = "offer"
|
|
1295
|
-
elif msg.verb == W_ and msg.src is not msg.dst:
|
|
1296
|
-
bind_step = "accept"
|
|
1297
|
-
elif msg.verb == I_:
|
|
1298
|
-
bind_step = "confirm" # payload could be "00"
|
|
1299
|
-
else:
|
|
1300
|
-
bind_step = None # unknown
|
|
1301
|
-
|
|
1302
|
-
if payload == "00":
|
|
1303
|
-
return {"phase": bind_step, SZ_BINDINGS: []}
|
|
1304
|
-
|
|
1305
|
-
assert msg.len >= 6 and msg.len % 6 == 0, msg.len # assuming not RQ
|
|
1306
|
-
assert msg.verb in (I_, W_, RP), msg.verb # devices will respond to a RQ!
|
|
1307
|
-
# assert (
|
|
1308
|
-
# msg.src.id == hex_id_to_dev_id(payload[6:12])
|
|
1309
|
-
# ), f"{payload[6:12]} ({hex_id_to_dev_id(payload[6:12])})" # NOTE: use_regex
|
|
1310
|
-
bindings = [
|
|
1311
|
-
_parser(payload[i : i + 12])
|
|
1312
|
-
for i in range(0, len(payload), 12)
|
|
1313
|
-
# if payload[i : i + 2] != "90" # TODO: WIP, what is 90?
|
|
1314
|
-
]
|
|
1315
|
-
return {"phase": bind_step, SZ_BINDINGS: bindings}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
@parser_decorator # unknown_1fca, HVAC?
|
|
1319
|
-
def parser_1fca(payload, msg) -> list:
|
|
1320
|
-
# .W --- 30:248208 34:021943 --:------ 1FCA 009 00-01FF-7BC990-FFFFFF # sent x2
|
|
1321
|
-
|
|
1322
|
-
return {
|
|
1323
|
-
f"_{SZ_UNKNOWN}_0": payload[:2],
|
|
1324
|
-
f"_{SZ_UNKNOWN}_1": payload[2:6],
|
|
1325
|
-
"device_id_0": hex_id_to_dev_id(payload[6:12]),
|
|
1326
|
-
"device_id_1": hex_id_to_dev_id(payload[12:]),
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
@parser_decorator # unknown_1fd0, from OTB
|
|
1331
|
-
def parser_1fd0(payload, msg) -> dict:
|
|
1332
|
-
|
|
1333
|
-
assert payload == "0000000000000000", _INFORM_DEV_MSG
|
|
1334
|
-
|
|
1335
|
-
return {
|
|
1336
|
-
SZ_PAYLOAD: payload,
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
@parser_decorator # opentherm_sync, otb_sync
|
|
1341
|
-
def parser_1fd4(payload, msg) -> dict:
|
|
1342
|
-
return {"ticker": int(payload[2:], 16)}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
@parser_decorator # WIP: unknown, HVAC
|
|
1346
|
-
def parser_2210(payload, msg) -> dict:
|
|
1347
|
-
# RP --- 32:153258 18:005904 --:------ 2210 042 00FF 00FFFFFF0000000000FFFFFFFFFF 00FFFFFF0000000000FFFFFFFFFF FFFFFF000000000000000800
|
|
1348
|
-
# RP --- 32:153258 18:005904 --:------ 2210 042 00FF 00FFFF960000000003FFFFFFFFFF 00FFFF960000000003FFFFFFFFFF FFFFFF000000000000000800
|
|
1349
|
-
|
|
1350
|
-
assert payload in (
|
|
1351
|
-
"00FF" + "00FFFFFF0000000000FFFFFFFFFF" * 2 + "FFFFFF000000000000000800",
|
|
1352
|
-
"00FF" + "00FFFF960000000003FFFFFFFFFF" * 2 + "FFFFFF000000000000000800",
|
|
1353
|
-
), _INFORM_DEV_MSG
|
|
1354
|
-
|
|
1355
|
-
return {}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
@parser_decorator # now_next_setpoint - Programmer/Hometronics
|
|
1359
|
-
def parser_2249(payload, msg) -> dict:
|
|
1360
|
-
# see: https://github.com/jrosser/honeymon/blob/master/decoder.cpp#L357-L370
|
|
1361
|
-
# .I --- 23:100224 --:------ 23:100224 2249 007 00-7EFF-7EFF-FFFF
|
|
1362
|
-
|
|
1363
|
-
def _parser(seqx) -> dict:
|
|
1364
|
-
minutes = int(seqx[10:], 16)
|
|
1365
|
-
next_setpoint = msg.dtm + td(minutes=minutes)
|
|
1366
|
-
return {
|
|
1367
|
-
"setpoint_now": temp_from_hex(seqx[2:6]),
|
|
1368
|
-
"setpoint_next": temp_from_hex(seqx[6:10]),
|
|
1369
|
-
"minutes_remaining": minutes,
|
|
1370
|
-
"_next_setpoint": dt.strftime(next_setpoint, "%H:%M:%S"),
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
# the ST9520C can support two heating zones, so: msg.len in (7, 14)?
|
|
1374
|
-
if msg._has_array:
|
|
1375
|
-
return [
|
|
1376
|
-
{
|
|
1377
|
-
SZ_ZONE_IDX: payload[i : i + 2],
|
|
1378
|
-
**_parser(payload[i + 2 : i + 14]),
|
|
1379
|
-
}
|
|
1380
|
-
for i in range(0, len(payload), 14)
|
|
1381
|
-
]
|
|
1382
|
-
|
|
1383
|
-
return _parser(payload)
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
@parser_decorator # program_enabled, HVAC
|
|
1387
|
-
def parser_22b0(payload, msg) -> dict:
|
|
1388
|
-
# Seen on Orcon: see 1470, 1F70, 22B0
|
|
1389
|
-
|
|
1390
|
-
# .W --- 37:171871 32:155617 --:------ 22B0 002 0005 # enable
|
|
1391
|
-
# .I --- 32:155617 37:171871 --:------ 22B0 002 0005
|
|
1392
|
-
|
|
1393
|
-
# .W --- 37:171871 32:155617 --:------ 22B0 002 0006 # disable
|
|
1394
|
-
# .I --- 32:155617 37:171871 --:------ 22B0 002 0006
|
|
1395
|
-
|
|
1396
|
-
return {
|
|
1397
|
-
"enabled": {"06": False, "05": True}.get(payload[2:4]),
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
@parser_decorator # ufh_setpoint, TODO: max length = 24?
|
|
1402
|
-
def parser_22c9(payload, msg) -> list:
|
|
1403
|
-
# .I --- 02:001107 --:------ 02:001107 22C9 024 00-0834-0A28-01-0108340A2801-0208340A2801-0308340A2801 # noqa: E501
|
|
1404
|
-
# .I --- 02:001107 --:------ 02:001107 22C9 006 04-0834-0A28-01
|
|
1405
|
-
|
|
1406
|
-
# .I --- 21:064743 --:------ 21:064743 22C9 006 00-07D0-0834-02
|
|
1407
|
-
# .W --- 21:064743 02:250708 --:------ 22C9 006 03-07D0-0834-02
|
|
1408
|
-
# .I --- 02:250708 21:064743 --:------ 22C9 008 03-07D0-7FFF-02-02-03
|
|
1409
|
-
|
|
1410
|
-
def _parser(seqx) -> dict:
|
|
1411
|
-
assert seqx[10:] in ("01", "02"), f"is {seqx[10:]}, expecting 01/02"
|
|
1412
|
-
|
|
1413
|
-
return {
|
|
1414
|
-
"temp_low": temp_from_hex(seqx[2:6]),
|
|
1415
|
-
"temp_high": temp_from_hex(seqx[6:10]),
|
|
1416
|
-
f"_{SZ_UNKNOWN}_0": seqx[10:],
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
if msg._has_array:
|
|
1420
|
-
return [
|
|
1421
|
-
{
|
|
1422
|
-
SZ_UFH_IDX: payload[i : i + 2],
|
|
1423
|
-
**_parser(payload[i : i + 12]),
|
|
1424
|
-
}
|
|
1425
|
-
for i in range(0, len(payload), 12)
|
|
1426
|
-
]
|
|
1427
|
-
|
|
1428
|
-
return _parser(payload[:12]) # TODO: [12:]
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
@parser_decorator # unknown_22d0, HVAC system switch?
|
|
1432
|
-
def parser_22d0(payload, msg) -> dict:
|
|
1433
|
-
|
|
1434
|
-
# When closing H/C contact (or enabling cooling mode with buttons when H/C contact is closed) on HCE80 it sends following packet:
|
|
1435
|
-
# .I — 02:044994 --:------ 02:044994 22D0 004 0010000A < AssertionError…
|
|
1436
|
-
|
|
1437
|
-
# When H/C contact is opened, it send the following packet (although this packet is not transmitted, when cooling mode is disabled with buttons):
|
|
1438
|
-
# .I — 02:044994 --:------ 02:044994 22D0 004 0000000A < AssertionError…
|
|
1439
|
-
|
|
1440
|
-
# .I --- 02:001107 --:------ 02:001107 22D0 004 00000002 # an UFC
|
|
1441
|
-
|
|
1442
|
-
# .W --- 21:064743 02:250708 --:------ 22D0 008 0314001E-14030020
|
|
1443
|
-
# .I --- 02:250708 21:064743 --:------ 22D0 004 03130000
|
|
1444
|
-
# .I --- 02:250708 --:------ 02:250708 22D0 004 00130000 # sends 3x, 1s apart
|
|
1445
|
-
|
|
1446
|
-
assert payload in (
|
|
1447
|
-
"00000002",
|
|
1448
|
-
"00130000",
|
|
1449
|
-
"03130000",
|
|
1450
|
-
"0314001E14030020",
|
|
1451
|
-
), _INFORM_DEV_MSG
|
|
1452
|
-
|
|
1453
|
-
return {
|
|
1454
|
-
"idx": payload[:2],
|
|
1455
|
-
SZ_PAYLOAD: payload[2:],
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
@parser_decorator # desired boiler setpoint
|
|
1460
|
-
def parser_22d9(payload, msg) -> dict:
|
|
1461
|
-
return {SZ_SETPOINT: temp_from_hex(payload[2:6])}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
@parser_decorator # WIP: unknown, HVAC
|
|
1465
|
-
def parser_22e0(payload, msg) -> dict:
|
|
1466
|
-
# RP --- 32:155617 18:005904 --:------ 22E0 004 00-34-A0-1E
|
|
1467
|
-
# RP --- 32:153258 18:005904 --:------ 22E0 004 00-64-A0-1E
|
|
1468
|
-
def _parser(seqx) -> dict:
|
|
1469
|
-
assert int(seqx, 16) <= 200 or seqx == "E6" # only for 22E0, not 22E5/22E9
|
|
1470
|
-
return int(seqx, 16) / 200
|
|
1471
|
-
|
|
1472
|
-
try:
|
|
1473
|
-
return {
|
|
1474
|
-
f"percent_{i}": percent_from_hex(payload[i : i + 2])
|
|
1475
|
-
for i in range(2, len(payload), 2)
|
|
1476
|
-
}
|
|
1477
|
-
except ValueError:
|
|
1478
|
-
return {
|
|
1479
|
-
"percent_2": percent_from_hex(payload[2:4]),
|
|
1480
|
-
"percent_4": _parser(payload[4:6]),
|
|
1481
|
-
"percent_6": percent_from_hex(payload[6:8]),
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
@parser_decorator # WIP: unknown, HVAC
|
|
1486
|
-
def parser_22e5(payload, msg) -> dict:
|
|
1487
|
-
# RP --- 32:153258 18:005904 --:------ 22E5 004 00-96-C8-14
|
|
1488
|
-
# RP --- 32:155617 18:005904 --:------ 22E5 004 00-72-C8-14
|
|
1489
|
-
|
|
1490
|
-
return parser_22e0(payload, msg)
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
@parser_decorator # WIP: unknown, HVAC
|
|
1494
|
-
def parser_22e9(payload, msg) -> dict:
|
|
1495
|
-
# RP --- 32:153258 18:005904 --:------ 22E9 004 00C8C814
|
|
1496
|
-
# RP --- 32:155617 18:005904 --:------ 22E9 004 008CC814
|
|
1497
|
-
|
|
1498
|
-
return parser_22e0(payload, msg)
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
@parser_decorator # fan_speed (switch_mode), HVAC
|
|
1502
|
-
def parser_22f1(payload, msg) -> dict:
|
|
1503
|
-
# Orcon wireless remote 15RF
|
|
1504
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000007 # Absent mode // Afwezig (absence mode, aka: weg/away) - low & doesn't respond to sensors
|
|
1505
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000107 # Mode 1: Low // Stand 1 (position low)
|
|
1506
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000207 # Mode 2: Med // Stand 2 (position med)
|
|
1507
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000307 # Mode 3: High // Stand 3 (position high)
|
|
1508
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000407 # Auto
|
|
1509
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000507 # Auto
|
|
1510
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000607 # Party/boost
|
|
1511
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000707 # Off
|
|
1512
|
-
|
|
1513
|
-
# .I 015 --:------ --:------ 39:159057 22F1 003 000004 # TBA: off/standby?
|
|
1514
|
-
# .I 015 --:------ --:------ 39:159057 22F1 003 000104 # TBA: trickle/min-speed?
|
|
1515
|
-
# .I 015 --:------ --:------ 39:159057 22F1 003 000204 # low
|
|
1516
|
-
# .I 016 --:------ --:------ 39:159057 22F1 003 000304 # medium
|
|
1517
|
-
# .I 017 --:------ --:------ 39:159057 22F1 003 000404 # high (aka boost if timer)
|
|
1518
|
-
|
|
1519
|
-
# Scheme x: 0|x standby/off, 1|x min, 2+|x rate as % of max (Itho?)
|
|
1520
|
-
# Scheme 4: 0|4 standby/off, 1|4 auto, 2|4 low, 3|4 med, 4|4 high/boost
|
|
1521
|
-
# Scheme 7: only seen 000[2345]07 -- ? off, auto, rate x/4, +3 others?
|
|
1522
|
-
# Scheme A: only seen 000[239A]0A -- Normal, Boost (purge), HeaterOff & HeaterAuto
|
|
1523
|
-
|
|
1524
|
-
try:
|
|
1525
|
-
assert payload[0:2] in ("00", "63")
|
|
1526
|
-
assert not payload[4:] or int(payload[2:4], 16) <= int(
|
|
1527
|
-
payload[4:], 16
|
|
1528
|
-
), "mode_idx > mode_max"
|
|
1529
|
-
except AssertionError as exc:
|
|
1530
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({exc})")
|
|
1531
|
-
|
|
1532
|
-
if msg._addrs[0] == NON_DEV_ADDR: # and payload[4:6] == "04":
|
|
1533
|
-
from .ramses import _22F1_MODE_ITHO as _22F1_FAN_MODE # TODO: only if 04
|
|
1534
|
-
|
|
1535
|
-
_22f1_mode_set = ("", "04")
|
|
1536
|
-
_22f1_scheme = "itho"
|
|
1537
|
-
|
|
1538
|
-
# elif msg._addrs[0] == NON_DEV_ADDR: # and payload[4:6] == "04":
|
|
1539
|
-
# _22F1_FAN_MODE = {
|
|
1540
|
-
# f"{x:02X}": f"speed_{x}" for x in range(int(payload[4:6], 16) + 1)
|
|
1541
|
-
# } | {"00": "off"}
|
|
1542
|
-
|
|
1543
|
-
# _22f1_mode_set = (payload[4:6], )
|
|
1544
|
-
# _22f1_scheme = "itho_2"
|
|
1545
|
-
|
|
1546
|
-
elif payload[4:6] == "0A":
|
|
1547
|
-
from .ramses import _22F1_MODE_NUAIRE as _22F1_FAN_MODE
|
|
1548
|
-
|
|
1549
|
-
_22f1_mode_set = ("", "0A")
|
|
1550
|
-
_22f1_scheme = "nuaire"
|
|
1551
|
-
|
|
1552
|
-
else:
|
|
1553
|
-
from .ramses import _22F1_MODE_ORCON as _22F1_FAN_MODE
|
|
1554
|
-
|
|
1555
|
-
_22f1_mode_set = ("", "04", "07", "0B") # 0B?
|
|
1556
|
-
_22f1_scheme = "orcon"
|
|
1557
|
-
|
|
1558
|
-
try:
|
|
1559
|
-
assert payload[2:4] in _22F1_FAN_MODE, f"unknown fan_mode: {payload[2:4]}"
|
|
1560
|
-
assert payload[4:6] in _22f1_mode_set, f"unknown mode_set: {payload[4:6]}"
|
|
1561
|
-
except AssertionError as exc:
|
|
1562
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({exc})")
|
|
1563
|
-
|
|
1564
|
-
return {
|
|
1565
|
-
SZ_FAN_MODE: _22F1_FAN_MODE.get(payload[2:4], f"unknown_{payload[2:4]}"),
|
|
1566
|
-
"_scheme": _22f1_scheme,
|
|
1567
|
-
"_mode_idx": f"{int(payload[2:4], 16) & 0x0F:02X}",
|
|
1568
|
-
"_mode_max": payload[4:6] or None,
|
|
1569
|
-
# "_payload": payload,
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
@parser_decorator # WIP: unknown, HVAC (flow rate?)
|
|
1574
|
-
def parser_22f2(payload, msg) -> dict:
|
|
1575
|
-
# RP --- 32:155617 18:005904 --:------ 22F2 006 00-019B 01-0201
|
|
1576
|
-
# RP --- 32:155617 18:005904 --:------ 22F2 006 00-0174 01-0208
|
|
1577
|
-
# RP --- 32:155617 18:005904 --:------ 22F2 006 00-01E5 01-0201
|
|
1578
|
-
|
|
1579
|
-
def _parser(seqx) -> dict:
|
|
1580
|
-
assert seqx[:2] in ("00", "01"), f"is {seqx[:2]}, expecting 00/01"
|
|
1581
|
-
|
|
1582
|
-
return {
|
|
1583
|
-
"hvac_idx": seqx[:2],
|
|
1584
|
-
"measure": temp_from_hex(seqx[2:]),
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
return [_parser(payload[i : i + 6]) for i in range(0, len(payload), 6)]
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
@parser_decorator # fan_boost, HVAC
|
|
1591
|
-
def parser_22f3(payload, msg) -> dict:
|
|
1592
|
-
# .I 019 --:------ --:------ 39:159057 22F3 003 00000A # 10 mins
|
|
1593
|
-
# .I 022 --:------ --:------ 39:159057 22F3 003 000014 # 20 mins
|
|
1594
|
-
# .I 026 --:------ --:------ 39:159057 22F3 003 00001E # 30 mins
|
|
1595
|
-
# .I --- 29:151550 29:237552 --:------ 22F3 007 00023C-0304-0000 # 60 mins
|
|
1596
|
-
# .I --- 29:162374 29:237552 --:------ 22F3 007 00020F-0304-0000 # 15 mins
|
|
1597
|
-
# .I --- 29:162374 29:237552 --:------ 22F3 007 00020F-0304-0000 # 15 mins
|
|
1598
|
-
|
|
1599
|
-
# NOTE: for boost timer for high
|
|
1600
|
-
try:
|
|
1601
|
-
# assert payload[2:4] in ("00", "02", "12", "x52"), f"byte 1: {flag8(payload[2:4])}"
|
|
1602
|
-
assert msg.len <= 7 or payload[14:] == "0000", f"byte 7: {payload[14:]}"
|
|
1603
|
-
except AssertionError as exc:
|
|
1604
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({exc})")
|
|
1605
|
-
|
|
1606
|
-
new_speed = { # from now, until timer expiry
|
|
1607
|
-
0x00: "fan_boost", # # set fan off, or 'boost' mode?
|
|
1608
|
-
0x01: "per_request", # # set fan as per payload[6:10]?
|
|
1609
|
-
0x02: "per_vent_speed", # set fan as per current fan mode/speed?
|
|
1610
|
-
}.get(
|
|
1611
|
-
int(payload[2:4], 0x10) & 0x07
|
|
1612
|
-
) # 0b0000-0111
|
|
1613
|
-
|
|
1614
|
-
fallback_speed = { # after timer expiry
|
|
1615
|
-
0x08: "fan_off", # # set fan off?
|
|
1616
|
-
0x10: "per_request", # # set fan as per payload[6:10], or payload[10:]?
|
|
1617
|
-
0x18: "per_vent_speed", # set fan as per current fan mode/speed?
|
|
1618
|
-
}.get(
|
|
1619
|
-
int(payload[2:4], 0x10) & 0x38
|
|
1620
|
-
) # 0b0011-1000
|
|
1621
|
-
|
|
1622
|
-
units = {
|
|
1623
|
-
0x00: "minutes",
|
|
1624
|
-
0x40: "hours",
|
|
1625
|
-
0x80: "index", # TODO: days, day-of-week, day-of-month?
|
|
1626
|
-
}.get(
|
|
1627
|
-
int(payload[2:4], 0x10) & 0xC0
|
|
1628
|
-
) # 0b1100-0000
|
|
1629
|
-
|
|
1630
|
-
duration = int(payload[4:6], 16) * 60 if units == "hours" else int(payload[4:6], 16)
|
|
1631
|
-
|
|
1632
|
-
if msg.len >= 3:
|
|
1633
|
-
result = {
|
|
1634
|
-
"minutes" if units != "index" else "index": duration,
|
|
1635
|
-
"flags": flag8_from_hex(payload[2:4]),
|
|
1636
|
-
"_new_speed_mode": new_speed,
|
|
1637
|
-
"_fallback_speed_mode": fallback_speed,
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
if msg.len >= 5 and payload[6:10] != "0000": # new speed?
|
|
1641
|
-
result["rate"] = parser_22f1(f"00{payload[6:10]}", msg).get("rate")
|
|
1642
|
-
|
|
1643
|
-
if msg.len >= 7: # fallback speed?
|
|
1644
|
-
result.update({f"_{SZ_UNKNOWN}_5": payload[10:]})
|
|
1645
|
-
|
|
1646
|
-
return result
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
@parser_decorator # WIP: unknown, HVAC
|
|
1650
|
-
def parser_22f4(payload, msg) -> dict:
|
|
1651
|
-
# RP --- 32:155617 18:005904 --:------ 22F4 013 00-60E6-00000000000000-200000
|
|
1652
|
-
# RP --- 32:153258 18:005904 --:------ 22F4 013 00-60DD-00000000000000-200000
|
|
1653
|
-
# RP --- 32:155617 18:005904 --:------ 22F4 013 00-40B0-00000000000000-200000
|
|
1654
|
-
|
|
1655
|
-
assert payload[:2] == "00"
|
|
1656
|
-
assert payload[6:] == "00000000000000200000"
|
|
1657
|
-
|
|
1658
|
-
return {
|
|
1659
|
-
"value_02": payload[2:4],
|
|
1660
|
-
"value_04": payload[4:6],
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
@parser_decorator # bypass_mode, HVAC
|
|
1665
|
-
def parser_22f7(payload, msg) -> dict:
|
|
1666
|
-
# RQ --- 37:171871 32:155617 --:------ 22F7 001 00
|
|
1667
|
-
# RP --- 32:155617 37:171871 --:------ 22F7 003 00FF00 # alse: 000000, 00C8C8
|
|
1668
|
-
|
|
1669
|
-
# .W --- 37:171871 32:155617 --:------ 22F7 003 0000EF # bypass off
|
|
1670
|
-
# .I --- 32:155617 37:171871 --:------ 22F7 003 000000
|
|
1671
|
-
# .W --- 37:171871 32:155617 --:------ 22F7 003 00C8EF # bypass on
|
|
1672
|
-
# .I --- 32:155617 37:171871 --:------ 22F7 003 00C800
|
|
1673
|
-
# .W --- 37:171871 32:155617 --:------ 22F7 003 00FFEF # bypass auto
|
|
1674
|
-
# .I --- 32:155617 37:171871 --:------ 22F7 003 00FFC8
|
|
1675
|
-
|
|
1676
|
-
result = {
|
|
1677
|
-
"bypass_mode": {"00": "off", "C8": "on", "FF": "auto"}.get(payload[2:4]),
|
|
1678
|
-
}
|
|
1679
|
-
if msg.verb != W_ or payload[4:] not in ("", "EF"):
|
|
1680
|
-
result["bypass_state"] = {"00": "off", "C8": "on"}.get(payload[4:])
|
|
1681
|
-
|
|
1682
|
-
return result
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
@parser_decorator # setpoint (of device/zones)
|
|
1686
|
-
def parser_2309(payload, msg) -> dict | list:
|
|
1687
|
-
|
|
1688
|
-
if msg._has_array:
|
|
1689
|
-
return [
|
|
1690
|
-
{
|
|
1691
|
-
SZ_ZONE_IDX: payload[i : i + 2],
|
|
1692
|
-
SZ_SETPOINT: temp_from_hex(payload[i + 2 : i + 6]),
|
|
1693
|
-
}
|
|
1694
|
-
for i in range(0, len(payload), 6)
|
|
1695
|
-
]
|
|
1696
|
-
|
|
1697
|
-
# RQ --- 22:131874 01:063844 --:------ 2309 003 020708
|
|
1698
|
-
if msg.verb == RQ and msg.len == 1: # some RQs have a payload (why?)
|
|
1699
|
-
return {}
|
|
1700
|
-
|
|
1701
|
-
return {SZ_SETPOINT: temp_from_hex(payload[2:])}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
@parser_decorator # zone_mode # TODO: messy
|
|
1705
|
-
def parser_2349(payload, msg) -> dict:
|
|
1706
|
-
# RQ --- 34:225071 30:258557 --:------ 2349 001 00
|
|
1707
|
-
# RP --- 30:258557 34:225071 --:------ 2349 013 007FFF00FFFFFFFFFFFFFFFFFF
|
|
1708
|
-
# RP --- 30:253184 34:010943 --:------ 2349 013 00064000FFFFFF00110E0507E5
|
|
1709
|
-
# .I --- 10:067219 --:------ 10:067219 2349 004 00000001
|
|
1710
|
-
|
|
1711
|
-
if msg.verb == RQ and msg.len <= 2: # some RQs have a payload (why?)
|
|
1712
|
-
return {}
|
|
1713
|
-
|
|
1714
|
-
assert msg.len in (7, 13), f"expected len 7,13, got {msg.len}"
|
|
1715
|
-
|
|
1716
|
-
assert payload[6:8] in ZON_MODE_MAP, f"{SZ_UNKNOWN} zone_mode: {payload[6:8]}"
|
|
1717
|
-
result = {
|
|
1718
|
-
SZ_MODE: ZON_MODE_MAP.get(payload[6:8]),
|
|
1719
|
-
SZ_SETPOINT: temp_from_hex(payload[2:6]),
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
if msg.len >= 7: # has a dtm if mode == "04"
|
|
1723
|
-
if payload[8:14] == "FF" * 3: # 03/FFFFFF OK if W?
|
|
1724
|
-
assert payload[6:8] != ZON_MODE_MAP.COUNTDOWN, f"{payload[6:8]} (0x00)"
|
|
1725
|
-
else:
|
|
1726
|
-
assert payload[6:8] == ZON_MODE_MAP.COUNTDOWN, f"{payload[6:8]} (0x01)"
|
|
1727
|
-
result[SZ_DURATION] = int(payload[8:14], 16)
|
|
1728
|
-
|
|
1729
|
-
if msg.len >= 13:
|
|
1730
|
-
if payload[14:] == "FF" * 6:
|
|
1731
|
-
assert payload[6:8] in (
|
|
1732
|
-
ZON_MODE_MAP.FOLLOW,
|
|
1733
|
-
ZON_MODE_MAP.PERMANENT,
|
|
1734
|
-
), f"{payload[6:8]} (0x02)"
|
|
1735
|
-
result[SZ_UNTIL] = None # TODO: remove?
|
|
1736
|
-
else:
|
|
1737
|
-
assert payload[6:8] != ZON_MODE_MAP.PERMANENT, f"{payload[6:8]} (0x03)"
|
|
1738
|
-
result[SZ_UNTIL] = dtm_from_hex(payload[14:26])
|
|
1739
|
-
|
|
1740
|
-
return result
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
@parser_decorator # unknown_2389, from 03:
|
|
1744
|
-
def parser_2389(payload, msg) -> dict:
|
|
1745
|
-
|
|
1746
|
-
return {
|
|
1747
|
-
f"_{SZ_UNKNOWN}": temp_from_hex(payload[2:6]),
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
@parser_decorator # unknown_2400, from OTB, FAN
|
|
1752
|
-
def parser_2400(payload, msg) -> dict:
|
|
1753
|
-
# RP --- 32:155617 18:005904 --:------ 2400 045 00001111-1010929292921110101020110010000080100010100000009191111191910011119191111111111100 # Orcon FAN
|
|
1754
|
-
# RP --- 10:048122 18:006402 --:------ 2400 004 0000000F
|
|
1755
|
-
# assert payload == "0000000F", _INFORM_DEV_MSG
|
|
1756
|
-
|
|
1757
|
-
return {
|
|
1758
|
-
SZ_PAYLOAD: payload,
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
@parser_decorator # unknown_2401, from OTB
|
|
1763
|
-
def parser_2401(payload, msg) -> dict:
|
|
1764
|
-
|
|
1765
|
-
try:
|
|
1766
|
-
assert payload[2:4] == "00", f"byte 1: {payload[2:4]}"
|
|
1767
|
-
assert (
|
|
1768
|
-
int(payload[4:6], 16) & 0b11110000 == 0
|
|
1769
|
-
), f"byte 2: {flag8_from_hex(payload[4:6])}"
|
|
1770
|
-
assert int(payload[6:], 0x10) <= 200, f"byte 3: {payload[6:]}"
|
|
1771
|
-
except AssertionError as exc:
|
|
1772
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({exc})")
|
|
1773
|
-
|
|
1774
|
-
return {
|
|
1775
|
-
SZ_PAYLOAD: payload,
|
|
1776
|
-
"_value_2": int(payload[4:6], 0x10),
|
|
1777
|
-
"_flags_2": flag8_from_hex(payload[4:6]),
|
|
1778
|
-
"_percent_3": percent_from_hex(payload[6:]),
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
@parser_decorator # unknown_2410, from OTB, FAN
|
|
1783
|
-
def parser_2410(payload, msg) -> dict:
|
|
1784
|
-
# RP --- 10:048122 18:006402 --:------ 2410 020 00-00000000-00000000-00000001-00000001-00000C # OTB
|
|
1785
|
-
# RP --- 32:155617 18:005904 --:------ 2410 020 00-00003EE8-00000000-FFFFFFFF-00000000-1002A6 # Orcon Fan
|
|
1786
|
-
|
|
1787
|
-
def unstuff(seqx: str) -> tuple:
|
|
1788
|
-
val = int(seqx, 16)
|
|
1789
|
-
# if val & 0x40:
|
|
1790
|
-
# raise TypeError
|
|
1791
|
-
signed = bool(val & 0x80)
|
|
1792
|
-
length = (val >> 3 & 0x07) or 1
|
|
1793
|
-
d_type = {0b000: "a", 0b001: "b", 0b010: "c", 0b100: "d"}.get(
|
|
1794
|
-
val & 0x07, val & 0x07
|
|
1795
|
-
)
|
|
1796
|
-
return signed, length, d_type
|
|
1797
|
-
|
|
1798
|
-
assert payload[:6] == "00" * 3, _INFORM_DEV_MSG
|
|
1799
|
-
assert payload[10:18] == "00" * 4, _INFORM_DEV_MSG
|
|
1800
|
-
assert payload[18:26] in ("00000001", "FFFFFFFF"), _INFORM_DEV_MSG
|
|
1801
|
-
assert payload[26:34] in ("00000001", "00000000"), _INFORM_DEV_MSG
|
|
1802
|
-
|
|
1803
|
-
return {
|
|
1804
|
-
"tail": payload[34:],
|
|
1805
|
-
"xxx_34": unstuff(payload[34:36]),
|
|
1806
|
-
"xxx_36": unstuff(payload[36:38]),
|
|
1807
|
-
"xxx_38": unstuff(payload[38:]),
|
|
1808
|
-
"cur_value": payload[2:10],
|
|
1809
|
-
"min_value": payload[10:18],
|
|
1810
|
-
"max_value": payload[18:26],
|
|
1811
|
-
"oth_value": payload[26:34],
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
@parser_decorator # fan_params, HVAC
|
|
1816
|
-
def parser_2411(payload, msg) -> dict:
|
|
1817
|
-
# There is a relationship between 0001 and 2411
|
|
1818
|
-
# RQ --- 37:171871 32:155617 --:------ 0001 005 0020000A04
|
|
1819
|
-
# RP --- 32:155617 37:171871 --:------ 0001 008 0020000A004E0B00 # 0A -> 2411|4E
|
|
1820
|
-
# RQ --- 37:171871 32:155617 --:------ 2411 003 00004E # 11th menu option (i.e. 0x0A)
|
|
1821
|
-
# RP --- 32:155617 37:171871 --:------ 2411 023 00004E460000000001000000000000000100000001A600
|
|
1822
|
-
|
|
1823
|
-
def counter(x) -> int:
|
|
1824
|
-
return int(x, 16)
|
|
1825
|
-
|
|
1826
|
-
def centile(x) -> float:
|
|
1827
|
-
return int(x, 16) / 10
|
|
1828
|
-
|
|
1829
|
-
_2411_DATA_TYPES = {
|
|
1830
|
-
"00": (2, counter), # 4E (0-1), 54 (15-60)
|
|
1831
|
-
"01": (2, centile), # 52 (0.0-25.0) (%)
|
|
1832
|
-
"0F": (2, percent_from_hex), # xx (0.0-1.0) (%)
|
|
1833
|
-
"10": (4, counter), # 31 (0-1800) (days)
|
|
1834
|
-
"92": (4, temp_from_hex), # 75 (0-30) (C)
|
|
1835
|
-
} # TODO: _2411_TYPES.get(payload[8:10], (8, no_op))
|
|
1836
|
-
|
|
1837
|
-
assert (
|
|
1838
|
-
payload[4:6] in _2411_TABLE
|
|
1839
|
-
), f"param {payload[4:6]} is unknown" # _INFORM_DEV_MSG
|
|
1840
|
-
description = _2411_TABLE.get(payload[4:6], "Unknown")
|
|
1841
|
-
|
|
1842
|
-
result = {
|
|
1843
|
-
"parameter": payload[4:6],
|
|
1844
|
-
"description": description,
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
if msg.verb == RQ:
|
|
1848
|
-
return result
|
|
1849
|
-
|
|
1850
|
-
assert (
|
|
1851
|
-
payload[8:10] in _2411_DATA_TYPES
|
|
1852
|
-
), f"param {payload[4:6]} has unknown data_type: {payload[8:10]}" # _INFORM_DEV_MSG
|
|
1853
|
-
length, parser = _2411_DATA_TYPES.get(payload[8:10], (8, lambda x: x))
|
|
1854
|
-
|
|
1855
|
-
result |= {
|
|
1856
|
-
"value": parser(payload[10:18][-length:]),
|
|
1857
|
-
f"_{SZ_VALUE}_06": payload[6:10],
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
if msg.len == 9:
|
|
1861
|
-
return result
|
|
1862
|
-
|
|
1863
|
-
return result | {
|
|
1864
|
-
"min_value": parser(payload[18:26][-length:]),
|
|
1865
|
-
"max_value": parser(payload[26:34][-length:]),
|
|
1866
|
-
"precision": parser(payload[34:42][-length:]),
|
|
1867
|
-
f"_{SZ_VALUE}_42": payload[42:],
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
@parser_decorator # unknown_2420, from OTB
|
|
1872
|
-
def parser_2420(payload, msg) -> dict:
|
|
1873
|
-
|
|
1874
|
-
assert payload == "00000010" + "00" * 34, _INFORM_DEV_MSG
|
|
1875
|
-
|
|
1876
|
-
return {
|
|
1877
|
-
SZ_PAYLOAD: payload,
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
@parser_decorator # _state (of cooling?), from BDR91T, hometronics CTL
|
|
1882
|
-
def parser_2d49(payload, msg) -> dict:
|
|
1883
|
-
|
|
1884
|
-
assert payload[2:] in ("0000", "00FF", "C800", "C8FF"), _INFORM_DEV_MSG
|
|
1885
|
-
|
|
1886
|
-
return {
|
|
1887
|
-
"state": bool_from_hex(payload[2:4]),
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
@parser_decorator # system_mode
|
|
1892
|
-
def parser_2e04(payload, msg) -> dict:
|
|
1893
|
-
# if msg.verb == W_:
|
|
1894
|
-
|
|
1895
|
-
# .I --— 01:020766 --:------ 01:020766 2E04 016 FFFFFFFFFFFFFF0007FFFFFFFFFFFF04 # Manual # noqa: E501
|
|
1896
|
-
# .I --— 01:020766 --:------ 01:020766 2E04 016 FFFFFFFFFFFFFF0000FFFFFFFFFFFF04 # Automatic/times # noqa: E501
|
|
1897
|
-
|
|
1898
|
-
if msg.len == 8: # evohome
|
|
1899
|
-
assert payload[:2] in SYS_MODE_MAP, f"Unknown system mode: {payload[:2]}"
|
|
1900
|
-
|
|
1901
|
-
elif msg.len == 16: # hometronics, lifestyle ID:
|
|
1902
|
-
assert 0 <= int(payload[:2], 16) <= 15 or payload[:2] == FF, payload[:2]
|
|
1903
|
-
assert payload[16:18] in (SYS_MODE_MAP.AUTO, SYS_MODE_MAP.CUSTOM), payload[
|
|
1904
|
-
16:18
|
|
1905
|
-
]
|
|
1906
|
-
assert payload[30:32] == SYS_MODE_MAP.DAY_OFF, payload[30:32]
|
|
1907
|
-
# assert False
|
|
1908
|
-
|
|
1909
|
-
else:
|
|
1910
|
-
# msg.len in (8, 16) # evohome 8, hometronics 16
|
|
1911
|
-
assert False, f"Packet length is {msg.len} (expecting 8, 16)"
|
|
1912
|
-
|
|
1913
|
-
result = {SZ_SYSTEM_MODE: SYS_MODE_MAP[payload[:2]]}
|
|
1914
|
-
if payload[:2] not in (
|
|
1915
|
-
SYS_MODE_MAP.AUTO,
|
|
1916
|
-
SYS_MODE_MAP.HEAT_OFF,
|
|
1917
|
-
SYS_MODE_MAP.AUTO_WITH_RESET,
|
|
1918
|
-
):
|
|
1919
|
-
result.update(
|
|
1920
|
-
{SZ_UNTIL: dtm_from_hex(payload[2:14]) if payload[14:16] != "00" else None}
|
|
1921
|
-
)
|
|
1922
|
-
return result # TODO: double-check the final "00"
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
@parser_decorator # presence_detect, HVAC sensor
|
|
1926
|
-
def parser_2e10(payload, msg) -> dict:
|
|
1927
|
-
|
|
1928
|
-
assert payload in ("0001", "000100"), _INFORM_DEV_MSG
|
|
1929
|
-
|
|
1930
|
-
return {
|
|
1931
|
-
"presence_detected": bool(payload[2:4]),
|
|
1932
|
-
f"_{SZ_UNKNOWN}_4": payload[4:],
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
@parser_decorator # current temperature (of device, zone/s)
|
|
1937
|
-
def parser_30c9(payload, msg) -> dict:
|
|
1938
|
-
|
|
1939
|
-
if msg._has_array:
|
|
1940
|
-
return [
|
|
1941
|
-
{
|
|
1942
|
-
SZ_ZONE_IDX: payload[i : i + 2],
|
|
1943
|
-
SZ_TEMPERATURE: temp_from_hex(payload[i + 2 : i + 6]),
|
|
1944
|
-
}
|
|
1945
|
-
for i in range(0, len(payload), 6)
|
|
1946
|
-
]
|
|
1947
|
-
|
|
1948
|
-
return {SZ_TEMPERATURE: temp_from_hex(payload[2:])}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
@parser_decorator # unknown_3110, HVAC
|
|
1952
|
-
def parser_3110(payload, msg) -> dict:
|
|
1953
|
-
# .I --- 02:250708 --:------ 02:250708 3110 004 0000C820
|
|
1954
|
-
# .I --- 21:042656 --:------ 21:042656 3110 004 00000020
|
|
1955
|
-
|
|
1956
|
-
try:
|
|
1957
|
-
assert payload[2:4] == "00", f"byte 1: {payload[2:4]}"
|
|
1958
|
-
assert int(payload[4:6], 16) <= 200, f"byte 2: {payload[4:6]}"
|
|
1959
|
-
assert payload[6:] in ("00", "10", "20"), f"byte 3: {payload[6:]}"
|
|
1960
|
-
except AssertionError as exc:
|
|
1961
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({exc})")
|
|
1962
|
-
|
|
1963
|
-
return {
|
|
1964
|
-
f"_{SZ_UNKNOWN}_1": payload[2:4],
|
|
1965
|
-
"_percent_2": percent_from_hex(payload[4:6]),
|
|
1966
|
-
"_value_3": payload[6:],
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
@parser_decorator # unknown_3120, from STA, FAN
|
|
1971
|
-
def parser_3120(payload, msg) -> dict:
|
|
1972
|
-
# .I --- 34:136285 --:------ 34:136285 3120 007 0070B0000000FF # every ~3:45:00!
|
|
1973
|
-
# RP --- 20:008749 18:142609 --:------ 3120 007 0070B000009CFF
|
|
1974
|
-
# .I --- 37:258565 --:------ 37:258565 3120 007 0080B0010003FF
|
|
1975
|
-
|
|
1976
|
-
try:
|
|
1977
|
-
assert payload[:2] == "00", f"byte 0: {payload[:2]}"
|
|
1978
|
-
assert payload[2:4] in ("00", "70", "80"), f"byte 1: {payload[2:4]}"
|
|
1979
|
-
assert payload[4:6] == "B0", f"byte 2: {payload[4:6]}"
|
|
1980
|
-
assert payload[6:8] in ("00", "01"), f"byte 3: {payload[6:8]}"
|
|
1981
|
-
assert payload[8:10] == "00", f"byte 4: {payload[8:10]}"
|
|
1982
|
-
assert payload[10:12] in ("00", "03", "0A", "9C"), f"byte 5: {payload[10:12]}"
|
|
1983
|
-
assert payload[12:] == "FF", f"byte 6: {payload[12:]}"
|
|
1984
|
-
except AssertionError as exc:
|
|
1985
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({exc})")
|
|
1986
|
-
|
|
1987
|
-
return {
|
|
1988
|
-
f"{SZ_UNKNOWN}_0": payload[2:10],
|
|
1989
|
-
f"{SZ_UNKNOWN}_5": payload[10:12],
|
|
1990
|
-
f"{SZ_UNKNOWN}_2": payload[12:],
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
@parser_decorator # WIP: unknown, HVAC
|
|
1995
|
-
def parser_313e(payload, msg) -> dict:
|
|
1996
|
-
# 11:00:59.412 RP --- 32:153258 18:005904 --:------ 313E 011 00-0000007937-003C80-0000
|
|
1997
|
-
# 11:02:23.961 RP --- 32:153258 18:005904 --:------ 313E 011 00-0000007B14-003C80-0000
|
|
1998
|
-
# 11:03:32.193 RP --- 32:153258 18:005904 --:------ 313E 011 00-0000007C1C-003C80-0000
|
|
1999
|
-
|
|
2000
|
-
assert payload[:2] == "00"
|
|
2001
|
-
assert payload[12:] == "003C800000"
|
|
2002
|
-
|
|
2003
|
-
return {
|
|
2004
|
-
"value_02": payload[2:12],
|
|
2005
|
-
"value_12": payload[12:18],
|
|
2006
|
-
"value_18": payload[18:],
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
@parser_decorator # datetime
|
|
2011
|
-
def parser_313f(payload, msg) -> dict: # TODO: look for TZ
|
|
2012
|
-
# 2020-03-28T03:59:21.315178 045 RP --- 01:158182 04:136513 --:------ 313F 009 00FC3500A41C0307E4 # noqa: E501
|
|
2013
|
-
# 2020-03-29T04:58:30.486343 045 RP --- 01:158182 04:136485 --:------ 313F 009 00FC8400C51D0307E4 # noqa: E501
|
|
2014
|
-
# 2022-09-20T20:50:32.800676 065 RP --- 01:182924 18:068640 --:------ 313F 009 00F9203234140907E6
|
|
2015
|
-
# 2020-05-31T11:37:50.351511 056 I --- --:------ --:------ 12:207082 313F 009 0038021ECB1F0507E4 # noqa: E501
|
|
2016
|
-
|
|
2017
|
-
# https://www.automatedhome.co.uk/vbulletin/showthread.php?5085-My-HGI80-equivalent-Domoticz-setup-without-HGI80&p=36422&viewfull=1#post36422
|
|
2018
|
-
# every day at ~4am TRV/RQ->CTL/RP, approx 5-10secs apart (CTL respond at any time)
|
|
2019
|
-
|
|
2020
|
-
assert msg.src.type != DEV_TYPE_MAP.CTL or payload[2:4] in (
|
|
2021
|
-
"F0",
|
|
2022
|
-
"F9",
|
|
2023
|
-
"FC",
|
|
2024
|
-
), f"{payload[2:4]} unexpected for CTL" # DEX
|
|
2025
|
-
assert (
|
|
2026
|
-
msg.src.type not in (DEV_TYPE_MAP.DTS, DEV_TYPE_MAP.DT2) or payload[2:4] == "38"
|
|
2027
|
-
), f"{payload[2:4]} unexpected for DTS" # DEX
|
|
2028
|
-
# assert (
|
|
2029
|
-
# msg.src.type != DEV_TYPE_MAP.FAN or payload[2:4] == "7C"
|
|
2030
|
-
# ), f"{payload[2:4]} unexpected for FAN" # DEX
|
|
2031
|
-
assert (
|
|
2032
|
-
msg.src.type != DEV_TYPE_MAP.RFG or payload[2:4] == "60"
|
|
2033
|
-
), "{payload[2:4]} unexpected for RFG" # DEX
|
|
2034
|
-
|
|
2035
|
-
return {
|
|
2036
|
-
SZ_DATETIME: dtm_from_hex(payload[4:18]),
|
|
2037
|
-
SZ_IS_DST: True if bool(int(payload[4:6], 16) & 0x80) else None,
|
|
2038
|
-
f"_{SZ_UNKNOWN}_0": payload[2:4],
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
@parser_decorator # heat_demand (of device, FC domain) - valve status (%open)
|
|
2043
|
-
def parser_3150(payload, msg) -> dict | list:
|
|
2044
|
-
# event-driven, and periodically; FC domain is maximum of all zones
|
|
2045
|
-
# TODO: all have a valid domain will UFC/CTL respond to an RQ, for FC, for a zone?
|
|
2046
|
-
|
|
2047
|
-
# .I --- 04:136513 --:------ 01:158182 3150 002 01CA < often seen CA, artefact?
|
|
2048
|
-
|
|
2049
|
-
def complex_idx(seqx, msg) -> dict:
|
|
2050
|
-
# assert seqx[:2] == FC or (int(seqx[:2], 16) < MAX_ZONES) # <5, 8 for UFC
|
|
2051
|
-
idx_name = "ufx_idx" if msg.src.type == DEV_TYPE_MAP.UFC else SZ_ZONE_IDX # DEX
|
|
2052
|
-
return {SZ_DOMAIN_ID if seqx[:1] == "F" else idx_name: seqx[:2]}
|
|
2053
|
-
|
|
2054
|
-
if msg._has_array:
|
|
2055
|
-
return [
|
|
2056
|
-
{
|
|
2057
|
-
**complex_idx(payload[i : i + 2], msg),
|
|
2058
|
-
**valve_demand(payload[i + 2 : i + 4]),
|
|
2059
|
-
}
|
|
2060
|
-
for i in range(0, len(payload), 4)
|
|
2061
|
-
]
|
|
2062
|
-
|
|
2063
|
-
return valve_demand(payload[2:]) # TODO: check UFC/FC is == CTL/FC
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
@parser_decorator # fan state (basic), HVAC
|
|
2067
|
-
def parser_31d9(payload, msg) -> dict:
|
|
2068
|
-
# NOTE: I have a suspicion that Itho use 0x00-C8 for %, whilst Nuaire use 0x00-64
|
|
2069
|
-
try:
|
|
2070
|
-
assert (
|
|
2071
|
-
payload[4:6] == "FF" or int(payload[4:6], 16) <= 200
|
|
2072
|
-
), f"byte 2: {payload[4:6]}"
|
|
2073
|
-
except AssertionError as exc:
|
|
2074
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({exc})")
|
|
2075
|
-
|
|
2076
|
-
bitmap = int(payload[2:4], 16)
|
|
2077
|
-
|
|
2078
|
-
# NOTE: 31D9[4:6] is fan_rate (itho?) *or* fan_mode (orcon?)
|
|
2079
|
-
result = {
|
|
2080
|
-
SZ_EXHAUST_FAN_SPEED: percent_from_hex(payload[4:6], high_res=True), # itho
|
|
2081
|
-
SZ_FAN_MODE: payload[4:6], # orcon
|
|
2082
|
-
"passive": bool(bitmap & 0x02),
|
|
2083
|
-
"damper_only": bool(bitmap & 0x04),
|
|
2084
|
-
"filter_dirty": bool(bitmap & 0x20),
|
|
2085
|
-
"frost_cycle": bool(bitmap & 0x40),
|
|
2086
|
-
"has_fault": bool(bitmap & 0x80),
|
|
2087
|
-
"_flags": flag8_from_hex(payload[2:4]),
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
if msg.len == 3: # usu: I -->20: (no seq#)
|
|
2091
|
-
return result
|
|
2092
|
-
|
|
2093
|
-
try:
|
|
2094
|
-
assert payload[6:8] in ("00", "07", "0A", "FE"), f"byte 3: {payload[6:8]}"
|
|
2095
|
-
except AssertionError as exc:
|
|
2096
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({exc})")
|
|
2097
|
-
|
|
2098
|
-
result.update({f"_{SZ_UNKNOWN}_3": payload[6:8]})
|
|
2099
|
-
|
|
2100
|
-
if msg.len == 4: # usu: I -->20: (no seq#)
|
|
2101
|
-
return result
|
|
2102
|
-
|
|
2103
|
-
try:
|
|
2104
|
-
assert payload[8:32] in ("00" * 12, "20" * 12), f"byte 4: {payload[8:32]}"
|
|
2105
|
-
assert payload[32:] in ("00", "04", "08"), f"byte 16: {payload[32:]}"
|
|
2106
|
-
except AssertionError as exc:
|
|
2107
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({exc})")
|
|
2108
|
-
|
|
2109
|
-
return {
|
|
2110
|
-
**result,
|
|
2111
|
-
f"_{SZ_UNKNOWN}_4": payload[8:32],
|
|
2112
|
-
f"{SZ_UNKNOWN}_16": payload[32:],
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
@parser_decorator # ventilation state (extended), HVAC
|
|
2117
|
-
def parser_31da(payload, msg) -> dict:
|
|
2118
|
-
|
|
2119
|
-
try:
|
|
2120
|
-
# assert (
|
|
2121
|
-
# int(payload[2:4], 16) <= 200
|
|
2122
|
-
# or int(payload[2:4], 16) & 0xF0 == 0xF0
|
|
2123
|
-
# or payload[2:4] == "EF"
|
|
2124
|
-
# ), f"[2:4] {payload[2:4]}"
|
|
2125
|
-
assert payload[4:6] in ("00", "40"), payload[4:6]
|
|
2126
|
-
# assert payload[6:10] in ("07D0", "7FFF"), payload[6:10]
|
|
2127
|
-
assert payload[10:12] == "EF" or int(payload[10:12], 16) <= 100, payload[10:12]
|
|
2128
|
-
assert (
|
|
2129
|
-
payload[12:14] == "EF" or int(payload[12:14], 16) <= 100
|
|
2130
|
-
), f"[12:14] {payload[10:12]}"
|
|
2131
|
-
# assert payload[30:34] in ("0002", "F000", "F800", "F808", "7FFF"), payload[30:34]
|
|
2132
|
-
# assert payload[34:36] == "EF", payload[34:36]
|
|
2133
|
-
assert (
|
|
2134
|
-
payload[36:38] == "EF" or int(payload[36:38], 16) & 0x1F <= 0x19
|
|
2135
|
-
), f"invalid _31DA_FAN_INFO: {payload[36:38]}"
|
|
2136
|
-
assert int(payload[38:40], 16) <= 200 or payload[38:40] in (
|
|
2137
|
-
"EF",
|
|
2138
|
-
"FF",
|
|
2139
|
-
), payload[38:40]
|
|
2140
|
-
# assert payload[40:42] in ("00", "EF", "FF"), payload[40:42]
|
|
2141
|
-
assert payload[46:48] in ("00", "EF"), f"[46:48] {payload[46:48]}"
|
|
2142
|
-
# assert payload[48:50] == "EF", payload[48:50]
|
|
2143
|
-
except AssertionError as exc:
|
|
2144
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({exc})")
|
|
2145
|
-
|
|
2146
|
-
# From an Orcon 15RF Display
|
|
2147
|
-
# 1 Software version
|
|
2148
|
-
# 4 RH value in home (%) SZ_INDOOR_HUMIDITY
|
|
2149
|
-
# 5 RH value supply air (%) SZ_OUTDOOR_HUMIDITY
|
|
2150
|
-
# 6 Exhaust air temperature out (°C) SZ_EXHAUST_TEMPERATURE
|
|
2151
|
-
# 7 Supply air temperature to home (°C) SZ_SUPPLY_TEMPERATURE
|
|
2152
|
-
# 8 Temperature from home (°C) SZ_INDOOR_TEMPERATURE
|
|
2153
|
-
# 9 Temperature outside (°C) SZ_OUTDOOR_TEMPERATURE
|
|
2154
|
-
# 10 Bypass position SZ_BYPASS_POSITION
|
|
2155
|
-
# 11 Exhaust fan speed (%) SZ_EXHAUST_FAN_SPEED
|
|
2156
|
-
# 12 Fan supply speed (%) SZ_SUPPLY_FAN_SPEED
|
|
2157
|
-
# 13 Remaining after run time (humidity scenario) (min.) SZ_REMAINING_TIME
|
|
2158
|
-
# 14 Preheater control (MaxComfort) (%) SZ_PRE_HEAT
|
|
2159
|
-
# 16 Actual supply flow rate (m3/h) SZ_SUPPLY_FLOW (Orcon is m3/h, data is L/s)
|
|
2160
|
-
# 17 Current discharge flow rate (m3/h) SZ_EXHAUST_FLOW
|
|
2161
|
-
|
|
2162
|
-
return {
|
|
2163
|
-
SZ_EXHAUST_FAN_SPEED: percent_from_hex(
|
|
2164
|
-
payload[38:40]
|
|
2165
|
-
), # maybe 31D9[4:6] for some?
|
|
2166
|
-
SZ_FAN_INFO: _31DA_FAN_INFO[int(payload[36:38], 16) & 0x1F], # 22F3-ish
|
|
2167
|
-
SZ_REMAINING_TIME: double_from_hex(payload[42:46]), # mins, 22F3[2:6]
|
|
2168
|
-
#
|
|
2169
|
-
SZ_AIR_QUALITY: percent_from_hex(payload[2:4]), # 12C8[2:4]
|
|
2170
|
-
SZ_AIR_QUALITY_BASE: int(payload[4:6], 16), # 12C8[4:6]
|
|
2171
|
-
SZ_CO2_LEVEL: double_from_hex(payload[6:10]), # ppm, 1298[2:6]
|
|
2172
|
-
SZ_INDOOR_HUMIDITY: percent_from_hex(payload[10:12], high_res=False), # 12A0?
|
|
2173
|
-
SZ_OUTDOOR_HUMIDITY: percent_from_hex(payload[12:14], high_res=False),
|
|
2174
|
-
SZ_EXHAUST_TEMPERATURE: temp_from_hex(payload[14:18]),
|
|
2175
|
-
SZ_SUPPLY_TEMPERATURE: temp_from_hex(payload[18:22]),
|
|
2176
|
-
SZ_INDOOR_TEMPERATURE: temp_from_hex(payload[22:26]),
|
|
2177
|
-
SZ_OUTDOOR_TEMPERATURE: temp_from_hex(payload[26:30]), # 1290?
|
|
2178
|
-
SZ_SPEED_CAP: int(payload[30:34], 16),
|
|
2179
|
-
SZ_BYPASS_POSITION: percent_from_hex(payload[34:36]),
|
|
2180
|
-
SZ_SUPPLY_FAN_SPEED: percent_from_hex(payload[40:42]),
|
|
2181
|
-
SZ_POST_HEAT: percent_from_hex(payload[46:48], high_res=False),
|
|
2182
|
-
SZ_PRE_HEAT: percent_from_hex(payload[48:50], high_res=False),
|
|
2183
|
-
SZ_SUPPLY_FLOW: double_from_hex(payload[50:54], factor=100), # L/sec
|
|
2184
|
-
SZ_EXHAUST_FLOW: double_from_hex(payload[54:58], factor=100), # L/sec
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
@parser_decorator # vent_demand, HVAC
|
|
2189
|
-
def parser_31e0(payload, msg) -> dict:
|
|
2190
|
-
"""Notes are.
|
|
2191
|
-
|
|
2192
|
-
van means “of”.
|
|
2193
|
-
- 0 = min. van min. potm would be:
|
|
2194
|
-
- 0 = minimum of minimum potentiometer
|
|
2195
|
-
|
|
2196
|
-
See: https://www.industrialcontrolsonline.com/honeywell-t991a
|
|
2197
|
-
- modulates air temperatures in ducts
|
|
2198
|
-
|
|
2199
|
-
case 0x31E0: ' 12768:
|
|
2200
|
-
{
|
|
2201
|
-
string str4;
|
|
2202
|
-
unchecked
|
|
2203
|
-
{
|
|
2204
|
-
result.Fan = Conversions.ToString((double)(int)data[checked(start + 1)] / 2.0);
|
|
2205
|
-
str4 = "";
|
|
2206
|
-
}
|
|
2207
|
-
str4 = (data[start + 2] & 0xF) switch
|
|
2208
|
-
{
|
|
2209
|
-
0 => str4 + "0 = min. potm. ",
|
|
2210
|
-
1 => str4 + "0 = min. van min. potm ",
|
|
2211
|
-
2 => str4 + "0 = min. fan ",
|
|
2212
|
-
_ => "",
|
|
2213
|
-
};
|
|
2214
|
-
switch (data[start + 2] & 0xF0)
|
|
2215
|
-
{
|
|
2216
|
-
case 16:
|
|
2217
|
-
str4 += "100 = max. potm";
|
|
2218
|
-
break;
|
|
2219
|
-
case 32:
|
|
2220
|
-
str4 += "100 = max. van max. potm ";
|
|
2221
|
-
break;
|
|
2222
|
-
case 48:
|
|
2223
|
-
str4 += "100 = max. fan ";
|
|
2224
|
-
break;
|
|
2225
|
-
}
|
|
2226
|
-
result.Data = str4;
|
|
2227
|
-
break;
|
|
2228
|
-
}
|
|
2229
|
-
"""
|
|
2230
|
-
|
|
2231
|
-
# .I --- 37:005302 32:132403 --:------ 31E0 008 00-0000-00 01-0064-00 # RF15 CO2 to Orcon HRC400 series SmartComfort Valve
|
|
2232
|
-
|
|
2233
|
-
# .I --- 29:146052 32:023459 --:------ 31E0 003 00-0000
|
|
2234
|
-
# .I --- 29:146052 32:023459 --:------ 31E0 003 00-00C8
|
|
2235
|
-
|
|
2236
|
-
# .I --- 32:168240 30:079129 --:------ 31E0 004 00-0000-FF
|
|
2237
|
-
# .I --- 32:168240 30:079129 --:------ 31E0 004 00-0000-FF
|
|
2238
|
-
# .I --- 32:166025 --:------ 30:079129 31E0 004 00-0000-00
|
|
2239
|
-
|
|
2240
|
-
# .I --- 32:168090 30:082155 --:------ 31E0 004 00-00C8-00
|
|
2241
|
-
# .I --- 37:258565 37:261128 --:------ 31E0 004 00-0001-00
|
|
2242
|
-
|
|
2243
|
-
def _parser(seqx) -> dict:
|
|
2244
|
-
assert seqx[6:] in ("", "00", "FF")
|
|
2245
|
-
return {
|
|
2246
|
-
# "hvac_idx": seqx[:2],
|
|
2247
|
-
"flags": seqx[2:4],
|
|
2248
|
-
"vent_demand": percent_from_hex(seqx[4:6]),
|
|
2249
|
-
f"_{SZ_UNKNOWN}_3": payload[6:],
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
if len(payload) > 8:
|
|
2253
|
-
return [_parser(payload[x : x + 8]) for x in range(0, len(payload), 8)]
|
|
2254
|
-
return _parser(payload)
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
@parser_decorator # supplied boiler water (flow) temp
|
|
2258
|
-
def parser_3200(payload, msg) -> dict:
|
|
2259
|
-
return {SZ_TEMPERATURE: temp_from_hex(payload[2:])}
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
@parser_decorator # return (boiler) water temp
|
|
2263
|
-
def parser_3210(payload, msg) -> dict:
|
|
2264
|
-
return {SZ_TEMPERATURE: temp_from_hex(payload[2:])}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
@parser_decorator # opentherm_msg, from OTB
|
|
2268
|
-
def parser_3220(payload, msg) -> dict:
|
|
2269
|
-
|
|
2270
|
-
try:
|
|
2271
|
-
ot_type, ot_id, ot_value, ot_schema = decode_frame(payload[2:10])
|
|
2272
|
-
except AssertionError as exc:
|
|
2273
|
-
raise AssertionError(f"OpenTherm: {exc}") from exc
|
|
2274
|
-
except ValueError as exc:
|
|
2275
|
-
raise InvalidPayloadError(f"OpenTherm: {exc}") from exc
|
|
2276
|
-
|
|
2277
|
-
# NOTE: Unknown-DataId isn't an invalid payload & is useful to train the OTB device
|
|
2278
|
-
if ot_schema is None and ot_type != OtMsgType.UNKNOWN_DATAID:
|
|
2279
|
-
raise InvalidPayloadError(f"OpenTherm: Unknown data-id: {ot_id}")
|
|
2280
|
-
|
|
2281
|
-
result = {
|
|
2282
|
-
MSG_ID: ot_id,
|
|
2283
|
-
MSG_TYPE: ot_type,
|
|
2284
|
-
MSG_NAME: ot_value.pop(MSG_NAME, None),
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
if msg.verb == RQ: # RQs have a context: msg_id (and a payload)
|
|
2288
|
-
assert (
|
|
2289
|
-
ot_type != OtMsgType.READ_DATA
|
|
2290
|
-
or payload[6:10] == "0000" # likely true for RAMSES
|
|
2291
|
-
), f"OpenTherm: Invalid msg-type|data-value: {ot_type}|{payload[6:10]}"
|
|
2292
|
-
|
|
2293
|
-
if ot_type != OtMsgType.READ_DATA:
|
|
2294
|
-
assert ot_type in (
|
|
2295
|
-
OtMsgType.WRITE_DATA,
|
|
2296
|
-
OtMsgType.INVALID_DATA,
|
|
2297
|
-
), f"OpenTherm: Invalid msg-type for RQ: {ot_type}"
|
|
2298
|
-
|
|
2299
|
-
result.update(ot_value) # TODO: find some of these packets to review
|
|
2300
|
-
|
|
2301
|
-
else: # if msg.verb == RP:
|
|
2302
|
-
_LIST = (OtMsgType.DATA_INVALID, OtMsgType.UNKNOWN_DATAID, OtMsgType.RESERVED)
|
|
2303
|
-
assert ot_type not in _LIST or payload[6:10] in (
|
|
2304
|
-
"0000",
|
|
2305
|
-
"FFFF",
|
|
2306
|
-
), f"OpenTherm: Invalid msg-type|data-value: {ot_type}|{payload[6:10]}"
|
|
2307
|
-
|
|
2308
|
-
if ot_type not in _LIST:
|
|
2309
|
-
assert ot_type in (
|
|
2310
|
-
OtMsgType.READ_ACK,
|
|
2311
|
-
OtMsgType.WRITE_ACK,
|
|
2312
|
-
), f"OpenTherm: Invalid msg-type for RP: {ot_type}"
|
|
2313
|
-
|
|
2314
|
-
result.update(ot_value)
|
|
2315
|
-
|
|
2316
|
-
try:
|
|
2317
|
-
assert ot_id != 0 or (
|
|
2318
|
-
[result[SZ_VALUE][i] for i in (2, 3, 4, 5, 6, 7)] == [0] * 6
|
|
2319
|
-
), result[SZ_VALUE]
|
|
2320
|
-
|
|
2321
|
-
assert ot_id != 0 or (
|
|
2322
|
-
[result[SZ_VALUE][8 + i] for i in (0, 4, 5, 6, 7)] == [0] * 5
|
|
2323
|
-
), result[SZ_VALUE]
|
|
2324
|
-
except AssertionError:
|
|
2325
|
-
_LOGGER.warning(
|
|
2326
|
-
f"{msg!r} < {_INFORM_DEV_MSG}, with a description of your system"
|
|
2327
|
-
)
|
|
2328
|
-
|
|
2329
|
-
result[MSG_DESC] = ot_schema.get(EN)
|
|
2330
|
-
return result
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
@parser_decorator # unknown_3221, from OTB, FAN
|
|
2334
|
-
def parser_3221(payload, msg) -> dict:
|
|
2335
|
-
|
|
2336
|
-
# RP --- 10:052644 18:198151 --:------ 3221 002 000F
|
|
2337
|
-
# RP --- 10:048122 18:006402 --:------ 3221 002 0000
|
|
2338
|
-
# RP --- 32:155617 18:005904 --:------ 3221 002 000A
|
|
2339
|
-
|
|
2340
|
-
assert int(payload[2:], 16) <= 0xC8, _INFORM_DEV_MSG
|
|
2341
|
-
|
|
2342
|
-
return {
|
|
2343
|
-
f"_{SZ_PAYLOAD}": payload,
|
|
2344
|
-
SZ_VALUE: int(payload[2:], 16),
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
@parser_decorator # WIP: unknown, HVAC
|
|
2349
|
-
def parser_3222(payload, msg) -> dict:
|
|
2350
|
-
# 06:30:14.322 RP --- 32:155617 18:005904 --:------ 3222 004 00-00-01-00
|
|
2351
|
-
# 00:09:26.263 RP --- 32:155617 18:005904 --:------ 3222 005 00-00-02-0009
|
|
2352
|
-
# 02:42:27.090 RP --- 32:155617 18:005904 --:------ 3222 007 00-06-04- 000F100E
|
|
2353
|
-
# 22:06:45.771 RP --- 32:155617 18:005904 --:------ 3222 011 00-02-08- 0009000F000F100E
|
|
2354
|
-
# 13:30:26.792 RP --- 32:155617 18:005904 --:------ 3222 012 00-01-09- 090009000F000F100E
|
|
2355
|
-
# 06:29:40.767 RP --- 32:155617 18:005904 --:------ 3222 013 00-00-0A-00090009000F000F100E
|
|
2356
|
-
|
|
2357
|
-
assert payload[:2] == "00"
|
|
2358
|
-
|
|
2359
|
-
if msg.len == 3:
|
|
2360
|
-
assert payload[4:] == "00"
|
|
2361
|
-
return {"percentage": percent_from_hex(payload[2:4])}
|
|
2362
|
-
|
|
2363
|
-
return {
|
|
2364
|
-
"start": payload[2:4],
|
|
2365
|
-
"length": payload[4:6],
|
|
2366
|
-
"data": f"{'..' * int(payload[2:4])}{payload[6:]}",
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
@parser_decorator # unknown_3223, from OTB
|
|
2371
|
-
def parser_3223(payload, msg) -> dict:
|
|
2372
|
-
|
|
2373
|
-
assert int(payload[2:], 16) <= 0xC8, _INFORM_DEV_MSG
|
|
2374
|
-
|
|
2375
|
-
return {
|
|
2376
|
-
f"_{SZ_PAYLOAD}": payload,
|
|
2377
|
-
SZ_VALUE: int(payload[2:], 16),
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
@parser_decorator # actuator_sync (aka sync_tpi: TPI cycle sync)
|
|
2382
|
-
def parser_3b00(payload, msg) -> dict:
|
|
2383
|
-
# system timing master: the device that sends I/FCC8 pkt controls the heater relay
|
|
2384
|
-
"""Decode a 3B00 packet (actuator_sync).
|
|
2385
|
-
|
|
2386
|
-
The heat relay regularly broadcasts a 3B00 at the end(?) of every TPI cycle, the
|
|
2387
|
-
frequency of which is determined by the (TPI) cycle rate in 1100.
|
|
2388
|
-
|
|
2389
|
-
The CTL subsequently broadcasts a 3B00 (i.e. at the start of every TPI cycle).
|
|
2390
|
-
|
|
2391
|
-
The OTB does not send these packets, but the CTL sends a regular broadcast anyway
|
|
2392
|
-
for the benefit of any zone actuators (e.g. zone valve zones).
|
|
2393
|
-
"""
|
|
2394
|
-
|
|
2395
|
-
# 053 I --- 13:209679 --:------ 13:209679 3B00 002 00C8
|
|
2396
|
-
# 045 I --- 01:158182 --:------ 01:158182 3B00 002 FCC8
|
|
2397
|
-
# 052 I --- 13:209679 --:------ 13:209679 3B00 002 00C8
|
|
2398
|
-
# 045 I --- 01:158182 --:------ 01:158182 3B00 002 FCC8
|
|
2399
|
-
|
|
2400
|
-
# 063 I --- 01:078710 --:------ 01:078710 3B00 002 FCC8
|
|
2401
|
-
# 064 I --- 01:078710 --:------ 01:078710 3B00 002 FCC8
|
|
2402
|
-
|
|
2403
|
-
def complex_idx(payload, msg) -> dict: # has complex idx
|
|
2404
|
-
if (
|
|
2405
|
-
msg.verb == I_
|
|
2406
|
-
and msg.src.type in (DEV_TYPE_MAP.CTL, DEV_TYPE_MAP.PRG)
|
|
2407
|
-
and msg.src is msg.dst
|
|
2408
|
-
): # DEX
|
|
2409
|
-
assert payload[:2] == FC
|
|
2410
|
-
return {SZ_DOMAIN_ID: FC}
|
|
2411
|
-
assert payload[:2] == "00"
|
|
2412
|
-
return {}
|
|
2413
|
-
|
|
2414
|
-
assert msg.len == 2, msg.len
|
|
2415
|
-
assert payload[:2] == {
|
|
2416
|
-
DEV_TYPE_MAP.CTL: FC,
|
|
2417
|
-
DEV_TYPE_MAP.BDR: "00",
|
|
2418
|
-
DEV_TYPE_MAP.PRG: FC,
|
|
2419
|
-
}.get(
|
|
2420
|
-
msg.src.type, "00"
|
|
2421
|
-
) # DEX
|
|
2422
|
-
assert payload[2:] == "C8", payload[2:] # Could it be a percentage?
|
|
2423
|
-
|
|
2424
|
-
return {
|
|
2425
|
-
**complex_idx(payload[:2], msg),
|
|
2426
|
-
"actuator_sync": bool_from_hex(payload[2:]),
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
@parser_decorator # actuator_state
|
|
2431
|
-
def parser_3ef0(payload, msg) -> dict:
|
|
2432
|
-
|
|
2433
|
-
if msg.src.type == DEV_TYPE_MAP.JIM: # Honeywell Jasper
|
|
2434
|
-
assert msg.len == 20, f"expecting len 20, got: {msg.len}"
|
|
2435
|
-
return {
|
|
2436
|
-
"ordinal": f"0x{payload[2:8]}",
|
|
2437
|
-
"blob": payload[8:],
|
|
2438
|
-
}
|
|
2439
|
-
|
|
2440
|
-
# TODO: These two should be picked up by the regex
|
|
2441
|
-
assert msg.len in (3, 6, 9), f"Invalid payload length: {msg.len}"
|
|
2442
|
-
assert payload[:2] == "00", f"Invalid payload context: {payload[:2]}"
|
|
2443
|
-
|
|
2444
|
-
if msg.len == 3: # I|BDR|003 (the following are the only two payloads ever seen)
|
|
2445
|
-
# .I --- 13:042805 --:------ 13:042805 3EF0 003 0000FF
|
|
2446
|
-
# .I --- 13:023770 --:------ 13:023770 3EF0 003 00C8FF
|
|
2447
|
-
assert payload[2:4] in ("00", "C8"), f"byte 1: {payload[2:4]} (not 00/C8)"
|
|
2448
|
-
assert payload[4:6] == "FF", f"byte 2: {payload[4:6]} (not FF)"
|
|
2449
|
-
mod_level = percent_from_hex(payload[2:4]) # , high_res=True)
|
|
2450
|
-
|
|
2451
|
-
else: # msg.len >= 6: # RP|OTB|006 (to RQ|CTL/HGI/RFG)
|
|
2452
|
-
# RP --- 10:004598 34:003611 --:------ 3EF0 006 0000100000FF
|
|
2453
|
-
# RP --- 10:004598 34:003611 --:------ 3EF0 006 0000110000FF
|
|
2454
|
-
# RP --- 10:138822 01:187666 --:------ 3EF0 006 0064100C00FF
|
|
2455
|
-
# RP --- 10:138822 01:187666 --:------ 3EF0 006 0064100200FF
|
|
2456
|
-
assert payload[4:6] in ("10", "11"), f"byte 2: {payload[4:6]}" # maybe 00 too?
|
|
2457
|
-
mod_level = percent_from_hex(payload[2:4], high_res=False) # 00-64 (or FF)
|
|
2458
|
-
|
|
2459
|
-
result = {
|
|
2460
|
-
"modulation_level": mod_level, # 0008[2:4], 3EF1[10:12]
|
|
2461
|
-
"_flags_2": payload[4:6],
|
|
2462
|
-
}
|
|
2463
|
-
|
|
2464
|
-
if msg.len >= 6: # RP|OTB|006 (to RQ|CTL/HGI/RFG)
|
|
2465
|
-
# RP --- 10:138822 01:187666 --:------ 3EF0 006 000110FA00FF # ?corrupt
|
|
2466
|
-
|
|
2467
|
-
# for OTB (there's no reliable) modulation_level <-> flame_state)
|
|
2468
|
-
|
|
2469
|
-
result.update(
|
|
2470
|
-
{
|
|
2471
|
-
"_flags_3": flag8_from_hex(payload[6:8]),
|
|
2472
|
-
"ch_active": bool(int(payload[6:8], 0x10) & 1 << 1),
|
|
2473
|
-
"dhw_active": bool(int(payload[6:8], 0x10) & 1 << 2),
|
|
2474
|
-
"flame_active": bool(int(payload[6:8], 0x10) & 1 << 3), # flame_on
|
|
2475
|
-
"_unknown_4": payload[8:10], # FF, 00, 01, 0A
|
|
2476
|
-
"_unknown_5": payload[10:12], # FF, 1C, ?others
|
|
2477
|
-
}
|
|
2478
|
-
)
|
|
2479
|
-
|
|
2480
|
-
if msg.len >= 9: # I/RP|OTB|009 (R8820A only?)
|
|
2481
|
-
assert int(payload[12:14], 16) & 0b11111100 == 0, f"byte 6: {payload[12:14]}"
|
|
2482
|
-
assert int(payload[12:14], 16) & 0b00000010 == 2, f"byte 6: {payload[12:14]}"
|
|
2483
|
-
assert 10 <= int(payload[14:16], 16) <= 90, f"byte 7: {payload[14:16]}"
|
|
2484
|
-
assert int(payload[16:18], 16) in (0, 100), f"byte 8: {payload[18:]}"
|
|
2485
|
-
|
|
2486
|
-
result.update(
|
|
2487
|
-
{
|
|
2488
|
-
"_flags_6": flag8_from_hex(payload[12:14]),
|
|
2489
|
-
"ch_enabled": bool(int(payload[12:14], 0x10) & 1 << 0),
|
|
2490
|
-
"ch_setpoint": int(payload[14:16], 0x10),
|
|
2491
|
-
"max_rel_modulation": percent_from_hex(payload[16:18], high_res=False),
|
|
2492
|
-
}
|
|
2493
|
-
)
|
|
2494
|
-
|
|
2495
|
-
try: # Trying to decode flags...
|
|
2496
|
-
# assert payload[4:6] != "11" or (
|
|
2497
|
-
# payload[2:4] == "00"
|
|
2498
|
-
# ), f"bytes 1+2: {payload[2:6]}" # 97% is 00 when 11, but not always
|
|
2499
|
-
|
|
2500
|
-
assert payload[4:6] in ("FF", "10", "11"), f"byte 2: {payload[4:6]}"
|
|
2501
|
-
|
|
2502
|
-
assert "_flags_3" not in result or (
|
|
2503
|
-
payload[6:8] == "FF" or int(payload[6:8], 0x10) & 0b10110000 == 0
|
|
2504
|
-
), f'byte 3: {result["_flags_3"]}'
|
|
2505
|
-
# only 01:10:040239 does 0b01000000
|
|
2506
|
-
|
|
2507
|
-
assert "_unknown_4" not in result or (
|
|
2508
|
-
payload[8:10] in ("FF", "00", "01", "04", "0A")
|
|
2509
|
-
), f"byte 4: {payload[8:10]}"
|
|
2510
|
-
# only 10:040239 does 04
|
|
2511
|
-
|
|
2512
|
-
assert "_unknown_5" not in result or (
|
|
2513
|
-
payload[10:12] in ("00", "1C", "FF")
|
|
2514
|
-
), f"byte 5: {payload[10:12]}"
|
|
2515
|
-
|
|
2516
|
-
assert "_flags_6" not in result or (
|
|
2517
|
-
int(payload[12:14], 0x10) & 0b11111100 == 0
|
|
2518
|
-
), f'byte 6: {result["_flags_6"]}'
|
|
2519
|
-
|
|
2520
|
-
except AssertionError as exc:
|
|
2521
|
-
_LOGGER.warning(
|
|
2522
|
-
f"{msg!r} < {_INFORM_DEV_MSG} ({exc}), with a description of your system"
|
|
2523
|
-
)
|
|
2524
|
-
|
|
2525
|
-
return result
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
@parser_decorator # actuator_cycle
|
|
2529
|
-
def parser_3ef1(payload, msg) -> dict:
|
|
2530
|
-
|
|
2531
|
-
if msg.src.type == DEV_TYPE_MAP.JIM: # Honeywell Jasper, DEX
|
|
2532
|
-
assert msg.len == 18, f"expecting len 18, got: {msg.len}"
|
|
2533
|
-
return {
|
|
2534
|
-
"ordinal": f"0x{payload[2:8]}",
|
|
2535
|
-
"blob": payload[8:],
|
|
2536
|
-
}
|
|
2537
|
-
|
|
2538
|
-
if (
|
|
2539
|
-
msg.src.type == DEV_TYPE_MAP.JST
|
|
2540
|
-
): # and msg.len == 12: # or (12, 20) Japser, DEX
|
|
2541
|
-
assert msg.len == 12, f"expecting len 12, got: {msg.len}"
|
|
2542
|
-
return {
|
|
2543
|
-
"ordinal": f"0x{payload[2:8]}",
|
|
2544
|
-
"blob": payload[8:],
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
|
-
if payload[12:] == "FF": # is BDR
|
|
2548
|
-
# assert (
|
|
2549
|
-
# re.compile(r"^00[0-9A-F]{10}FF").match(payload)
|
|
2550
|
-
# ), "doesn't match: " + r"^00[0-9A-F]{10}FF"
|
|
2551
|
-
assert int(payload[2:6], 16) <= 7200, f"byte 1: {payload[2:6]}"
|
|
2552
|
-
# assert payload[6:10] in ("87B3", "9DFA", "DCE1", "E638", "F8F7") or (
|
|
2553
|
-
# int(payload[6:10], 16) <= 7200
|
|
2554
|
-
# ), f"byte 3: {payload[6:10]}"
|
|
2555
|
-
assert percent_from_hex(payload[10:12]) in (0, 1), f"byte 5: {payload[10:12]}"
|
|
2556
|
-
|
|
2557
|
-
else: # is OTB
|
|
2558
|
-
# assert (
|
|
2559
|
-
# re.compile(r"^00[0-9A-F]{10}10").match(payload)
|
|
2560
|
-
# ), "doesn't match: " + r"^00[0-9A-F]{10}10"
|
|
2561
|
-
assert payload[2:6] == "7FFF", f"byte 1: {payload[2:6]}"
|
|
2562
|
-
assert payload[6:10] == "003C", f"byte 3: {payload[6:10]}" # 60 seconds
|
|
2563
|
-
assert percent_from_hex(payload[10:12]) <= 1, f"byte 5: {payload[10:12]}"
|
|
2564
|
-
|
|
2565
|
-
cycle_countdown = None if payload[2:6] == "7FFF" else int(payload[2:6], 16)
|
|
2566
|
-
|
|
2567
|
-
return {
|
|
2568
|
-
"modulation_level": percent_from_hex(payload[10:12]), # 0008[2:4], 3EF0[2:4]
|
|
2569
|
-
"actuator_countdown": int(payload[6:10], 16),
|
|
2570
|
-
"cycle_countdown": cycle_countdown,
|
|
2571
|
-
f"_{SZ_UNKNOWN}_0": payload[12:],
|
|
2572
|
-
}
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
@parser_decorator # timestamp, HVAC
|
|
2576
|
-
def parser_4401(payload, msg) -> dict:
|
|
2577
|
-
|
|
2578
|
-
if msg.verb == RP:
|
|
2579
|
-
return {}
|
|
2580
|
-
|
|
2581
|
-
# assert payload[:4] == "1000", _INFORM_DEV_MSG
|
|
2582
|
-
# assert payload[24:] == "0000000000000063", _INFORM_DEV_MSG
|
|
2583
|
-
|
|
2584
|
-
return {
|
|
2585
|
-
"epoch_02": f"0x{payload[4:12]}",
|
|
2586
|
-
"epoch_07": f"0x{payload[14:22]}",
|
|
2587
|
-
"xxxxx_13": f"0x{payload[22:24]}",
|
|
2588
|
-
"epoch_13": f"0x{payload[26:34]}",
|
|
2589
|
-
} # epoch are in seconds
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
@parser_decorator # hvac_4e01
|
|
2593
|
-
def parser_4e01(payload, msg) -> dict:
|
|
2594
|
-
return {f"val_{x}": temp_from_hex(payload[x : x + 4]) for x in range(2, 34, 4)}
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
@parser_decorator # hvac_4e02
|
|
2598
|
-
def parser_4e02(payload, msg) -> dict:
|
|
2599
|
-
|
|
2600
|
-
return (
|
|
2601
|
-
{f"val_{x}": temp_from_hex(payload[x : x + 4]) for x in range(2, 34, 4)}
|
|
2602
|
-
| {"val_34": payload[34:36]}
|
|
2603
|
-
| {f"val_{x}": temp_from_hex(payload[x : x + 4]) for x in range(36, 68, 4)}
|
|
2604
|
-
)
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
# @parser_decorator # faked puzzle pkt shouldn't be decorated
|
|
2608
|
-
def parser_7fff(payload, msg) -> dict:
|
|
2609
|
-
|
|
2610
|
-
if payload[:2] != "00":
|
|
2611
|
-
_LOGGER.debug("Invalid/deprecated Puzzle packet")
|
|
2612
|
-
return {
|
|
2613
|
-
"msg_type": payload[:2],
|
|
2614
|
-
SZ_PAYLOAD: str_from_hex(payload[2:]),
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
|
-
if payload[2:4] not in LOOKUP_PUZZ:
|
|
2618
|
-
_LOGGER.debug("Invalid/deprecated Puzzle packet")
|
|
2619
|
-
return {
|
|
2620
|
-
"msg_type": payload[2:4],
|
|
2621
|
-
"message": str_from_hex(payload[4:]),
|
|
2622
|
-
}
|
|
2623
|
-
|
|
2624
|
-
result: dict[str, None | str] = {}
|
|
2625
|
-
if payload[2:4] != "13":
|
|
2626
|
-
dtm = dt.fromtimestamp(int(payload[4:16], 16) / 1000) # TZ-naive
|
|
2627
|
-
result["datetime"] = dtm.isoformat(timespec="milliseconds")
|
|
2628
|
-
|
|
2629
|
-
msg_type = LOOKUP_PUZZ.get(payload[2:4], SZ_PAYLOAD)
|
|
2630
|
-
|
|
2631
|
-
if payload[2:4] == "11":
|
|
2632
|
-
msg = str_from_hex(payload[16:])
|
|
2633
|
-
result[msg_type] = f"{msg[:4]}|{msg[4:6]}|{msg[6:]}"
|
|
2634
|
-
|
|
2635
|
-
elif payload[2:4] == "13":
|
|
2636
|
-
result[msg_type] = str_from_hex(payload[4:])
|
|
2637
|
-
|
|
2638
|
-
elif payload[2:4] == "7F":
|
|
2639
|
-
result[msg_type] = payload[4:]
|
|
2640
|
-
|
|
2641
|
-
else:
|
|
2642
|
-
result[msg_type] = str_from_hex(payload[16:])
|
|
2643
|
-
|
|
2644
|
-
return {**result, "parser": f"v{VERSION}"}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
@parser_decorator
|
|
2648
|
-
def parser_unknown(payload, msg) -> dict:
|
|
2649
|
-
# TODO: it may be useful to generically search payloads for hex_ids, commands, etc.
|
|
2650
|
-
|
|
2651
|
-
# These are generic parsers
|
|
2652
|
-
if msg.len == 2 and payload[:2] == "00":
|
|
2653
|
-
return {
|
|
2654
|
-
f"_{SZ_PAYLOAD}": payload,
|
|
2655
|
-
f"_{SZ_VALUE}": {"00": False, "C8": True}.get(
|
|
2656
|
-
payload[2:], int(payload[2:], 16)
|
|
2657
|
-
),
|
|
2658
|
-
}
|
|
2659
|
-
|
|
2660
|
-
if msg.len == 3 and payload[:2] == "00":
|
|
2661
|
-
return {
|
|
2662
|
-
f"_{SZ_PAYLOAD}": payload,
|
|
2663
|
-
f"_{SZ_VALUE}": temp_from_hex(payload[2:]),
|
|
2664
|
-
}
|
|
2665
|
-
|
|
2666
|
-
raise NotImplementedError
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
PAYLOAD_PARSERS = {
|
|
2670
|
-
k[7:].upper(): v
|
|
2671
|
-
for k, v in locals().items()
|
|
2672
|
-
if callable(v) and k.startswith("parser_") and len(k) == 11
|
|
2673
|
-
}
|