ramses-rf 0.22.40__py3-none-any.whl → 0.51.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +377 -513
- ramses_rf/helpers.py +57 -19
- ramses_rf/py.typed +0 -0
- ramses_rf/schemas.py +148 -194
- ramses_rf/system/__init__.py +16 -23
- ramses_rf/system/faultlog.py +363 -0
- ramses_rf/system/heat.py +295 -302
- ramses_rf/system/schedule.py +312 -198
- ramses_rf/system/zones.py +318 -238
- ramses_rf/version.py +2 -8
- ramses_rf-0.51.1.dist-info/METADATA +72 -0
- ramses_rf-0.51.1.dist-info/RECORD +55 -0
- {ramses_rf-0.22.40.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.40.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_rf/protocol → ramses_tx}/parsers.py +1266 -1003
- ramses_tx/protocol.py +801 -0
- ramses_tx/protocol_fsm.py +672 -0
- ramses_tx/py.typed +0 -0
- {ramses_rf/protocol → ramses_tx}/ramses.py +262 -185
- {ramses_rf/protocol → ramses_tx}/schemas.py +150 -133
- ramses_tx/transport.py +1471 -0
- ramses_tx/typed_dicts.py +492 -0
- ramses_tx/typing.py +181 -0
- ramses_tx/version.py +4 -0
- ramses_rf/discovery.py +0 -398
- ramses_rf/protocol/__init__.py +0 -59
- ramses_rf/protocol/backports.py +0 -42
- ramses_rf/protocol/command.py +0 -1576
- ramses_rf/protocol/const.py +0 -697
- ramses_rf/protocol/exceptions.py +0 -111
- ramses_rf/protocol/helpers.py +0 -390
- ramses_rf/protocol/opentherm.py +0 -1170
- ramses_rf/protocol/packet.py +0 -235
- ramses_rf/protocol/protocol.py +0 -613
- ramses_rf/protocol/transport.py +0 -1011
- ramses_rf/protocol/version.py +0 -10
- ramses_rf/system/hvac.py +0 -82
- ramses_rf-0.22.40.dist-info/METADATA +0 -64
- ramses_rf-0.22.40.dist-info/RECORD +0 -42
- ramses_rf-0.22.40.dist-info/top_level.txt +0 -1
|
@@ -1,70 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
"""RAMSES RF - payload processors.
|
|
3
|
+
|
|
4
|
+
NOTES: aspirations on a consistent Schema, going forward:
|
|
5
|
+
|
|
6
|
+
:mode/state: | :bool: | :mutex (infinitive. vs -ing): | :flags:
|
|
7
|
+
mode (config.) | enabled | disabled, heat, cool, heat_cool... | ch_enabled, dhw_enabled
|
|
8
|
+
state (action) | active | idle, heating, cooling... | is_heating, is_cooling
|
|
9
|
+
|
|
10
|
+
- prefer: enabled: True over xx_enabled: True (if only ever 1 flag)
|
|
11
|
+
- prefer: active: True over is_heating: True (if only ever 1 flag)
|
|
12
|
+
- avoid: is_enabled, is_active
|
|
13
|
+
"""
|
|
14
|
+
|
|
5
15
|
from __future__ import annotations
|
|
6
16
|
|
|
7
17
|
import logging
|
|
8
18
|
import re
|
|
9
|
-
from
|
|
10
|
-
from datetime import timedelta as td
|
|
11
|
-
from typing import
|
|
19
|
+
from collections.abc import Mapping
|
|
20
|
+
from datetime import datetime as dt, timedelta as td
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
12
22
|
|
|
13
|
-
from .
|
|
23
|
+
from . import exceptions as exc
|
|
24
|
+
from .address import ALL_DEV_ADDR, NON_DEV_ADDR, hex_id_to_dev_id
|
|
14
25
|
from .const import (
|
|
15
|
-
DEV_ROLE,
|
|
16
26
|
DEV_ROLE_MAP,
|
|
17
27
|
DEV_TYPE_MAP,
|
|
18
28
|
FAULT_DEVICE_CLASS,
|
|
19
29
|
FAULT_STATE,
|
|
20
30
|
FAULT_TYPE,
|
|
21
31
|
SYS_MODE_MAP,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
SZ_AIR_QUALITY_BASE,
|
|
32
|
+
SZ_ACCEPT,
|
|
33
|
+
SZ_ACTIVE,
|
|
25
34
|
SZ_BINDINGS,
|
|
26
|
-
|
|
35
|
+
SZ_BYPASS_MODE,
|
|
36
|
+
SZ_BYPASS_STATE,
|
|
27
37
|
SZ_CHANGE_COUNTER,
|
|
28
|
-
|
|
38
|
+
SZ_CONFIRM,
|
|
29
39
|
SZ_DATETIME,
|
|
40
|
+
SZ_DEMAND,
|
|
30
41
|
SZ_DEVICE_CLASS,
|
|
31
42
|
SZ_DEVICE_ID,
|
|
32
43
|
SZ_DEVICE_ROLE,
|
|
33
44
|
SZ_DEVICES,
|
|
45
|
+
SZ_DHW_FLOW_RATE,
|
|
34
46
|
SZ_DOMAIN_ID,
|
|
47
|
+
SZ_DOMAIN_IDX,
|
|
35
48
|
SZ_DURATION,
|
|
36
|
-
SZ_EXHAUST_FAN_SPEED,
|
|
37
|
-
SZ_EXHAUST_FLOW,
|
|
38
|
-
SZ_EXHAUST_TEMPERATURE,
|
|
39
|
-
SZ_FAN_INFO,
|
|
40
49
|
SZ_FAN_MODE,
|
|
50
|
+
SZ_FAN_RATE,
|
|
51
|
+
SZ_FAULT_STATE,
|
|
52
|
+
SZ_FAULT_TYPE,
|
|
41
53
|
SZ_FRAG_LENGTH,
|
|
42
54
|
SZ_FRAG_NUMBER,
|
|
43
55
|
SZ_FRAGMENT,
|
|
44
|
-
SZ_INDOOR_HUMIDITY,
|
|
45
|
-
SZ_INDOOR_TEMPERATURE,
|
|
46
56
|
SZ_IS_DST,
|
|
47
57
|
SZ_LANGUAGE,
|
|
58
|
+
SZ_LOCAL_OVERRIDE,
|
|
59
|
+
SZ_LOG_ENTRY,
|
|
60
|
+
SZ_LOG_IDX,
|
|
61
|
+
SZ_MAX_TEMP,
|
|
62
|
+
SZ_MIN_TEMP,
|
|
48
63
|
SZ_MODE,
|
|
64
|
+
SZ_MULTIROOM_MODE,
|
|
49
65
|
SZ_NAME,
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
SZ_OEM_CODE,
|
|
67
|
+
SZ_OFFER,
|
|
68
|
+
SZ_OPENWINDOW_FUNCTION,
|
|
52
69
|
SZ_PAYLOAD,
|
|
53
|
-
|
|
54
|
-
SZ_PRE_HEAT,
|
|
70
|
+
SZ_PHASE,
|
|
55
71
|
SZ_PRESSURE,
|
|
56
72
|
SZ_RELAY_DEMAND,
|
|
57
|
-
|
|
73
|
+
SZ_REMAINING_DAYS,
|
|
74
|
+
SZ_REMAINING_PERCENT,
|
|
58
75
|
SZ_SETPOINT,
|
|
59
|
-
|
|
60
|
-
SZ_SUPPLY_FAN_SPEED,
|
|
61
|
-
SZ_SUPPLY_FLOW,
|
|
62
|
-
SZ_SUPPLY_TEMPERATURE,
|
|
76
|
+
SZ_SETPOINT_BOUNDS,
|
|
63
77
|
SZ_SYSTEM_MODE,
|
|
64
78
|
SZ_TEMPERATURE,
|
|
79
|
+
SZ_TIMESTAMP,
|
|
65
80
|
SZ_TOTAL_FRAGS,
|
|
66
81
|
SZ_UFH_IDX,
|
|
67
|
-
SZ_UNKNOWN,
|
|
68
82
|
SZ_UNTIL,
|
|
69
83
|
SZ_VALUE,
|
|
70
84
|
SZ_WINDOW_OPEN,
|
|
@@ -74,24 +88,52 @@ from .const import (
|
|
|
74
88
|
SZ_ZONE_TYPE,
|
|
75
89
|
ZON_MODE_MAP,
|
|
76
90
|
ZON_ROLE_MAP,
|
|
77
|
-
|
|
91
|
+
DevRole,
|
|
92
|
+
FaultDeviceClass,
|
|
78
93
|
)
|
|
79
|
-
from .exceptions import InvalidPayloadError
|
|
80
94
|
from .fingerprints import check_signature
|
|
81
95
|
from .helpers import (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
hex_to_bool,
|
|
97
|
+
hex_to_date,
|
|
98
|
+
hex_to_dtm,
|
|
99
|
+
hex_to_dts,
|
|
100
|
+
hex_to_flag8,
|
|
101
|
+
hex_to_percent,
|
|
102
|
+
hex_to_str,
|
|
103
|
+
hex_to_temp,
|
|
104
|
+
parse_air_quality,
|
|
105
|
+
parse_bypass_position,
|
|
106
|
+
parse_capabilities,
|
|
107
|
+
parse_co2_level,
|
|
108
|
+
parse_exhaust_fan_speed,
|
|
109
|
+
parse_exhaust_flow,
|
|
110
|
+
parse_exhaust_temp,
|
|
111
|
+
parse_fan_info,
|
|
112
|
+
parse_fault_log_entry,
|
|
113
|
+
parse_humidity_element,
|
|
114
|
+
parse_indoor_humidity,
|
|
115
|
+
parse_indoor_temp,
|
|
116
|
+
parse_outdoor_humidity,
|
|
117
|
+
parse_outdoor_temp,
|
|
118
|
+
parse_post_heater,
|
|
119
|
+
parse_pre_heater,
|
|
120
|
+
parse_remaining_mins,
|
|
121
|
+
parse_supply_fan_speed,
|
|
122
|
+
parse_supply_flow,
|
|
123
|
+
parse_supply_temp,
|
|
124
|
+
parse_valve_demand,
|
|
125
|
+
)
|
|
126
|
+
from .opentherm import (
|
|
127
|
+
EN,
|
|
128
|
+
SZ_DESCRIPTION,
|
|
129
|
+
SZ_MSG_ID,
|
|
130
|
+
SZ_MSG_NAME,
|
|
131
|
+
SZ_MSG_TYPE,
|
|
132
|
+
OtMsgType,
|
|
133
|
+
decode_frame,
|
|
92
134
|
)
|
|
93
|
-
from .
|
|
94
|
-
from .
|
|
135
|
+
from .ramses import _31D9_FAN_INFO_VASCO, _2411_PARAMS_SCHEMA
|
|
136
|
+
from .typed_dicts import PayDictT
|
|
95
137
|
from .version import VERSION
|
|
96
138
|
|
|
97
139
|
# Kudos & many thanks to:
|
|
@@ -100,14 +142,19 @@ from .version import VERSION
|
|
|
100
142
|
# - ReneKlootwijk: 3EF0
|
|
101
143
|
# - brucemiranda: 3EF0, others
|
|
102
144
|
# - janvken: 10D0, 1470, 1F70, 22B0, 2411, several others
|
|
145
|
+
# - tomkooij: 3110
|
|
146
|
+
# - RemyDeRuysscher: 10E0, 31DA (and related), others
|
|
147
|
+
# - silverailscolo: 12A0, 31DA, others
|
|
103
148
|
|
|
104
149
|
|
|
105
|
-
# skipcq: PY-W2000
|
|
106
150
|
from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
107
151
|
I_,
|
|
108
152
|
RP,
|
|
109
153
|
RQ,
|
|
110
154
|
W_,
|
|
155
|
+
Code,
|
|
156
|
+
)
|
|
157
|
+
from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
111
158
|
F6,
|
|
112
159
|
F8,
|
|
113
160
|
F9,
|
|
@@ -117,37 +164,29 @@ from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
|
117
164
|
FF,
|
|
118
165
|
)
|
|
119
166
|
|
|
120
|
-
|
|
167
|
+
if TYPE_CHECKING:
|
|
168
|
+
from .message import MessageBase as Message # HACK: merge MsgBase into Msg
|
|
121
169
|
|
|
122
|
-
|
|
170
|
+
_2411_TABLE = {k: v["description"] for k, v in _2411_PARAMS_SCHEMA.items()}
|
|
123
171
|
|
|
124
172
|
LOOKUP_PUZZ = {
|
|
125
173
|
"10": "engine", # . # version str, e.g. v0.14.0
|
|
126
174
|
"11": "impersonating", # pkt header, e.g. 30C9| I|03:123001 (15 characters, packed)
|
|
127
175
|
"12": "message", # . # message only, max len is 16 ascii characters
|
|
128
176
|
"13": "message", # . # message only, but without a timestamp, max len 22 chars
|
|
177
|
+
"20": "engine", # . # version str, e.g. v0.50.0, has higher-precision timestamp
|
|
129
178
|
"7F": "null", # . # packet is null / was nullified: payload to be ignored
|
|
130
179
|
} # "00" is reserved
|
|
131
180
|
|
|
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
181
|
|
|
182
|
+
_INFORM_DEV_MSG = "Support the development of ramses_rf by reporting this packet"
|
|
138
183
|
|
|
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
184
|
|
|
146
|
-
|
|
185
|
+
_LOGGER = _PKT_LOGGER = logging.getLogger(__name__)
|
|
147
186
|
|
|
148
187
|
|
|
149
|
-
|
|
150
|
-
def parser_0001(payload, msg) ->
|
|
188
|
+
# rf_unknown
|
|
189
|
+
def parser_0001(payload: str, msg: Message) -> Mapping[str, bool | str | None]:
|
|
151
190
|
# When in test mode, a 12: will send a W ?every 6 seconds:
|
|
152
191
|
# 12:39:56.099 061 W --- 12:010740 --:------ 12:010740 0001 005 0000000501
|
|
153
192
|
# 12:40:02.098 061 W --- 12:010740 --:------ 12:010740 0001 005 0000000501
|
|
@@ -175,8 +214,8 @@ def parser_0001(payload, msg) -> dict:
|
|
|
175
214
|
# 15:13:34.759 063 W --- 01:145038 --:------ 01:145038 0001 005 FF00000505
|
|
176
215
|
|
|
177
216
|
# sent by a CTL
|
|
178
|
-
# 16:49:46.
|
|
179
|
-
# 16:53:34.
|
|
217
|
+
# 16:49:46.125 057 W --- 04:166090 --:------ 01:032820 0001 005 0100000505
|
|
218
|
+
# 16:53:34.635 058 W --- 04:166090 --:------ 01:032820 0001 005 0100000505
|
|
180
219
|
|
|
181
220
|
# loopback (not Tx'd) by a HGI80 whenever its button is pressed
|
|
182
221
|
# 00:22:41.540 --- I --- --:------ --:------ --:------ 0001 005 00FFFF02FF
|
|
@@ -196,6 +235,8 @@ def parser_0001(payload, msg) -> dict:
|
|
|
196
235
|
mode = "heat"
|
|
197
236
|
|
|
198
237
|
if mode == "hvac":
|
|
238
|
+
result: dict[str, bool | str | None]
|
|
239
|
+
|
|
199
240
|
assert payload[:2] == "00", payload[:2]
|
|
200
241
|
# assert payload[2:4] in ("20", "80", "A0"), payload[2:4]
|
|
201
242
|
# assert payload[4:6] == "00", payload[4:6]
|
|
@@ -207,13 +248,8 @@ def parser_0001(payload, msg) -> dict:
|
|
|
207
248
|
if msg.len >= 7:
|
|
208
249
|
result.update({"next_slot_num": payload[12:14]})
|
|
209
250
|
if msg.len >= 8:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
"boolean_14": None
|
|
213
|
-
if payload[14:16] == "FF"
|
|
214
|
-
else bool(int(payload[14:16]))
|
|
215
|
-
}
|
|
216
|
-
)
|
|
251
|
+
_14 = None if payload[14:16] == "FF" else bool(int(payload[14:16]))
|
|
252
|
+
result.update({"boolean_14": _14})
|
|
217
253
|
return result
|
|
218
254
|
|
|
219
255
|
assert payload[2:6] in ("0000", "FFFF"), payload[2:6]
|
|
@@ -224,46 +260,37 @@ def parser_0001(payload, msg) -> dict:
|
|
|
224
260
|
}
|
|
225
261
|
|
|
226
262
|
|
|
227
|
-
|
|
228
|
-
def parser_0002(payload, msg) -> dict:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
# - 03...... for other stuff?
|
|
235
|
-
|
|
236
|
-
if msg.src.type == DEV_TYPE_MAP.HCW: # payload[2:] == DEV_TYPE_MAP.HCW, DEX
|
|
237
|
-
assert payload == "03020105"
|
|
238
|
-
return {f"_{SZ_UNKNOWN}": payload}
|
|
263
|
+
# outdoor_sensor (outdoor_weather / outdoor_temperature)
|
|
264
|
+
def parser_0002(payload: str, msg: Message) -> dict[str, Any]:
|
|
265
|
+
if payload[6:] == "02": # or: msg.src.type == DEV_TYPE_MAP.OUT:
|
|
266
|
+
return {
|
|
267
|
+
SZ_TEMPERATURE: hex_to_temp(payload[2:6]),
|
|
268
|
+
"_unknown": payload[6:],
|
|
269
|
+
}
|
|
239
270
|
|
|
240
|
-
|
|
241
|
-
return {
|
|
242
|
-
SZ_TEMPERATURE: temp_from_hex(payload[2:6]),
|
|
243
|
-
f"_{SZ_UNKNOWN}": payload[6:],
|
|
244
|
-
}
|
|
271
|
+
return {"_payload": payload}
|
|
245
272
|
|
|
246
273
|
|
|
247
|
-
|
|
248
|
-
def parser_0004(payload, msg) ->
|
|
274
|
+
# zone_name
|
|
275
|
+
def parser_0004(payload: str, msg: Message) -> PayDictT._0004:
|
|
249
276
|
# RQ payload is zz00; limited to 12 chars in evohome UI? if "7F"*20: not a zone
|
|
250
277
|
|
|
251
|
-
return {} if payload[4:] == "7F" * 20 else {SZ_NAME:
|
|
278
|
+
return {} if payload[4:] == "7F" * 20 else {SZ_NAME: hex_to_str(payload[4:])}
|
|
252
279
|
|
|
253
280
|
|
|
254
|
-
|
|
255
|
-
def parser_0005(payload, msg) -> dict | list[dict]: # TODO:
|
|
281
|
+
# system_zones (add/del a zone?) # TODO: needs a cleanup
|
|
282
|
+
def parser_0005(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
256
283
|
# .I --- 01:145038 --:------ 01:145038 0005 004 00000100
|
|
257
284
|
# RP --- 02:017205 18:073736 --:------ 0005 004 0009001F
|
|
258
285
|
# .I --- 34:064023 --:------ 34:064023 0005 012 000A0000-000F0000-00100000
|
|
259
286
|
|
|
260
|
-
def _parser(seqx) -> dict:
|
|
287
|
+
def _parser(seqx: str) -> dict:
|
|
261
288
|
if msg.src.type == DEV_TYPE_MAP.UFC: # DEX, or use: seqx[2:4] == ...
|
|
262
|
-
zone_mask =
|
|
289
|
+
zone_mask = hex_to_flag8(seqx[6:8], lsb=True)
|
|
263
290
|
elif msg.len == 3: # ATC928G1000 - 1st gen monochrome model, max 8 zones
|
|
264
|
-
zone_mask =
|
|
291
|
+
zone_mask = hex_to_flag8(seqx[4:6], lsb=True)
|
|
265
292
|
else:
|
|
266
|
-
zone_mask =
|
|
293
|
+
zone_mask = hex_to_flag8(seqx[4:6], lsb=True) + hex_to_flag8(
|
|
267
294
|
seqx[6:8], lsb=True
|
|
268
295
|
)
|
|
269
296
|
zone_class = ZON_ROLE_MAP.get(seqx[2:4], DEV_ROLE_MAP[seqx[2:4]])
|
|
@@ -277,20 +304,20 @@ def parser_0005(payload, msg) -> dict | list[dict]: # TODO: needs a cleanup
|
|
|
277
304
|
return {SZ_ZONE_TYPE: payload[2:4], SZ_ZONE_CLASS: DEV_ROLE_MAP[payload[2:4]]}
|
|
278
305
|
|
|
279
306
|
if msg._has_array:
|
|
280
|
-
assert (
|
|
281
|
-
msg
|
|
282
|
-
)
|
|
307
|
+
assert msg.verb == I_ and msg.src.type == DEV_TYPE_MAP.RND, (
|
|
308
|
+
f"{msg!r} # expecting I/{DEV_TYPE_MAP.RND}:"
|
|
309
|
+
) # DEX
|
|
283
310
|
return [_parser(payload[i : i + 8]) for i in range(0, len(payload), 8)]
|
|
284
311
|
|
|
285
312
|
return _parser(payload)
|
|
286
313
|
|
|
287
314
|
|
|
288
|
-
|
|
289
|
-
def parser_0006(payload, msg) ->
|
|
315
|
+
# schedule_sync (any changes?)
|
|
316
|
+
def parser_0006(payload: str, msg: Message) -> PayDictT._0006:
|
|
290
317
|
"""Return the total number of changes to the schedules, including the DHW schedule.
|
|
291
318
|
|
|
292
319
|
An RQ is sent every ~60s by a RFG100, an increase will prompt it to send a run of
|
|
293
|
-
RQ
|
|
320
|
+
RQ|0404s (it seems to assume only the zones may have changed?).
|
|
294
321
|
"""
|
|
295
322
|
# 16:10:34.288 053 RQ --- 30:071715 01:145038 --:------ 0006 001 00
|
|
296
323
|
# 16:10:34.291 053 RP --- 01:145038 30:071715 --:------ 0006 004 00050008
|
|
@@ -301,13 +328,12 @@ def parser_0006(payload, msg) -> dict:
|
|
|
301
328
|
assert payload[2:4] == "05"
|
|
302
329
|
|
|
303
330
|
return {
|
|
304
|
-
SZ_CHANGE_COUNTER: int(payload[4:], 16),
|
|
305
|
-
"_header": payload[:4],
|
|
331
|
+
SZ_CHANGE_COUNTER: None if payload[4:] == "FFFF" else int(payload[4:], 16),
|
|
306
332
|
}
|
|
307
333
|
|
|
308
334
|
|
|
309
|
-
|
|
310
|
-
def parser_0008(payload, msg) ->
|
|
335
|
+
# relay_demand (domain/zone/device)
|
|
336
|
+
def parser_0008(payload: str, msg: Message) -> PayDictT._0008:
|
|
311
337
|
# https://www.domoticaforum.eu/viewtopic.php?f=7&t=5806&start=105#p73681
|
|
312
338
|
# e.g. Electric Heat Zone
|
|
313
339
|
|
|
@@ -321,16 +347,16 @@ def parser_0008(payload, msg) -> dict:
|
|
|
321
347
|
|
|
322
348
|
if msg.src.type == DEV_TYPE_MAP.JST and msg.len == 13: # Honeywell Japser, DEX
|
|
323
349
|
assert msg.len == 13, "expecting length 13"
|
|
324
|
-
return {
|
|
350
|
+
return { # type: ignore[typeddict-item]
|
|
325
351
|
"ordinal": f"0x{payload[2:8]}",
|
|
326
352
|
"blob": payload[8:],
|
|
327
353
|
}
|
|
328
354
|
|
|
329
|
-
return {SZ_RELAY_DEMAND:
|
|
355
|
+
return {SZ_RELAY_DEMAND: hex_to_percent(payload[2:4])} # 3EF0[2:4], 3EF1[10:12]
|
|
330
356
|
|
|
331
357
|
|
|
332
|
-
|
|
333
|
-
def parser_0009(payload, msg) -> dict | list:
|
|
358
|
+
# relay_failsafe
|
|
359
|
+
def parser_0009(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
334
360
|
"""The relay failsafe mode.
|
|
335
361
|
|
|
336
362
|
The failsafe mode defines the relay behaviour if the RF communication is lost (e.g.
|
|
@@ -338,18 +364,18 @@ def parser_0009(payload, msg) -> dict | list:
|
|
|
338
364
|
False (disabled) - if RF comms are lost, relay will be held in OFF position
|
|
339
365
|
True (enabled) - if RF comms are lost, relay will cycle at 20% ON, 80% OFF
|
|
340
366
|
|
|
341
|
-
This setting may need to be enabled to ensure
|
|
367
|
+
This setting may need to be enabled to ensure frost protect mode.
|
|
342
368
|
"""
|
|
343
369
|
# can get: 003 or 006, e.g.: FC01FF-F901FF or FC00FF-F900FF
|
|
344
370
|
# .I --- 23:100224 --:------ 23:100224 0009 003 0100FF # 2-zone ST9520C
|
|
345
371
|
# .I --- 10:040239 01:223036 --:------ 0009 003 000000
|
|
346
372
|
|
|
347
|
-
def _parser(seqx) -> dict:
|
|
373
|
+
def _parser(seqx: str) -> dict:
|
|
348
374
|
assert seqx[:2] in (F9, FC) or int(seqx[:2], 16) < 16
|
|
349
375
|
return {
|
|
350
376
|
SZ_DOMAIN_ID if seqx[:1] == "F" else SZ_ZONE_IDX: seqx[:2],
|
|
351
377
|
"failsafe_enabled": {"00": False, "01": True}.get(seqx[2:4]),
|
|
352
|
-
|
|
378
|
+
"unknown_0": seqx[4:],
|
|
353
379
|
}
|
|
354
380
|
|
|
355
381
|
if msg._has_array:
|
|
@@ -357,26 +383,23 @@ def parser_0009(payload, msg) -> dict | list:
|
|
|
357
383
|
|
|
358
384
|
return {
|
|
359
385
|
"failsafe_enabled": {"00": False, "01": True}.get(payload[2:4]),
|
|
360
|
-
|
|
386
|
+
"unknown_0": payload[4:],
|
|
361
387
|
}
|
|
362
388
|
|
|
363
389
|
|
|
364
|
-
|
|
365
|
-
def parser_000a(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
# RP --- 01:140959 22:017139 --:------ 000A 006 081001F40DAC
|
|
370
|
-
|
|
371
|
-
def _parser(seqx) -> dict: # null_rp: "007FFF7FFF"
|
|
390
|
+
# zone_params (zone_config)
|
|
391
|
+
def parser_000a(
|
|
392
|
+
payload: str, msg: Message
|
|
393
|
+
) -> PayDictT._000A | list[PayDictT._000A] | PayDictT.EMPTY:
|
|
394
|
+
def _parser(seqx: str) -> PayDictT._000A: # null_rp: "007FFF7FFF"
|
|
372
395
|
bitmap = int(seqx[2:4], 16)
|
|
373
396
|
return {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
397
|
+
SZ_MIN_TEMP: hex_to_temp(seqx[4:8]),
|
|
398
|
+
SZ_MAX_TEMP: hex_to_temp(seqx[8:]),
|
|
399
|
+
SZ_LOCAL_OVERRIDE: not bool(bitmap & 1),
|
|
400
|
+
SZ_OPENWINDOW_FUNCTION: not bool(bitmap & 2),
|
|
401
|
+
SZ_MULTIROOM_MODE: not bool(bitmap & 16),
|
|
402
|
+
"_unknown_bitmap": f"0b{bitmap:08b}", # TODO: try W with this
|
|
380
403
|
} # cannot determine zone_type from this information
|
|
381
404
|
|
|
382
405
|
if msg._has_array: # NOTE: these arrays can span 2 pkts!
|
|
@@ -395,13 +418,13 @@ def parser_000a(payload, msg) -> dict | list:
|
|
|
395
418
|
return _parser(payload)
|
|
396
419
|
|
|
397
420
|
|
|
398
|
-
|
|
399
|
-
def parser_000c(payload, msg) -> dict:
|
|
421
|
+
# zone_devices
|
|
422
|
+
def parser_000c(payload: str, msg: Message) -> dict[str, Any]:
|
|
400
423
|
# .I --- 34:092243 --:------ 34:092243 000C 018 00-0A-7F-FFFFFF 00-0F-7F-FFFFFF 00-10-7F-FFFFFF # noqa: E501
|
|
401
424
|
# RP --- 01:145038 18:013393 --:------ 000C 006 00-00-00-10DAFD
|
|
402
425
|
# RP --- 01:145038 18:013393 --:------ 000C 012 01-00-00-10DAF5 01-00-00-10DAFB
|
|
403
426
|
|
|
404
|
-
def complex_idx(seqx, msg) -> dict: # complex index
|
|
427
|
+
def complex_idx(seqx: str, msg: Message) -> dict: # complex index
|
|
405
428
|
"""domain_id, zone_idx, or ufx_idx|zone_idx."""
|
|
406
429
|
|
|
407
430
|
# TODO: 000C to a UFC should be ufh_ifx, not zone_idx
|
|
@@ -413,9 +436,9 @@ def parser_000c(payload, msg) -> dict:
|
|
|
413
436
|
}
|
|
414
437
|
|
|
415
438
|
if payload[2:4] in (DEV_ROLE_MAP.DHW, DEV_ROLE_MAP.HTG):
|
|
416
|
-
assert (
|
|
417
|
-
|
|
418
|
-
)
|
|
439
|
+
assert int(seqx, 16) < 1 if payload[2:4] == DEV_ROLE_MAP.DHW else 2, (
|
|
440
|
+
f"invalid _idx: '{seqx}' (0x01)"
|
|
441
|
+
)
|
|
419
442
|
return {SZ_DOMAIN_ID: FA if payload[:2] == "00" else F9}
|
|
420
443
|
|
|
421
444
|
if payload[2:4] == DEV_ROLE_MAP.APP:
|
|
@@ -425,15 +448,17 @@ def parser_000c(payload, msg) -> dict:
|
|
|
425
448
|
assert int(seqx, 16) < 16, f"invalid zone_idx: '{seqx}' (0x03)"
|
|
426
449
|
return {SZ_ZONE_IDX: seqx}
|
|
427
450
|
|
|
428
|
-
def _parser(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
451
|
+
def _parser(
|
|
452
|
+
seqx: str,
|
|
453
|
+
) -> dict: # TODO: assumption that all id/idx are same is wrong!
|
|
454
|
+
assert seqx[:2] == payload[:2], (
|
|
455
|
+
f"idx != {payload[:2]} (seqx = {seqx}), short={is_short_000C(payload)}"
|
|
456
|
+
)
|
|
432
457
|
assert int(seqx[:2], 16) < 16
|
|
433
458
|
assert seqx[4:6] == "7F" or seqx[6:] != "F" * 6, f"Bad device_id: {seqx[6:]}"
|
|
434
459
|
return {hex_id_to_dev_id(seqx[6:12]): seqx[4:6]}
|
|
435
460
|
|
|
436
|
-
def is_short_000C(payload) -> bool:
|
|
461
|
+
def is_short_000C(payload: str) -> bool:
|
|
437
462
|
"""Return True if it is a short 000C (element length is 5, not 6)."""
|
|
438
463
|
|
|
439
464
|
if (pkt_len := len(payload)) != 72:
|
|
@@ -449,10 +474,12 @@ def parser_000c(payload, msg) -> dict:
|
|
|
449
474
|
elif all(payload[i : i + 2] == payload[2:4] for i in range(12, pkt_len, 10)):
|
|
450
475
|
return True # len(element) = 5 (10)
|
|
451
476
|
|
|
452
|
-
raise
|
|
477
|
+
raise exc.PacketPayloadInvalid(
|
|
478
|
+
"Unable to determine element length"
|
|
479
|
+
) # return None
|
|
453
480
|
|
|
454
481
|
if payload[2:4] == DEV_ROLE_MAP.HTG and payload[:2] == "01":
|
|
455
|
-
dev_role = DEV_ROLE_MAP[
|
|
482
|
+
dev_role = DEV_ROLE_MAP[DevRole.HT1]
|
|
456
483
|
else:
|
|
457
484
|
dev_role = DEV_ROLE_MAP[payload[2:4]]
|
|
458
485
|
|
|
@@ -482,19 +509,18 @@ def parser_000c(payload, msg) -> dict:
|
|
|
482
509
|
}
|
|
483
510
|
|
|
484
511
|
|
|
485
|
-
|
|
486
|
-
def parser_000e(payload, msg) -> dict:
|
|
487
|
-
|
|
488
|
-
assert payload in ("000000", "000014"), _INFORM_DEV_MSG
|
|
512
|
+
# unknown_000e, from STA
|
|
513
|
+
def parser_000e(payload: str, msg: Message) -> dict[str, Any]:
|
|
514
|
+
assert payload in ("000014", "000028"), _INFORM_DEV_MSG
|
|
489
515
|
|
|
490
516
|
return {
|
|
491
517
|
SZ_PAYLOAD: payload,
|
|
492
518
|
}
|
|
493
519
|
|
|
494
520
|
|
|
495
|
-
|
|
496
|
-
def parser_0016(payload, msg) -> dict:
|
|
497
|
-
# TODO: does 0016 include parent_idx?, but RQ
|
|
521
|
+
# rf_check
|
|
522
|
+
def parser_0016(payload: str, msg: Message) -> dict[str, Any]:
|
|
523
|
+
# TODO: does 0016 include parent_idx?, but RQ|07:|0000?
|
|
498
524
|
# RQ --- 22:060293 01:078710 --:------ 0016 002 0200
|
|
499
525
|
# RP --- 01:078710 22:060293 --:------ 0016 002 021E
|
|
500
526
|
# RQ --- 12:010740 01:145038 --:------ 0016 002 0800
|
|
@@ -512,21 +538,19 @@ def parser_0016(payload, msg) -> dict:
|
|
|
512
538
|
}
|
|
513
539
|
|
|
514
540
|
|
|
515
|
-
|
|
516
|
-
def parser_0100(payload, msg) ->
|
|
517
|
-
|
|
541
|
+
# language (of device/system)
|
|
542
|
+
def parser_0100(payload: str, msg: Message) -> PayDictT._0100 | PayDictT.EMPTY:
|
|
518
543
|
if msg.verb == RQ and msg.len == 1: # some RQs have a payload
|
|
519
544
|
return {}
|
|
520
545
|
|
|
521
546
|
return {
|
|
522
|
-
SZ_LANGUAGE:
|
|
523
|
-
|
|
547
|
+
SZ_LANGUAGE: hex_to_str(payload[2:6]),
|
|
548
|
+
"_unknown_0": payload[6:],
|
|
524
549
|
}
|
|
525
550
|
|
|
526
551
|
|
|
527
|
-
|
|
528
|
-
def parser_0150(payload, msg) -> dict:
|
|
529
|
-
|
|
552
|
+
# unknown_0150, from OTB
|
|
553
|
+
def parser_0150(payload: str, msg: Message) -> dict[str, Any]:
|
|
530
554
|
assert payload == "000000", _INFORM_DEV_MSG
|
|
531
555
|
|
|
532
556
|
return {
|
|
@@ -534,8 +558,8 @@ def parser_0150(payload, msg) -> dict:
|
|
|
534
558
|
}
|
|
535
559
|
|
|
536
560
|
|
|
537
|
-
|
|
538
|
-
def parser_01d0(payload, msg) -> dict:
|
|
561
|
+
# unknown_01d0, from a HR91 (when its buttons are pushed)
|
|
562
|
+
def parser_01d0(payload: str, msg: Message) -> dict[str, Any]:
|
|
539
563
|
# 23:57:28.869 045 W --- 04:000722 01:158182 --:------ 01D0 002 0003
|
|
540
564
|
# 23:57:28.931 045 I --- 01:158182 04:000722 --:------ 01D0 002 0003
|
|
541
565
|
# 23:57:31.581 048 W --- 04:000722 01:158182 --:------ 01E9 002 0003
|
|
@@ -545,24 +569,105 @@ def parser_01d0(payload, msg) -> dict:
|
|
|
545
569
|
|
|
546
570
|
assert payload[2:] in ("00", "03"), _INFORM_DEV_MSG
|
|
547
571
|
return {
|
|
548
|
-
|
|
572
|
+
"unknown_0": payload[2:],
|
|
549
573
|
}
|
|
550
574
|
|
|
551
575
|
|
|
552
|
-
|
|
553
|
-
def parser_01e9(payload, msg) -> dict:
|
|
576
|
+
# unknown_01e9, from a HR91 (when its buttons are pushed)
|
|
577
|
+
def parser_01e9(payload: str, msg: Message) -> dict[str, Any]:
|
|
554
578
|
# 23:57:31.581348 048 W --- 04:000722 01:158182 --:------ 01E9 002 0003
|
|
555
579
|
# 23:57:31.643188 045 I --- 01:158182 04:000722 --:------ 01E9 002 0000
|
|
556
580
|
|
|
557
581
|
assert payload[2:] in ("00", "03"), _INFORM_DEV_MSG
|
|
558
582
|
return {
|
|
559
|
-
|
|
583
|
+
"unknown_0": payload[2:],
|
|
560
584
|
}
|
|
561
585
|
|
|
562
586
|
|
|
563
|
-
|
|
564
|
-
def
|
|
565
|
-
#
|
|
587
|
+
# unknown_01ff, to/from a Itho Spider/Thermostat
|
|
588
|
+
def parser_01ff(payload: str, msg: Message) -> dict[str, Any]:
|
|
589
|
+
# see: https://github.com/zxdavb/ramses_rf/issues/73 & 101
|
|
590
|
+
|
|
591
|
+
# lots of '80's, and I see temps are `int(payload[6:8], 16) / 2`, so I wonder if 0x80 is N/A?
|
|
592
|
+
# also is '7F'
|
|
593
|
+
|
|
594
|
+
# return {
|
|
595
|
+
# "dis_temp": None if payload[4:6] == "80" else int(payload[4:6], 16) / 2,
|
|
596
|
+
# "set_temp": int(payload[6:8], 16) / 2,
|
|
597
|
+
# "max_temp": int(payload[8:10], 16) / 2, # 22C9 - temp high
|
|
598
|
+
# "mode_val": payload[10:12],
|
|
599
|
+
# "mode_xxx": payload[10:11] in ("9", "B", "D") and payload[11:12] in ("0", "2"),
|
|
600
|
+
# }
|
|
601
|
+
|
|
602
|
+
assert payload[:4] in ("0080", "0180"), f"{_INFORM_DEV_MSG} ({payload[:4]})"
|
|
603
|
+
assert payload[12:14] == "00", f"{_INFORM_DEV_MSG} ({payload[12:14]})"
|
|
604
|
+
# assert payload[16:22] in (
|
|
605
|
+
# "00143C",
|
|
606
|
+
# "002430",
|
|
607
|
+
# "7F8080",
|
|
608
|
+
# ), f"{_INFORM_DEV_MSG} ({payload[16:22]})" # idx|25.9C?
|
|
609
|
+
assert payload[26:30] == "0000", f"{_INFORM_DEV_MSG} ({payload[26:30]})"
|
|
610
|
+
assert payload[34:46] == "80800280FF80", f"{_INFORM_DEV_MSG} ({payload[34:46]})"
|
|
611
|
+
# assert payload[48:] in (
|
|
612
|
+
# "0000",
|
|
613
|
+
# "0020",
|
|
614
|
+
# "0084",
|
|
615
|
+
# "00A4",
|
|
616
|
+
# ), f"{_INFORM_DEV_MSG} ({payload[48:]})"
|
|
617
|
+
|
|
618
|
+
if msg.verb in (I_, RQ): # from Spider thermostat to gateway
|
|
619
|
+
assert payload[14:16] == "80", f"{_INFORM_DEV_MSG} ({payload[14:16]})"
|
|
620
|
+
# assert payload[22:26] in (
|
|
621
|
+
# "2832",
|
|
622
|
+
# "2840",
|
|
623
|
+
# ), f"{_INFORM_DEV_MSG} ({payload[22:26]})"
|
|
624
|
+
# assert payload[30:34] in (
|
|
625
|
+
# "0104",
|
|
626
|
+
# "4402",
|
|
627
|
+
# "C102",
|
|
628
|
+
# "C402",
|
|
629
|
+
# ), f"{_INFORM_DEV_MSG} ({payload[30:34]})"
|
|
630
|
+
assert payload[46:48] in ("04", "07"), f"{_INFORM_DEV_MSG} ({payload[46:48]})"
|
|
631
|
+
|
|
632
|
+
if msg.verb in (RP, W_): # from Spider gateway to thermostat
|
|
633
|
+
# assert payload[14:16] in (
|
|
634
|
+
# "00",
|
|
635
|
+
# "7F",
|
|
636
|
+
# "80",
|
|
637
|
+
# ), f"{_INFORM_DEV_MSG} ({payload[14:16]})"
|
|
638
|
+
# assert payload[22:26] in (
|
|
639
|
+
# "2840",
|
|
640
|
+
# "8080",
|
|
641
|
+
# ), f"{_INFORM_DEV_MSG} ({payload[22:26]})"
|
|
642
|
+
# assert payload[30:34] in (
|
|
643
|
+
# "0104",
|
|
644
|
+
# "3100",
|
|
645
|
+
# "3700",
|
|
646
|
+
# "B400",
|
|
647
|
+
# ), f"{_INFORM_DEV_MSG} ({payload[30:34]})"
|
|
648
|
+
assert payload[46:48] in (
|
|
649
|
+
"00",
|
|
650
|
+
"04",
|
|
651
|
+
"07",
|
|
652
|
+
), f"{_INFORM_DEV_MSG} ({payload[46:48]})"
|
|
653
|
+
|
|
654
|
+
setpoint_bounds = (
|
|
655
|
+
int(payload[6:8], 16) / 2, # as: 22C9[2:6] and [6:10] ???
|
|
656
|
+
None if msg.verb in (RP, W_) else int(payload[8:10], 16) / 2,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
SZ_TEMPERATURE: None if msg.verb in (RP, W_) else int(payload[4:6], 16) / 2,
|
|
661
|
+
SZ_SETPOINT_BOUNDS: setpoint_bounds,
|
|
662
|
+
"time_planning": not bool(int(payload[10:12], 16) & 1 << 6),
|
|
663
|
+
"temp_adjusted": bool(int(payload[10:12], 16) & 1 << 5),
|
|
664
|
+
"_flags_10": payload[10:12], #
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
# zone_schedule (fragment)
|
|
669
|
+
def parser_0404(payload: str, msg: Message) -> PayDictT._0404:
|
|
670
|
+
# Retrieval of Zone schedule (NB: 200008)
|
|
566
671
|
# RQ --- 30:185469 01:037519 --:------ 0404 007 00-200008-00-0100
|
|
567
672
|
# RP --- 01:037519 30:185469 --:------ 0404 048 00-200008-29-0103-6E2...
|
|
568
673
|
# RQ --- 30:185469 01:037519 --:------ 0404 007 00-200008-00-0203
|
|
@@ -570,7 +675,7 @@ def parser_0404(payload, msg) -> dict:
|
|
|
570
675
|
# RQ --- 30:185469 01:037519 --:------ 0404 007 00-200008-00-0303
|
|
571
676
|
# RP --- 01:037519 30:185469 --:------ 0404 038 00-200008-1F-0303-C10...
|
|
572
677
|
|
|
573
|
-
#
|
|
678
|
+
# Retrieval of DHW schedule (NB: 230008)
|
|
574
679
|
# RQ --- 30:185469 01:037519 --:------ 0404 007 00-230008-00-0100
|
|
575
680
|
# RP --- 01:037519 30:185469 --:------ 0404 048 00-230008-29-0103-618...
|
|
576
681
|
# RQ --- 30:185469 01:037519 --:------ 0404 007 00-230008-00-0203
|
|
@@ -595,7 +700,7 @@ def parser_0404(payload, msg) -> dict:
|
|
|
595
700
|
if int(payload[8:10], 16) * 2 != (frag_length := len(payload[14:])) and (
|
|
596
701
|
msg.verb != I_ or frag_length != 0
|
|
597
702
|
):
|
|
598
|
-
raise
|
|
703
|
+
raise exc.PacketPayloadInvalid(f"Incorrect fragment length: 0x{payload[8:10]}")
|
|
599
704
|
|
|
600
705
|
if msg.verb == RQ: # have a ctx: idx|frag_idx
|
|
601
706
|
return {
|
|
@@ -624,21 +729,30 @@ def parser_0404(payload, msg) -> dict:
|
|
|
624
729
|
}
|
|
625
730
|
|
|
626
731
|
|
|
627
|
-
|
|
628
|
-
def parser_0418(payload, msg) ->
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
# RP --- 01:145038 18:013393 --:------ 0418 022 000036B0010000000000108000007FFFFF7000000000 # noqa: E501
|
|
632
|
-
# RP --- 01:145038 18:013393 --:------ 0418 022 000000B00401010000008694A3CC7FFFFF70000ECC8A # noqa: E501
|
|
633
|
-
# .I --- 01:037519 --:------ 01:037519 0418 022 000000B0050000000000239581877FFFFF7000000001 # Evotouch Battery Error # noqa: E501
|
|
634
|
-
# RP --- 01:037519 18:140805 --:------ 0418 022 004024B0060006000000CB94A112FFFFFF70007AD47D # noqa: E501
|
|
635
|
-
# 0 0 1 1 3 3
|
|
636
|
-
# 2 8 2 8 0 8
|
|
732
|
+
# system_fault (fault_log_entry) - needs refactoring
|
|
733
|
+
def parser_0418(payload: str, msg: Message) -> PayDictT._0418 | PayDictT._0418_NULL:
|
|
734
|
+
null_result: PayDictT._0418_NULL
|
|
735
|
+
full_result: PayDictT._0418
|
|
637
736
|
|
|
638
737
|
# assert int(payload[4:6], 16) < 64, f"Unexpected log_idx: 0x{payload[4:6]}"
|
|
639
738
|
|
|
640
|
-
|
|
641
|
-
|
|
739
|
+
# RQ --- 18:017804 01:145038 --:------ 0418 003 000005 # log_idx=0x05
|
|
740
|
+
# RP --- 01:145038 18:017804 --:------ 0418 022 000005B0040000000000CD17B5AE7FFFFF7000000001 # log_idx=0x05
|
|
741
|
+
|
|
742
|
+
# RQ --- 18:017804 01:145038 --:------ 0418 003 000006 # log_idx=0x06
|
|
743
|
+
# RP --- 01:145038 18:017804 --:------ 0418 022 000000B0000000000000000000007FFFFF7000000000 # log_idx=None (00)
|
|
744
|
+
|
|
745
|
+
if msg.verb == RQ: # has a ctx: log_idx
|
|
746
|
+
null_result = {SZ_LOG_IDX: payload[4:6]} # type: ignore[typeddict-item]
|
|
747
|
+
return null_result
|
|
748
|
+
|
|
749
|
+
# NOTE: such payloads have idx=="00": if verb is I, can safely assume log_idx is 0,
|
|
750
|
+
# but for RP it is sentinel for null (we can't know the corresponding RQ's log_idx)
|
|
751
|
+
elif hex_to_dts(payload[18:30]) is None:
|
|
752
|
+
null_result = {SZ_LOG_ENTRY: None}
|
|
753
|
+
if msg.verb == I_:
|
|
754
|
+
null_result = {SZ_LOG_IDX: payload[4:6]} | null_result # type: ignore[assignment]
|
|
755
|
+
return null_result
|
|
642
756
|
|
|
643
757
|
try:
|
|
644
758
|
assert payload[2:4] in FAULT_STATE, f"fault state: {payload[2:4]}"
|
|
@@ -648,81 +762,77 @@ def parser_0418(payload, msg) -> dict:
|
|
|
648
762
|
assert int(payload[10:12], 16) < 16 or (
|
|
649
763
|
payload[10:12] in ("1C", F6, F9, FA, FC)
|
|
650
764
|
), f"domain id: {payload[10:12]}"
|
|
651
|
-
except AssertionError as
|
|
765
|
+
except AssertionError as err:
|
|
652
766
|
_LOGGER.warning(
|
|
653
|
-
f"{msg!r} < {_INFORM_DEV_MSG} ({
|
|
767
|
+
f"{msg!r} < {_INFORM_DEV_MSG} ({err}), with a photo of your fault log"
|
|
654
768
|
)
|
|
655
769
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
"state": FAULT_STATE.get(payload[2:4], payload[2:4]),
|
|
659
|
-
"type": FAULT_TYPE.get(payload[8:10], payload[8:10]),
|
|
660
|
-
SZ_DEVICE_CLASS: FAULT_DEVICE_CLASS.get(payload[12:14], payload[12:14]),
|
|
661
|
-
}
|
|
770
|
+
# log_entry will not be None, because of guard clauses, above
|
|
771
|
+
log_entry: PayDictT.FAULT_LOG_ENTRY = parse_fault_log_entry(payload) # type: ignore[assignment]
|
|
662
772
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
elif payload[10:12] == FA and result[SZ_DEVICE_CLASS] == SZ_ACTUATOR:
|
|
666
|
-
result[SZ_DEVICE_CLASS] = DEV_ROLE_MAP[DEV_ROLE.HTG] # speculative
|
|
667
|
-
elif payload[10:12] == F9 and result[SZ_DEVICE_CLASS] == SZ_ACTUATOR:
|
|
668
|
-
result[SZ_DEVICE_CLASS] = DEV_ROLE_MAP[DEV_ROLE.HT1] # speculative
|
|
773
|
+
# log_idx is not intrinsic to the fault & increments as the fault moves down the log
|
|
774
|
+
log_entry.pop(f"_{SZ_LOG_IDX}") # type: ignore[misc]
|
|
669
775
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
result.update({key_name: payload[10:12]})
|
|
776
|
+
_KEYS = (SZ_TIMESTAMP, SZ_FAULT_STATE, SZ_FAULT_TYPE)
|
|
777
|
+
entry = [v for k, v in log_entry.items() if k in _KEYS]
|
|
673
778
|
|
|
674
|
-
if
|
|
675
|
-
|
|
676
|
-
elif
|
|
677
|
-
|
|
779
|
+
if log_entry[SZ_DEVICE_CLASS] != FaultDeviceClass.ACTUATOR:
|
|
780
|
+
entry.append(log_entry[SZ_DEVICE_CLASS])
|
|
781
|
+
elif log_entry[SZ_DOMAIN_IDX] == FC:
|
|
782
|
+
entry.append(DEV_ROLE_MAP[DevRole.APP]) # actual evohome UI
|
|
783
|
+
elif log_entry[SZ_DOMAIN_IDX] == FA:
|
|
784
|
+
entry.append(DEV_ROLE_MAP[DevRole.HTG]) # speculative
|
|
785
|
+
elif log_entry[SZ_DOMAIN_IDX] == F9:
|
|
786
|
+
entry.append(DEV_ROLE_MAP[DevRole.HT1]) # speculative
|
|
787
|
+
else:
|
|
788
|
+
entry.append(FaultDeviceClass.ACTUATOR)
|
|
678
789
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
)
|
|
790
|
+
# TODO: remove the qualifier (the assert is false)
|
|
791
|
+
if log_entry[SZ_DEVICE_CLASS] != FaultDeviceClass.CONTROLLER:
|
|
792
|
+
# assert log_entry[SZ_DOMAIN_IDX] == "00", log_entry[SZ_DOMAIN_IDX]
|
|
793
|
+
# key_name = SZ_ZONE_IDX if int(payload[10:12], 16) < 16 else SZ_DOMAIN_ID
|
|
794
|
+
# log_entry.update({key_name: payload[10:12]})
|
|
795
|
+
entry.append(log_entry[SZ_DOMAIN_IDX])
|
|
686
796
|
|
|
687
|
-
|
|
797
|
+
if log_entry[SZ_DEVICE_ID] not in ("00:000000", "00:000001", "00:000002"):
|
|
798
|
+
# "00:000001 for Controller? "00:000002 for Unknown?
|
|
799
|
+
entry.append(log_entry[SZ_DEVICE_ID])
|
|
688
800
|
|
|
801
|
+
entry.extend((payload[6:8], payload[14:18], payload[30:38])) # TODO: remove?
|
|
689
802
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
# .I --- 34:064023 --:------ 34:064023 042F 008 00-0000-0026-0026-F5
|
|
696
|
-
# .I --- 34:092243 --:------ 34:092243 042F 008 00-0001-0021-0022-01
|
|
697
|
-
# .I 34:011469 --:------ 34:011469 042F 008 00-0001-0003-0004-BC
|
|
803
|
+
full_result = {
|
|
804
|
+
SZ_LOG_IDX: payload[4:6], # type: ignore[typeddict-item]
|
|
805
|
+
SZ_LOG_ENTRY: tuple([str(r) for r in entry]),
|
|
806
|
+
}
|
|
807
|
+
return full_result
|
|
698
808
|
|
|
699
|
-
# .I --- 32:168090 --:------ 32:168090 042F 009 00-0000100F00105050
|
|
700
|
-
# .I --- 32:166025 --:------ 32:166025 042F 009 00-050E0B0C00111470
|
|
701
809
|
|
|
810
|
+
# unknown_042f, from STA, VMS
|
|
811
|
+
def parser_042f(payload: str, msg: Message) -> dict[str, Any]:
|
|
702
812
|
return {
|
|
703
813
|
"counter_1": f"0x{payload[2:6]}",
|
|
704
814
|
"counter_3": f"0x{payload[6:10]}",
|
|
705
815
|
"counter_5": f"0x{payload[10:14]}",
|
|
706
|
-
|
|
816
|
+
"unknown_7": f"0x{payload[14:]}",
|
|
707
817
|
}
|
|
708
818
|
|
|
709
819
|
|
|
710
|
-
|
|
711
|
-
def parser_0b04(payload, msg) -> dict:
|
|
820
|
+
# TODO: unknown_0b04, from THM (only when its a CTL?)
|
|
821
|
+
def parser_0b04(payload: str, msg: Message) -> dict[str, Any]:
|
|
712
822
|
# .I --- --:------ --:------ 12:207082 0B04 002 00C8 # batch of 3, every 24h
|
|
713
823
|
|
|
714
824
|
return {
|
|
715
|
-
|
|
825
|
+
"unknown_1": payload[2:],
|
|
716
826
|
}
|
|
717
827
|
|
|
718
828
|
|
|
719
|
-
|
|
720
|
-
def parser_1030(payload, msg) ->
|
|
829
|
+
# mixvalve_config (zone), FAN
|
|
830
|
+
def parser_1030(payload: str, msg: Message) -> PayDictT._1030:
|
|
721
831
|
# .I --- 01:145038 --:------ 01:145038 1030 016 0A-C80137-C9010F-CA0196-CB0100-CC0101
|
|
722
832
|
# .I --- --:------ --:------ 12:144017 1030 016 01-C80137-C9010F-CA0196-CB010F-CC0101
|
|
723
833
|
# RP --- 32:155617 18:005904 --:------ 1030 007 00-200100-21011F
|
|
724
834
|
|
|
725
|
-
def _parser(seqx) -> dict:
|
|
835
|
+
def _parser(seqx: str) -> dict:
|
|
726
836
|
assert seqx[2:4] == "01", seqx[2:4]
|
|
727
837
|
|
|
728
838
|
param_name = {
|
|
@@ -741,36 +851,32 @@ def parser_1030(payload, msg) -> dict:
|
|
|
741
851
|
# assert payload[30:] in ("00", "01"), payload[30:]
|
|
742
852
|
|
|
743
853
|
params = [_parser(payload[i : i + 6]) for i in range(2, len(payload), 6)]
|
|
744
|
-
return {k: v for x in params for k, v in x.items()}
|
|
854
|
+
return {k: v for x in params for k, v in x.items()} # type: ignore[return-value]
|
|
745
855
|
|
|
746
856
|
|
|
747
|
-
|
|
748
|
-
def parser_1060(payload, msg) ->
|
|
857
|
+
# device_battery (battery_state)
|
|
858
|
+
def parser_1060(payload: str, msg: Message) -> PayDictT._1060:
|
|
749
859
|
"""Return the battery state.
|
|
750
860
|
|
|
751
861
|
Some devices (04:) will also report battery level.
|
|
752
862
|
"""
|
|
753
|
-
# 06:48:23.948 049 I --- 12:010740 --:------ 12:010740 1060 003 00FF01
|
|
754
|
-
# 16:18:43.515 051 I --- 12:010740 --:------ 12:010740 1060 003 00FF00
|
|
755
|
-
# 16:14:44.180 054 I --- 04:056057 --:------ 04:056057 1060 003 002800
|
|
756
|
-
# 17:34:35.460 087 I --- 04:189076 --:------ 01:145038 1060 003 026401
|
|
757
863
|
|
|
758
864
|
assert msg.len == 3, msg.len
|
|
759
865
|
assert payload[4:6] in ("00", "01")
|
|
760
866
|
|
|
761
867
|
return {
|
|
762
868
|
"battery_low": payload[4:] == "00",
|
|
763
|
-
"battery_level":
|
|
869
|
+
"battery_level": None if payload[2:4] == "00" else hex_to_percent(payload[2:4]),
|
|
764
870
|
}
|
|
765
871
|
|
|
766
872
|
|
|
767
|
-
|
|
768
|
-
def parser_1081(payload, msg) ->
|
|
769
|
-
return {SZ_SETPOINT:
|
|
873
|
+
# max_ch_setpoint (supply high limit)
|
|
874
|
+
def parser_1081(payload: str, msg: Message) -> PayDictT._1081:
|
|
875
|
+
return {SZ_SETPOINT: hex_to_temp(payload[2:])}
|
|
770
876
|
|
|
771
877
|
|
|
772
|
-
|
|
773
|
-
def parser_1090(payload, msg) ->
|
|
878
|
+
# unknown_1090 (non-Evohome, e.g. ST9520C)
|
|
879
|
+
def parser_1090(payload: str, msg: Message) -> PayDictT._1090:
|
|
774
880
|
# 14:08:05.176 095 RP --- 23:100224 22:219457 --:------ 1090 005 007FFF01F4
|
|
775
881
|
# 18:08:05.809 095 RP --- 23:100224 22:219457 --:------ 1090 005 007FFF01F4
|
|
776
882
|
|
|
@@ -779,26 +885,25 @@ def parser_1090(payload, msg) -> dict:
|
|
|
779
885
|
assert int(payload[:2], 16) < 2, _INFORM_DEV_MSG
|
|
780
886
|
|
|
781
887
|
return {
|
|
782
|
-
|
|
783
|
-
|
|
888
|
+
"temperature_0": hex_to_temp(payload[2:6]),
|
|
889
|
+
"temperature_1": hex_to_temp(payload[6:10]),
|
|
784
890
|
}
|
|
785
891
|
|
|
786
892
|
|
|
787
|
-
|
|
788
|
-
def parser_1098(payload, msg) -> dict:
|
|
789
|
-
|
|
893
|
+
# unknown_1098, from OTB
|
|
894
|
+
def parser_1098(payload: str, msg: Message) -> dict[str, Any]:
|
|
790
895
|
assert payload == "00C8", _INFORM_DEV_MSG
|
|
791
896
|
|
|
792
897
|
return {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
payload[2:],
|
|
898
|
+
"_payload": payload,
|
|
899
|
+
"_value": {"00": False, "C8": True}.get(
|
|
900
|
+
payload[2:], hex_to_percent(payload[2:])
|
|
796
901
|
),
|
|
797
902
|
}
|
|
798
903
|
|
|
799
904
|
|
|
800
|
-
|
|
801
|
-
def parser_10a0(payload, msg) ->
|
|
905
|
+
# dhw (cylinder) params # FIXME: a bit messy
|
|
906
|
+
def parser_10a0(payload: str, msg: Message) -> PayDictT._10A0 | PayDictT.EMPTY:
|
|
802
907
|
# RQ --- 07:045960 01:145038 --:------ 10A0 006 00-1087-00-03E4 # RQ/RP, every 24h
|
|
803
908
|
# RP --- 01:145038 07:045960 --:------ 10A0 006 00-109A-00-03E8
|
|
804
909
|
# RP --- 10:048122 18:006402 --:------ 10A0 003 00-1B58
|
|
@@ -827,34 +932,32 @@ def parser_10a0(payload, msg) -> dict:
|
|
|
827
932
|
assert msg.len in (1, 3, 6), msg.len # OTB uses 3, evohome uses 6
|
|
828
933
|
assert payload[:2] in ("00", "01"), payload[:2] # can be two DHW valves/system
|
|
829
934
|
|
|
830
|
-
result = {}
|
|
935
|
+
result: PayDictT._10A0 = {} # type: ignore[typeddict-item]
|
|
831
936
|
if msg.len >= 2:
|
|
832
|
-
setpoint =
|
|
937
|
+
setpoint = hex_to_temp(payload[2:6]) # 255 for OTB? iff no DHW?
|
|
833
938
|
result = {SZ_SETPOINT: None if setpoint == 255 else setpoint} # 30.0-85.0 C
|
|
834
939
|
if msg.len >= 4:
|
|
835
940
|
result["overrun"] = int(payload[6:8], 16) # 0-10 minutes
|
|
836
941
|
if msg.len >= 6:
|
|
837
|
-
result["differential"] =
|
|
942
|
+
result["differential"] = hex_to_temp(payload[8:12]) # 1.0-10.0 C
|
|
838
943
|
|
|
839
944
|
return result
|
|
840
945
|
|
|
841
946
|
|
|
842
|
-
|
|
843
|
-
def parser_10b0(payload, msg) -> dict:
|
|
844
|
-
|
|
947
|
+
# unknown_10b0, from OTB
|
|
948
|
+
def parser_10b0(payload: str, msg: Message) -> dict[str, Any]:
|
|
845
949
|
assert payload == "0000", _INFORM_DEV_MSG
|
|
846
950
|
|
|
847
951
|
return {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
payload[2:],
|
|
952
|
+
"_payload": payload,
|
|
953
|
+
"_value": {"00": False, "C8": True}.get(
|
|
954
|
+
payload[2:], hex_to_percent(payload[2:])
|
|
851
955
|
),
|
|
852
956
|
}
|
|
853
957
|
|
|
854
958
|
|
|
855
|
-
|
|
856
|
-
def parser_10d0(payload, msg) -> dict:
|
|
857
|
-
|
|
959
|
+
# filter_change, HVAC
|
|
960
|
+
def parser_10d0(payload: str, msg: Message) -> dict[str, Any]:
|
|
858
961
|
# 2022-07-03T22:52:34.571579 045 W --- 37:171871 32:155617 --:------ 10D0 002 00FF
|
|
859
962
|
# 2022-07-03T22:52:34.596526 066 I --- 32:155617 37:171871 --:------ 10D0 006 0047B44F0000
|
|
860
963
|
# then...
|
|
@@ -865,22 +968,27 @@ def parser_10d0(payload, msg) -> dict:
|
|
|
865
968
|
# Default is 180 180 200. The returned value is the amount of days (180),
|
|
866
969
|
# total amount of days till change (180), percentage (200)
|
|
867
970
|
|
|
971
|
+
result: dict[str, bool | float | None]
|
|
972
|
+
|
|
868
973
|
if msg.verb == W_:
|
|
869
|
-
|
|
870
|
-
else:
|
|
871
|
-
result = {"days_remaining": int(payload[2:4], 16)}
|
|
974
|
+
return {"reset_counter": payload[2:4] != "00"}
|
|
872
975
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
if
|
|
876
|
-
result
|
|
976
|
+
result = {}
|
|
977
|
+
|
|
978
|
+
if payload[2:4] not in ("FF", "FE"):
|
|
979
|
+
result[SZ_REMAINING_DAYS] = int(payload[2:4], 16)
|
|
980
|
+
|
|
981
|
+
if payload[4:6] not in ("FF", "FE"):
|
|
982
|
+
result["days_lifetime"] = int(payload[4:6], 16)
|
|
983
|
+
|
|
984
|
+
result[SZ_REMAINING_PERCENT] = hex_to_percent(payload[6:8])
|
|
877
985
|
|
|
878
986
|
return result
|
|
879
987
|
|
|
880
988
|
|
|
881
|
-
|
|
882
|
-
def parser_10e0(payload, msg) -> dict:
|
|
883
|
-
if payload == "00": # some HVAC devices
|
|
989
|
+
# device_info
|
|
990
|
+
def parser_10e0(payload: str, msg: Message) -> dict[str, Any]:
|
|
991
|
+
if payload == "00": # some HVAC devices will RP|10E0|00
|
|
884
992
|
return {}
|
|
885
993
|
|
|
886
994
|
assert msg.len in (19, 28, 29, 30, 36, 38), msg.len # >= 19, msg.len
|
|
@@ -891,39 +999,39 @@ def parser_10e0(payload, msg) -> dict:
|
|
|
891
999
|
# if DEV_MODE: # TODO
|
|
892
1000
|
try: # DEX
|
|
893
1001
|
check_signature(msg.src.type, payload[2:20])
|
|
894
|
-
except ValueError as
|
|
895
|
-
_LOGGER.
|
|
896
|
-
f"{msg!r} < {_INFORM_DEV_MSG}, with the make/model of device: {msg.src} ({
|
|
1002
|
+
except ValueError as err:
|
|
1003
|
+
_LOGGER.info(
|
|
1004
|
+
f"{msg!r} < {_INFORM_DEV_MSG}, with the make/model of device: {msg.src} ({err})"
|
|
897
1005
|
)
|
|
898
1006
|
|
|
899
1007
|
description, _, unknown = payload[36:].partition("00")
|
|
900
|
-
assert msg.verb == RP or not unknown, f"{unknown}"
|
|
901
1008
|
|
|
902
1009
|
result = {
|
|
903
|
-
|
|
904
|
-
"
|
|
905
|
-
# "manufacturer_group": payload[2:6], # 0001/0002
|
|
1010
|
+
SZ_OEM_CODE: payload[14:16], # 00/FF is CH/DHW, 01/6x is HVAC
|
|
1011
|
+
# "_manufacturer_group": payload[2:6], # 0001-HVAC, 0002-CH/DHW
|
|
906
1012
|
"manufacturer_sub_id": payload[6:8],
|
|
907
1013
|
"product_id": payload[8:10], # if CH/DHW: matches device_type (sometimes)
|
|
1014
|
+
"date_1": hex_to_date(payload[28:36]) or "0000-00-00", # hardware?
|
|
1015
|
+
"date_2": hex_to_date(payload[20:28]) or "0000-00-00", # firmware?
|
|
908
1016
|
# "software_ver_id": payload[10:12],
|
|
909
1017
|
# "list_ver_id": payload[12:14], # if FF/01 is CH/DHW, then 01/FF
|
|
910
|
-
"oem_code": payload[14:16], # 00/FF is CH/DHW, 01/6x is HVAC
|
|
911
1018
|
# # "additional_ver_a": payload[16:18],
|
|
912
1019
|
# # "additional_ver_b": payload[18:20],
|
|
1020
|
+
# "_signature": payload[2:20],
|
|
913
1021
|
"description": bytearray.fromhex(description).decode(),
|
|
914
1022
|
}
|
|
915
|
-
if
|
|
916
|
-
result[
|
|
1023
|
+
if unknown: # TODO: why only RP|OTB, I|DT4s do this?
|
|
1024
|
+
result["_unknown"] = unknown
|
|
917
1025
|
return result
|
|
918
1026
|
|
|
919
1027
|
|
|
920
|
-
|
|
921
|
-
def parser_10e1(payload, msg) ->
|
|
1028
|
+
# device_id
|
|
1029
|
+
def parser_10e1(payload: str, msg: Message) -> PayDictT._10E1:
|
|
922
1030
|
return {SZ_DEVICE_ID: hex_id_to_dev_id(payload[2:])}
|
|
923
1031
|
|
|
924
1032
|
|
|
925
|
-
|
|
926
|
-
def parser_10e2(payload, msg) -> dict:
|
|
1033
|
+
# unknown_10e2 - HVAC
|
|
1034
|
+
def parser_10e2(payload: str, msg: Message) -> dict[str, Any]:
|
|
927
1035
|
# .I --- --:------ --:------ 20:231151 10E2 003 00AD74 # every 2 minutes
|
|
928
1036
|
|
|
929
1037
|
assert payload[:2] == "00", _INFORM_DEV_MSG
|
|
@@ -934,10 +1042,12 @@ def parser_10e2(payload, msg) -> dict:
|
|
|
934
1042
|
}
|
|
935
1043
|
|
|
936
1044
|
|
|
937
|
-
|
|
938
|
-
def parser_1100(
|
|
939
|
-
|
|
940
|
-
|
|
1045
|
+
# tpi_params (domain/zone/device) # FIXME: a bit messy
|
|
1046
|
+
def parser_1100(
|
|
1047
|
+
payload: str, msg: Message
|
|
1048
|
+
) -> PayDictT._1100 | PayDictT._1100_IDX | PayDictT._JASPER | PayDictT.EMPTY:
|
|
1049
|
+
def complex_idx(seqx: str) -> PayDictT._1100_IDX | PayDictT.EMPTY:
|
|
1050
|
+
return {SZ_DOMAIN_ID: seqx} if seqx[:1] == "F" else {} # type: ignore[typeddict-item] # only FC
|
|
941
1051
|
|
|
942
1052
|
if msg.src.type == DEV_TYPE_MAP.JIM: # Honeywell Japser, DEX
|
|
943
1053
|
assert msg.len == 19, msg.len
|
|
@@ -958,37 +1068,35 @@ def parser_1100(payload, msg) -> dict:
|
|
|
958
1068
|
# - min_on_time: 1 (1-5) // ?? (1, 5, 10,...30)
|
|
959
1069
|
# - min_off_time: 1 (1-?) // ?? (0, 5, 10, 15)
|
|
960
1070
|
|
|
961
|
-
def _parser(seqx) ->
|
|
1071
|
+
def _parser(seqx: str) -> PayDictT._1100:
|
|
962
1072
|
return {
|
|
963
1073
|
"cycle_rate": int(int(payload[2:4], 16) / 4), # cycles/hour
|
|
964
1074
|
"min_on_time": int(payload[4:6], 16) / 4, # min
|
|
965
1075
|
"min_off_time": int(payload[6:8], 16) / 4, # min
|
|
966
|
-
|
|
1076
|
+
"_unknown_0": payload[8:10], # always 00, FF?
|
|
967
1077
|
}
|
|
968
1078
|
|
|
969
1079
|
result = _parser(payload)
|
|
970
1080
|
|
|
971
1081
|
if msg.len > 5:
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1082
|
+
pbw = hex_to_temp(payload[10:14])
|
|
1083
|
+
|
|
1084
|
+
assert pbw is None or 1.5 <= pbw <= 3.0, (
|
|
1085
|
+
f"unexpected value for PBW: {payload[10:14]}"
|
|
1086
|
+
)
|
|
975
1087
|
|
|
976
1088
|
result.update(
|
|
977
1089
|
{
|
|
978
|
-
"proportional_band_width":
|
|
979
|
-
|
|
1090
|
+
"proportional_band_width": pbw,
|
|
1091
|
+
"_unknown_1": payload[14:], # always 01?
|
|
980
1092
|
}
|
|
981
1093
|
)
|
|
982
1094
|
|
|
983
|
-
return
|
|
984
|
-
**complex_idx(payload[:2]),
|
|
985
|
-
**result,
|
|
986
|
-
}
|
|
1095
|
+
return complex_idx(payload[:2]) | result
|
|
987
1096
|
|
|
988
1097
|
|
|
989
|
-
|
|
990
|
-
def parser_11f0(payload, msg) -> dict:
|
|
991
|
-
|
|
1098
|
+
# unknown_11f0, from heatpump relay
|
|
1099
|
+
def parser_11f0(payload: str, msg: Message) -> dict[str, Any]:
|
|
992
1100
|
assert payload == "000009000000000000", _INFORM_DEV_MSG
|
|
993
1101
|
|
|
994
1102
|
return {
|
|
@@ -996,142 +1104,88 @@ def parser_11f0(payload, msg) -> dict:
|
|
|
996
1104
|
}
|
|
997
1105
|
|
|
998
1106
|
|
|
999
|
-
|
|
1000
|
-
def parser_1260(payload, msg) ->
|
|
1001
|
-
return {SZ_TEMPERATURE:
|
|
1107
|
+
# dhw cylinder temperature
|
|
1108
|
+
def parser_1260(payload: str, msg: Message) -> PayDictT._1260:
|
|
1109
|
+
return {SZ_TEMPERATURE: hex_to_temp(payload[2:])}
|
|
1002
1110
|
|
|
1003
1111
|
|
|
1004
|
-
|
|
1005
|
-
def parser_1280(payload, msg) ->
|
|
1006
|
-
|
|
1112
|
+
# HVAC: outdoor humidity
|
|
1113
|
+
def parser_1280(payload: str, msg: Message) -> PayDictT._1280:
|
|
1114
|
+
return parse_outdoor_humidity(payload[2:])
|
|
1007
1115
|
|
|
1008
|
-
rh = percent_from_hex(payload[2:4], high_res=False)
|
|
1009
|
-
if msg.len == 2:
|
|
1010
|
-
return {SZ_OUTDOOR_HUMIDITY: rh}
|
|
1011
1116
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
@parser_decorator # outdoor temperature
|
|
1020
|
-
def parser_1290(payload, msg) -> dict:
|
|
1021
|
-
# evohome responds to an RQ
|
|
1022
|
-
return {SZ_TEMPERATURE: temp_from_hex(payload[2:])}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
@parser_decorator # co2_level
|
|
1026
|
-
def parser_1298(payload, msg) -> dict:
|
|
1027
|
-
# .I --- 37:258565 --:------ 37:258565 1298 003 0007D0
|
|
1028
|
-
FAULT_CODES_CO2 = {
|
|
1029
|
-
"80": "sensor short circuit",
|
|
1030
|
-
"81": "sensor open",
|
|
1031
|
-
"83": "sensor value too high",
|
|
1032
|
-
"84": "sensor value too low",
|
|
1033
|
-
"85": "sensor unreliable",
|
|
1034
|
-
}
|
|
1035
|
-
if fault := FAULT_CODES_CO2.get(payload[:2]):
|
|
1036
|
-
return {"sensor_fault": fault}
|
|
1037
|
-
|
|
1038
|
-
return {SZ_CO2_LEVEL: double_from_hex(payload[2:])}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
@parser_decorator # indoor_humidity
|
|
1042
|
-
def parser_12a0(payload, msg) -> dict:
|
|
1043
|
-
|
|
1044
|
-
FAULT_CODES_RHUM = {
|
|
1045
|
-
"EF": "sensor not available",
|
|
1046
|
-
"F0": "sensor short circuit",
|
|
1047
|
-
"F1": "sensor open",
|
|
1048
|
-
"F2": "sensor not available",
|
|
1049
|
-
"F3": "sensor value too high",
|
|
1050
|
-
"F4": "sensor value too low",
|
|
1051
|
-
"F5": "sensor unreliable",
|
|
1052
|
-
} # relative humidity sensor
|
|
1053
|
-
|
|
1054
|
-
assert payload[2:4] in FAULT_CODES_RHUM or int(payload[2:4], 16) <= 100
|
|
1055
|
-
if fault := FAULT_CODES_RHUM.get(payload[2:4]):
|
|
1056
|
-
return {"sensor_fault": fault}
|
|
1057
|
-
|
|
1058
|
-
# FAULT_CODES_TEMP = {
|
|
1059
|
-
# "7F": "sensor not available",
|
|
1060
|
-
# "80": "sensor short circuit",
|
|
1061
|
-
# "81": "sensor open",
|
|
1062
|
-
# "82": "sensor not available",
|
|
1063
|
-
# "83": "sensor value too high",
|
|
1064
|
-
# "84": "sensor value too low",
|
|
1065
|
-
# "85": "sensor unreliable",
|
|
1066
|
-
# }
|
|
1067
|
-
# assert payload[4:6] in FAULT_CODES_TEMP or ...
|
|
1068
|
-
# if (fault := FAULT_CODES_TEMP.get(payload[2:4])):
|
|
1069
|
-
# return {"sensor_fault": fault}
|
|
1117
|
+
# outdoor temperature
|
|
1118
|
+
def parser_1290(payload: str, msg: Message) -> PayDictT._1290:
|
|
1119
|
+
# evohome responds to an RQ, also from OTB
|
|
1120
|
+
return parse_outdoor_temp(payload[2:])
|
|
1070
1121
|
|
|
1071
|
-
rh = percent_from_hex(payload[2:4], high_res=False)
|
|
1072
|
-
if msg.len == 2:
|
|
1073
|
-
return {SZ_INDOOR_HUMIDITY: rh}
|
|
1074
1122
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
}
|
|
1123
|
+
# HVAC: co2_level, see: 31DA[6:10]
|
|
1124
|
+
def parser_1298(payload: str, msg: Message) -> PayDictT._1298:
|
|
1125
|
+
return parse_co2_level(payload[2:6])
|
|
1126
|
+
|
|
1080
1127
|
|
|
1128
|
+
# HVAC: indoor_humidity, array of 3 sets for HRU
|
|
1129
|
+
def parser_12a0(
|
|
1130
|
+
payload: str, msg: Message
|
|
1131
|
+
) -> PayDictT.INDOOR_HUMIDITY | list[PayDictT._12A0]:
|
|
1132
|
+
if len(payload) <= 14:
|
|
1133
|
+
return parse_indoor_humidity(payload[2:12])
|
|
1081
1134
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1135
|
+
return [
|
|
1136
|
+
{
|
|
1137
|
+
"hvac_idx": payload[i : i + 2], # used as index
|
|
1138
|
+
**parse_humidity_element(payload[i + 2 : i + 12], payload[i : i + 2]),
|
|
1139
|
+
}
|
|
1140
|
+
for i in range(0, len(payload), 14)
|
|
1141
|
+
]
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
# window_state (of a device/zone)
|
|
1145
|
+
def parser_12b0(payload: str, msg: Message) -> PayDictT._12B0:
|
|
1084
1146
|
assert payload[2:] in ("0000", "C800", "FFFF"), payload[2:] # "FFFF" means N/A
|
|
1085
1147
|
|
|
1086
1148
|
return {
|
|
1087
|
-
SZ_WINDOW_OPEN:
|
|
1149
|
+
SZ_WINDOW_OPEN: hex_to_bool(payload[2:4]),
|
|
1088
1150
|
}
|
|
1089
1151
|
|
|
1090
1152
|
|
|
1091
|
-
|
|
1092
|
-
def parser_12c0(payload, msg) ->
|
|
1093
|
-
|
|
1153
|
+
# displayed temperature (on a TR87RF bound to a RFG100)
|
|
1154
|
+
def parser_12c0(payload: str, msg: Message) -> PayDictT._12C0:
|
|
1094
1155
|
if payload[2:4] == "80":
|
|
1095
|
-
temp = None
|
|
1096
|
-
elif payload[4:] == "00": # units are 1.0 F
|
|
1156
|
+
temp: float | None = None
|
|
1157
|
+
elif payload[4:6] == "00": # units are 1.0 F
|
|
1097
1158
|
temp = int(payload[2:4], 16)
|
|
1098
1159
|
else: # if payload[4:] == "01": # units are 0.5 C
|
|
1099
1160
|
temp = int(payload[2:4], 16) / 2
|
|
1100
1161
|
|
|
1101
|
-
|
|
1162
|
+
result: PayDictT._12C0 = {
|
|
1102
1163
|
SZ_TEMPERATURE: temp,
|
|
1103
|
-
"units": {"00": "Fahrenheit", "01": "Celsius"}[payload[4:6]],
|
|
1104
|
-
f"_{SZ_UNKNOWN}_6": payload[6:],
|
|
1164
|
+
"units": {"00": "Fahrenheit", "01": "Celsius"}[payload[4:6]], # type: ignore[typeddict-item]
|
|
1105
1165
|
}
|
|
1166
|
+
if len(payload) > 6:
|
|
1167
|
+
result["_unknown_6"] = payload[6:]
|
|
1168
|
+
return result
|
|
1106
1169
|
|
|
1107
1170
|
|
|
1108
|
-
|
|
1109
|
-
def parser_12c8(payload, msg) ->
|
|
1110
|
-
|
|
1111
|
-
# 04:50:01.717 078 I --- 37:261128 --:------ 37:261128 12C8 003 00A740
|
|
1112
|
-
# 04:50:31.443 078 I --- 37:261128 --:------ 37:261128 31DA 029 007A40-05993AEF7FFF7FFF7FFF7FFFF808EF1807000000EFEF7FFF7FFF # noqa: E501
|
|
1113
|
-
# 04:50:31.544 078 I --- 37:261128 --:------ 37:261128 12C8 003 007A40
|
|
1114
|
-
# 04:51:40.262 079 I --- 37:261128 --:------ 37:261128 31DA 029 009540-054B3AEF7FFF7FFF7FFF7FFFF808EF180E000000EFEF7FFF7FFF # noqa: E501
|
|
1115
|
-
# 04:51:41.192 078 I --- 37:261128 --:------ 37:261128 12C8 003 009540
|
|
1116
|
-
|
|
1117
|
-
return {
|
|
1118
|
-
SZ_AIR_QUALITY: percent_from_hex(payload[2:4]), # 31DA[2:4]
|
|
1119
|
-
SZ_AIR_QUALITY_BASE: int(payload[4:6], 16), # 31DA[4:6]
|
|
1120
|
-
}
|
|
1171
|
+
# HVAC: air_quality (and air_quality_basis), see: 31DA[2:6]
|
|
1172
|
+
def parser_12c8(payload: str, msg: Message) -> PayDictT._12C8:
|
|
1173
|
+
return parse_air_quality(payload[2:6])
|
|
1121
1174
|
|
|
1122
1175
|
|
|
1123
|
-
|
|
1124
|
-
def parser_12f0(payload, msg) ->
|
|
1125
|
-
return {
|
|
1176
|
+
# dhw_flow_rate
|
|
1177
|
+
def parser_12f0(payload: str, msg: Message) -> PayDictT._12F0:
|
|
1178
|
+
return {SZ_DHW_FLOW_RATE: hex_to_temp(payload[2:])}
|
|
1126
1179
|
|
|
1127
1180
|
|
|
1128
|
-
|
|
1129
|
-
def parser_1300(payload, msg) ->
|
|
1130
|
-
|
|
1181
|
+
# ch_pressure
|
|
1182
|
+
def parser_1300(payload: str, msg: Message) -> PayDictT._1300:
|
|
1183
|
+
# 0x9F6 (2550 dec = 2.55 bar) appears to be a sentinel value
|
|
1184
|
+
return {SZ_PRESSURE: None if payload[2:] == "09F6" else hex_to_temp(payload[2:])}
|
|
1131
1185
|
|
|
1132
1186
|
|
|
1133
|
-
|
|
1134
|
-
def parser_1470(payload, msg) -> dict:
|
|
1187
|
+
# programme_scheme, HVAC
|
|
1188
|
+
def parser_1470(payload: str, msg: Message) -> dict[str, Any]:
|
|
1135
1189
|
# Seen on Orcon: see 1470, 1F70, 22B0
|
|
1136
1190
|
|
|
1137
1191
|
SCHEDULE_SCHEME = {
|
|
@@ -1155,18 +1209,23 @@ def parser_1470(payload, msg) -> dict:
|
|
|
1155
1209
|
return {
|
|
1156
1210
|
"scheme": SCHEDULE_SCHEME.get(payload[2:3], f"unknown_{payload[2:3]}"),
|
|
1157
1211
|
"daily_setpoints": payload[3:4],
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1212
|
+
"_value_4": payload[4:8],
|
|
1213
|
+
"_value_8": payload[8:10],
|
|
1214
|
+
"_value_10": payload[10:],
|
|
1161
1215
|
}
|
|
1162
1216
|
|
|
1163
1217
|
|
|
1164
|
-
|
|
1165
|
-
def parser_1f09(payload, msg) ->
|
|
1218
|
+
# system_sync
|
|
1219
|
+
def parser_1f09(payload: str, msg: Message) -> PayDictT._1F09:
|
|
1166
1220
|
# 22:51:19.287 067 I --- --:------ --:------ 12:193204 1F09 003 010A69
|
|
1167
1221
|
# 22:51:19.318 068 I --- --:------ --:------ 12:193204 2309 003 010866
|
|
1168
1222
|
# 22:51:19.321 067 I --- --:------ --:------ 12:193204 30C9 003 0108C3
|
|
1169
1223
|
|
|
1224
|
+
# domain_id from 01:/CTL:
|
|
1225
|
+
# - FF for regular sync messages
|
|
1226
|
+
# - 00 when responding to a request
|
|
1227
|
+
# - F8 after binding a device
|
|
1228
|
+
|
|
1170
1229
|
assert msg.len == 3, f"length is {msg.len}, expecting 3"
|
|
1171
1230
|
assert payload[:2] in ("00", "01", F8, FF) # W/F8
|
|
1172
1231
|
|
|
@@ -1179,33 +1238,34 @@ def parser_1f09(payload, msg) -> dict:
|
|
|
1179
1238
|
}
|
|
1180
1239
|
|
|
1181
1240
|
|
|
1182
|
-
|
|
1183
|
-
def parser_1f41(payload, msg) ->
|
|
1241
|
+
# dhw_mode
|
|
1242
|
+
def parser_1f41(payload: str, msg: Message) -> PayDictT._1F41:
|
|
1184
1243
|
# 053 RP --- 01:145038 18:013393 --:------ 1F41 006 00FF00FFFFFF # no stored DHW
|
|
1244
|
+
|
|
1185
1245
|
assert payload[4:6] in ZON_MODE_MAP, f"{payload[4:6]} (0xjj)"
|
|
1186
|
-
assert (
|
|
1187
|
-
|
|
1188
|
-
)
|
|
1189
|
-
assert (
|
|
1190
|
-
|
|
1191
|
-
)
|
|
1192
|
-
assert (
|
|
1193
|
-
payload[6:12]
|
|
1194
|
-
)
|
|
1246
|
+
assert payload[4:6] == ZON_MODE_MAP.TEMPORARY or msg.len == 6, (
|
|
1247
|
+
f"{msg!r}: expected length 6"
|
|
1248
|
+
)
|
|
1249
|
+
assert payload[4:6] != ZON_MODE_MAP.TEMPORARY or msg.len == 12, (
|
|
1250
|
+
f"{msg!r}: expected length 12"
|
|
1251
|
+
)
|
|
1252
|
+
assert payload[6:12] == "FFFFFF", (
|
|
1253
|
+
f"{msg!r}: expected FFFFFF instead of '{payload[6:12]}'"
|
|
1254
|
+
)
|
|
1195
1255
|
|
|
1196
|
-
result = {SZ_MODE: ZON_MODE_MAP.get(payload[4:6])}
|
|
1256
|
+
result: PayDictT._1F41 = {SZ_MODE: ZON_MODE_MAP.get(payload[4:6])} # type: ignore[typeddict-item]
|
|
1197
1257
|
if payload[2:4] != "FF":
|
|
1198
|
-
result[
|
|
1258
|
+
result[SZ_ACTIVE] = {"00": False, "01": True, "FF": None}[payload[2:4]]
|
|
1199
1259
|
# if payload[4:6] == ZON_MODE_MAP.COUNTDOWN:
|
|
1200
1260
|
# result[SZ_UNTIL] = dtm_from_hex(payload[6:12])
|
|
1201
1261
|
if payload[4:6] == ZON_MODE_MAP.TEMPORARY:
|
|
1202
|
-
result[SZ_UNTIL] =
|
|
1262
|
+
result[SZ_UNTIL] = hex_to_dtm(payload[12:24])
|
|
1203
1263
|
|
|
1204
1264
|
return result
|
|
1205
1265
|
|
|
1206
1266
|
|
|
1207
|
-
|
|
1208
|
-
def parser_1f70(payload, msg) -> dict:
|
|
1267
|
+
# programme_config, HVAC
|
|
1268
|
+
def parser_1f70(payload: str, msg: Message) -> dict[str, Any]:
|
|
1209
1269
|
# Seen on Orcon: see 1470, 1F70, 22B0
|
|
1210
1270
|
|
|
1211
1271
|
try:
|
|
@@ -1221,8 +1281,8 @@ def parser_1f70(payload, msg) -> dict:
|
|
|
1221
1281
|
assert msg.verb == RP or payload[26:] == "000000"
|
|
1222
1282
|
assert msg.verb != RP or payload[26:] == "008000"
|
|
1223
1283
|
|
|
1224
|
-
except AssertionError as
|
|
1225
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({
|
|
1284
|
+
except AssertionError as err:
|
|
1285
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
1226
1286
|
|
|
1227
1287
|
# assert int(payload[16:18], 16) < 7, _INFORM_DEV_MSG
|
|
1228
1288
|
|
|
@@ -1231,58 +1291,30 @@ def parser_1f70(payload, msg) -> dict:
|
|
|
1231
1291
|
"setpoint_idx": payload[8:10], # needs to be mod 1470[3:4]?
|
|
1232
1292
|
"start_time": f"{int(payload[18:20], 16):02d}:{int(payload[20:22], 16):02d}",
|
|
1233
1293
|
"fan_speed_wip": payload[24:26], # # E4/E5/E6 / 00(RQ)
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1294
|
+
"_value_02": payload[2:4], # # 00/01 / 00(RQ)
|
|
1295
|
+
"_value_04": payload[4:8], # # 0800
|
|
1296
|
+
"_value_10": payload[10:14], # 0000
|
|
1297
|
+
"_value_14": payload[14:16], # 15(RP,I) / 00(RQ,W)
|
|
1298
|
+
"_value_22": payload[22:24], # 60 / 00(RQ)
|
|
1299
|
+
"_value_26": payload[26:], # # 008000(RP) / 000000(I/RQ/W)
|
|
1240
1300
|
}
|
|
1241
1301
|
|
|
1242
1302
|
|
|
1243
|
-
|
|
1244
|
-
def parser_1fc9(payload, msg) ->
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
# .I is missing?
|
|
1248
|
-
# .W --- 10:048122 01:145038 --:------ 1FC9 006 003EF028BBFA
|
|
1249
|
-
# .I --- 01:145038 10:048122 --:------ 1FC9 006 00FFFF06368E
|
|
1250
|
-
|
|
1251
|
-
# .I --- 07:045960 --:------ 07:045960 1FC9 012 0012601CB388001FC91CB388
|
|
1252
|
-
# .W --- 01:145038 07:045960 --:------ 1FC9 006 0010A006368E
|
|
1253
|
-
# .I --- 07:045960 01:145038 --:------ 1FC9 006 0012601CB388
|
|
1254
|
-
|
|
1255
|
-
# .I --- 01:145038 --:------ 01:145038 1FC9 018 FA000806368EFC3B0006368EFA1FC906368E
|
|
1256
|
-
# .W --- 13:081807 01:145038 --:------ 1FC9 006 003EF0353F8F
|
|
1257
|
-
# .I --- 01:145038 13:081807 --:------ 1FC9 006 00FFFF06368E
|
|
1258
|
-
|
|
1259
|
-
# this is an array of codes
|
|
1260
|
-
# 049 I --- 01:145038 --:------ 01:145038 1FC9 018 07-0008-06368E FC-3B00-06368E 07-1FC9-06368E # noqa: E501
|
|
1261
|
-
# 047 I --- 01:145038 --:------ 01:145038 1FC9 018 FA-0008-06368E FC-3B00-06368E FA-1FC9-06368E # noqa: E501
|
|
1262
|
-
# 065 I --- 01:145038 --:------ 01:145038 1FC9 024 FC-0008-06368E FC-3150-06368E FB-3150-06368E FC-1FC9-06368E # noqa: E501
|
|
1263
|
-
|
|
1264
|
-
# HW valve binding:
|
|
1265
|
-
# 063 I --- 01:145038 --:------ 01:145038 1FC9 018 FA-0008-06368E FC-3B00-06368E FA-1FC9-06368E # noqa: E501
|
|
1266
|
-
# CH valve binding:
|
|
1267
|
-
# 071 I --- 01:145038 --:------ 01:145038 1FC9 018 F9-0008-06368E FC-3B00-06368E F9-1FC9-06368E # noqa: E501
|
|
1268
|
-
# ZoneValve zone binding
|
|
1269
|
-
# 045 W --- 13:106039 01:145038 --:------ 1FC9 012 00-3EF0-359E37 00-3B00-359E37
|
|
1270
|
-
# DHW binding..
|
|
1271
|
-
# 045 W --- 13:163733 01:145038 --:------ 1FC9 012 00-3EF0-367F95 00-3B00-367F95
|
|
1272
|
-
|
|
1273
|
-
# 049 I --- 01:145038 --:------ 01:145038 1FC9 018 F9-0008-06368E FC-3B00-06368E F9-1FC9-06368E # noqa: E501
|
|
1274
|
-
|
|
1275
|
-
# the new (heatpump-aware) BDR91:
|
|
1276
|
-
# 045 RP --- 13:035462 18:013393 --:------ 1FC9 018 00-3EF0-348A86 00-11F0-348A86 90-7FE1-DD6ABD # noqa: E501
|
|
1277
|
-
|
|
1278
|
-
def _parser(seqx) -> list:
|
|
1303
|
+
# rf_bind
|
|
1304
|
+
def parser_1fc9(payload: str, msg: Message) -> PayDictT._1FC9:
|
|
1305
|
+
def _parser(seqx: str) -> list[str]:
|
|
1279
1306
|
if seqx[:2] not in ("90",):
|
|
1280
|
-
assert
|
|
1307
|
+
assert (
|
|
1308
|
+
seqx[6:] == payload[6:12] # [6:12] is repeated
|
|
1309
|
+
), f"{seqx[6:]} != {payload[6:12]}" # all with same controller
|
|
1281
1310
|
if seqx[:2] not in (
|
|
1282
|
-
"
|
|
1283
|
-
"
|
|
1284
|
-
"
|
|
1285
|
-
"
|
|
1311
|
+
"21", # HVAC, Nuaire
|
|
1312
|
+
"63", # HVAC
|
|
1313
|
+
"65", # HVAC, ClimaRad
|
|
1314
|
+
"66", # HVAC, Vasco
|
|
1315
|
+
"67", # HVAC
|
|
1316
|
+
"6C", # HVAC
|
|
1317
|
+
"90", # HEAT
|
|
1286
1318
|
F6,
|
|
1287
1319
|
F9,
|
|
1288
1320
|
FA,
|
|
@@ -1290,20 +1322,22 @@ def parser_1fc9(payload, msg) -> list:
|
|
|
1290
1322
|
FC,
|
|
1291
1323
|
FF,
|
|
1292
1324
|
): # or: not in DOMAIN_TYPE_MAP: ??
|
|
1293
|
-
assert int(seqx[:2], 16) < 16
|
|
1325
|
+
assert int(seqx[:2], 16) < 16, _INFORM_DEV_MSG
|
|
1294
1326
|
return [seqx[:2], seqx[2:6], hex_id_to_dev_id(seqx[6:])]
|
|
1295
1327
|
|
|
1296
|
-
if msg.verb == I_ and msg.
|
|
1297
|
-
|
|
1328
|
+
if msg.verb == I_ and msg.dst.id in (msg.src.id, ALL_DEV_ADDR.id):
|
|
1329
|
+
bind_phase = SZ_OFFER
|
|
1298
1330
|
elif msg.verb == W_ and msg.src is not msg.dst:
|
|
1299
|
-
|
|
1331
|
+
bind_phase = SZ_ACCEPT
|
|
1300
1332
|
elif msg.verb == I_:
|
|
1301
|
-
|
|
1333
|
+
bind_phase = SZ_CONFIRM # len(payload) could be 2 (e.g. 00, 21)
|
|
1334
|
+
elif msg.verb == RP:
|
|
1335
|
+
bind_phase = None
|
|
1302
1336
|
else:
|
|
1303
|
-
|
|
1337
|
+
raise exc.PacketPayloadInvalid("Unknown binding format")
|
|
1304
1338
|
|
|
1305
|
-
if payload ==
|
|
1306
|
-
return {
|
|
1339
|
+
if len(payload) == 2 and bind_phase == SZ_CONFIRM:
|
|
1340
|
+
return {SZ_PHASE: bind_phase, SZ_BINDINGS: [[payload]]} # double-bracket OK
|
|
1307
1341
|
|
|
1308
1342
|
assert msg.len >= 6 and msg.len % 6 == 0, msg.len # assuming not RQ
|
|
1309
1343
|
assert msg.verb in (I_, W_, RP), msg.verb # devices will respond to a RQ!
|
|
@@ -1315,24 +1349,23 @@ def parser_1fc9(payload, msg) -> list:
|
|
|
1315
1349
|
for i in range(0, len(payload), 12)
|
|
1316
1350
|
# if payload[i : i + 2] != "90" # TODO: WIP, what is 90?
|
|
1317
1351
|
]
|
|
1318
|
-
return {
|
|
1352
|
+
return {SZ_PHASE: bind_phase, SZ_BINDINGS: bindings}
|
|
1319
1353
|
|
|
1320
1354
|
|
|
1321
|
-
|
|
1322
|
-
def parser_1fca(payload, msg) ->
|
|
1355
|
+
# unknown_1fca, HVAC?
|
|
1356
|
+
def parser_1fca(payload: str, msg: Message) -> Mapping[str, str]:
|
|
1323
1357
|
# .W --- 30:248208 34:021943 --:------ 1FCA 009 00-01FF-7BC990-FFFFFF # sent x2
|
|
1324
1358
|
|
|
1325
1359
|
return {
|
|
1326
|
-
|
|
1327
|
-
|
|
1360
|
+
"_unknown_0": payload[:2],
|
|
1361
|
+
"_unknown_1": payload[2:6],
|
|
1328
1362
|
"device_id_0": hex_id_to_dev_id(payload[6:12]),
|
|
1329
1363
|
"device_id_1": hex_id_to_dev_id(payload[12:]),
|
|
1330
1364
|
}
|
|
1331
1365
|
|
|
1332
1366
|
|
|
1333
|
-
|
|
1334
|
-
def parser_1fd0(payload, msg) -> dict:
|
|
1335
|
-
|
|
1367
|
+
# unknown_1fd0, from OTB
|
|
1368
|
+
def parser_1fd0(payload: str, msg: Message) -> dict[str, Any]:
|
|
1336
1369
|
assert payload == "0000000000000000", _INFORM_DEV_MSG
|
|
1337
1370
|
|
|
1338
1371
|
return {
|
|
@@ -1340,35 +1373,51 @@ def parser_1fd0(payload, msg) -> dict:
|
|
|
1340
1373
|
}
|
|
1341
1374
|
|
|
1342
1375
|
|
|
1343
|
-
|
|
1344
|
-
def parser_1fd4(payload, msg) ->
|
|
1376
|
+
# opentherm_sync, otb_sync
|
|
1377
|
+
def parser_1fd4(payload: str, msg: Message) -> PayDictT._1FD4:
|
|
1345
1378
|
return {"ticker": int(payload[2:], 16)}
|
|
1346
1379
|
|
|
1347
1380
|
|
|
1348
|
-
|
|
1349
|
-
def parser_2210(payload, msg) -> dict:
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1381
|
+
# WIP: unknown, HVAC
|
|
1382
|
+
def parser_2210(payload: str, msg: Message) -> dict[str, Any]:
|
|
1383
|
+
try:
|
|
1384
|
+
assert msg.verb in (RP, I_) or payload == "00"
|
|
1385
|
+
assert payload[10:12] == payload[38:40] and payload[10:12] in (
|
|
1386
|
+
"58",
|
|
1387
|
+
"96",
|
|
1388
|
+
"FF",
|
|
1389
|
+
), f"expected (58|96|FF), not {payload[10:12]}"
|
|
1390
|
+
assert payload[20:22] == payload[48:50] and payload[20:22] in (
|
|
1391
|
+
"00",
|
|
1392
|
+
"03",
|
|
1393
|
+
), f"expected (00|03), not {payload[10:12]}"
|
|
1394
|
+
assert payload[78:80] in ("00", "02"), f"expected (00|02), not {payload[78:80]}"
|
|
1395
|
+
assert payload[80:82] in ("01", "08"), f"expected (01|08), not {payload[80:82]}"
|
|
1396
|
+
assert payload[82:] in ("00", "40"), f"expected (00|40), not {payload[82:]}"
|
|
1397
|
+
|
|
1398
|
+
except AssertionError as err:
|
|
1399
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
1357
1400
|
|
|
1358
|
-
return {
|
|
1401
|
+
return {
|
|
1402
|
+
"unknown_10": payload[10:12],
|
|
1403
|
+
"unknown_20": payload[20:22],
|
|
1404
|
+
"unknown_78": payload[78:80],
|
|
1405
|
+
"unknown_80": payload[80:82],
|
|
1406
|
+
"unknown_82": payload[82:],
|
|
1407
|
+
}
|
|
1359
1408
|
|
|
1360
1409
|
|
|
1361
|
-
|
|
1362
|
-
def parser_2249(payload, msg) -> dict:
|
|
1410
|
+
# now_next_setpoint - Programmer/Hometronics
|
|
1411
|
+
def parser_2249(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
1363
1412
|
# see: https://github.com/jrosser/honeymon/blob/master/decoder.cpp#L357-L370
|
|
1364
1413
|
# .I --- 23:100224 --:------ 23:100224 2249 007 00-7EFF-7EFF-FFFF
|
|
1365
1414
|
|
|
1366
|
-
def _parser(seqx) -> dict:
|
|
1415
|
+
def _parser(seqx: str) -> dict[str, bool | float | int | str | None]:
|
|
1367
1416
|
minutes = int(seqx[10:], 16)
|
|
1368
1417
|
next_setpoint = msg.dtm + td(minutes=minutes)
|
|
1369
1418
|
return {
|
|
1370
|
-
"setpoint_now":
|
|
1371
|
-
"setpoint_next":
|
|
1419
|
+
"setpoint_now": hex_to_temp(seqx[2:6]),
|
|
1420
|
+
"setpoint_next": hex_to_temp(seqx[6:10]),
|
|
1372
1421
|
"minutes_remaining": minutes,
|
|
1373
1422
|
"_next_setpoint": dt.strftime(next_setpoint, "%H:%M:%S"),
|
|
1374
1423
|
}
|
|
@@ -1386,14 +1435,14 @@ def parser_2249(payload, msg) -> dict:
|
|
|
1386
1435
|
return _parser(payload)
|
|
1387
1436
|
|
|
1388
1437
|
|
|
1389
|
-
|
|
1390
|
-
def parser_22b0(payload, msg) -> dict:
|
|
1438
|
+
# program_enabled, HVAC
|
|
1439
|
+
def parser_22b0(payload: str, msg: Message) -> dict[str, Any]:
|
|
1391
1440
|
# Seen on Orcon: see 1470, 1F70, 22B0
|
|
1392
1441
|
|
|
1393
|
-
# .W --- 37:171871 32:155617 --:------ 22B0 002 0005 # enable
|
|
1442
|
+
# .W --- 37:171871 32:155617 --:------ 22B0 002 0005 # enable, calendar on
|
|
1394
1443
|
# .I --- 32:155617 37:171871 --:------ 22B0 002 0005
|
|
1395
1444
|
|
|
1396
|
-
# .W --- 37:171871 32:155617 --:------ 22B0 002 0006 # disable
|
|
1445
|
+
# .W --- 37:171871 32:155617 --:------ 22B0 002 0006 # disable, calendar off
|
|
1397
1446
|
# .I --- 32:155617 37:171871 --:------ 22B0 002 0006
|
|
1398
1447
|
|
|
1399
1448
|
return {
|
|
@@ -1401,23 +1450,24 @@ def parser_22b0(payload, msg) -> dict:
|
|
|
1401
1450
|
}
|
|
1402
1451
|
|
|
1403
1452
|
|
|
1404
|
-
|
|
1405
|
-
def parser_22c9(payload, msg) -> list:
|
|
1453
|
+
# setpoint_bounds, TODO: max length = 24?
|
|
1454
|
+
def parser_22c9(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
1406
1455
|
# .I --- 02:001107 --:------ 02:001107 22C9 024 00-0834-0A28-01-0108340A2801-0208340A2801-0308340A2801 # noqa: E501
|
|
1407
1456
|
# .I --- 02:001107 --:------ 02:001107 22C9 006 04-0834-0A28-01
|
|
1408
1457
|
|
|
1409
1458
|
# .I --- 21:064743 --:------ 21:064743 22C9 006 00-07D0-0834-02
|
|
1410
1459
|
# .W --- 21:064743 02:250708 --:------ 22C9 006 03-07D0-0834-02
|
|
1411
|
-
# .I --- 02:250708 21:064743 --:------ 22C9 008 03-07D0-7FFF-
|
|
1460
|
+
# .I --- 02:250708 21:064743 --:------ 22C9 008 03-07D0-7FFF-020203
|
|
1461
|
+
|
|
1462
|
+
# Notes on 008|suffix: only seen as I, only when no array, only as 7FFF(0101|0202)03$
|
|
1412
1463
|
|
|
1413
|
-
def _parser(seqx) -> dict:
|
|
1414
|
-
assert seqx[10:] in ("01", "02"), f"is {seqx[10:]}, expecting 01
|
|
1464
|
+
def _parser(seqx: str) -> dict:
|
|
1465
|
+
assert seqx[10:] in ("01", "02"), f"is {seqx[10:]}, expecting 01 or 02"
|
|
1415
1466
|
|
|
1416
1467
|
return {
|
|
1417
|
-
"
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
}
|
|
1468
|
+
SZ_MODE: {"01": "heat", "02": "cool"}[seqx[10:]], # TODO: or action?
|
|
1469
|
+
SZ_SETPOINT_BOUNDS: (hex_to_temp(seqx[2:6]), hex_to_temp(seqx[6:10])),
|
|
1470
|
+
} # lower, upper setpoints
|
|
1421
1471
|
|
|
1422
1472
|
if msg._has_array:
|
|
1423
1473
|
return [
|
|
@@ -1428,114 +1478,91 @@ def parser_22c9(payload, msg) -> list:
|
|
|
1428
1478
|
for i in range(0, len(payload), 12)
|
|
1429
1479
|
]
|
|
1430
1480
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1481
|
+
assert msg.len != 8 or payload[10:] in ("010103", "020203"), _INFORM_DEV_MSG
|
|
1433
1482
|
|
|
1434
|
-
|
|
1435
|
-
def parser_22d0(payload, msg) -> dict:
|
|
1483
|
+
return _parser(payload[:12])
|
|
1436
1484
|
|
|
1437
|
-
# When closing H/C contact (or enabling cooling mode with buttons when H/C contact is closed) on HCE80 it sends following packet:
|
|
1438
|
-
# .I — 02:044994 --:------ 02:044994 22D0 004 0010000A < AssertionError…
|
|
1439
1485
|
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1486
|
+
# unknown_22d0, UFH system mode (heat/cool)
|
|
1487
|
+
def parser_22d0(payload: str, msg: Message) -> dict[str, Any]:
|
|
1488
|
+
def _parser(seqx: str) -> dict:
|
|
1489
|
+
# assert seqx[2:4] in ("00", "03", "10", "13", "14"), _INFORM_DEV_MSG
|
|
1490
|
+
assert seqx[4:6] == "00", _INFORM_DEV_MSG
|
|
1491
|
+
return {
|
|
1492
|
+
"idx": seqx[:2],
|
|
1493
|
+
"_flags": hex_to_flag8(seqx[2:4]),
|
|
1494
|
+
"cool_mode": bool(int(seqx[2:4], 16) & 0x02),
|
|
1495
|
+
"heat_mode": bool(int(seqx[2:4], 16) & 0x04),
|
|
1496
|
+
"is_active": bool(int(seqx[2:4], 16) & 0x10),
|
|
1497
|
+
"_unknown": payload[4:],
|
|
1498
|
+
}
|
|
1448
1499
|
|
|
1449
|
-
|
|
1450
|
-
"
|
|
1451
|
-
|
|
1452
|
-
"
|
|
1453
|
-
"0314001E14030020",
|
|
1454
|
-
), _INFORM_DEV_MSG
|
|
1500
|
+
if len(payload) == 8:
|
|
1501
|
+
assert payload[6:] in ("00", "02", "0A"), _INFORM_DEV_MSG
|
|
1502
|
+
else:
|
|
1503
|
+
assert payload[4:] == "001E14030020", _INFORM_DEV_MSG
|
|
1455
1504
|
|
|
1456
|
-
return
|
|
1457
|
-
"idx": payload[:2],
|
|
1458
|
-
SZ_PAYLOAD: payload[2:],
|
|
1459
|
-
}
|
|
1505
|
+
return _parser(payload)
|
|
1460
1506
|
|
|
1461
1507
|
|
|
1462
|
-
|
|
1463
|
-
def parser_22d9(payload, msg) ->
|
|
1464
|
-
return {SZ_SETPOINT:
|
|
1508
|
+
# desired boiler setpoint
|
|
1509
|
+
def parser_22d9(payload: str, msg: Message) -> PayDictT._22D9:
|
|
1510
|
+
return {SZ_SETPOINT: hex_to_temp(payload[2:6])}
|
|
1465
1511
|
|
|
1466
1512
|
|
|
1467
|
-
|
|
1468
|
-
def parser_22e0(payload, msg) ->
|
|
1513
|
+
# WIP: unknown, HVAC
|
|
1514
|
+
def parser_22e0(payload: str, msg: Message) -> Mapping[str, float | None]:
|
|
1469
1515
|
# RP --- 32:155617 18:005904 --:------ 22E0 004 00-34-A0-1E
|
|
1470
1516
|
# RP --- 32:153258 18:005904 --:------ 22E0 004 00-64-A0-1E
|
|
1471
|
-
def _parser(seqx) ->
|
|
1517
|
+
def _parser(seqx: str) -> float:
|
|
1472
1518
|
assert int(seqx, 16) <= 200 or seqx == "E6" # only for 22E0, not 22E5/22E9
|
|
1473
1519
|
return int(seqx, 16) / 200
|
|
1474
1520
|
|
|
1475
1521
|
try:
|
|
1476
1522
|
return {
|
|
1477
|
-
f"percent_{i}":
|
|
1523
|
+
f"percent_{i}": hex_to_percent(payload[i : i + 2])
|
|
1478
1524
|
for i in range(2, len(payload), 2)
|
|
1479
1525
|
}
|
|
1480
1526
|
except ValueError:
|
|
1481
1527
|
return {
|
|
1482
|
-
"percent_2":
|
|
1528
|
+
"percent_2": hex_to_percent(payload[2:4]),
|
|
1483
1529
|
"percent_4": _parser(payload[4:6]),
|
|
1484
|
-
"percent_6":
|
|
1530
|
+
"percent_6": hex_to_percent(payload[6:8]),
|
|
1485
1531
|
}
|
|
1486
1532
|
|
|
1487
1533
|
|
|
1488
|
-
|
|
1489
|
-
def parser_22e5(payload, msg) ->
|
|
1534
|
+
# WIP: unknown, HVAC
|
|
1535
|
+
def parser_22e5(payload: str, msg: Message) -> Mapping[str, float | None]:
|
|
1490
1536
|
# RP --- 32:153258 18:005904 --:------ 22E5 004 00-96-C8-14
|
|
1491
1537
|
# RP --- 32:155617 18:005904 --:------ 22E5 004 00-72-C8-14
|
|
1492
1538
|
|
|
1493
1539
|
return parser_22e0(payload, msg)
|
|
1494
1540
|
|
|
1495
1541
|
|
|
1496
|
-
|
|
1497
|
-
def parser_22e9(payload, msg) ->
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1542
|
+
# WIP: unknown, HVAC
|
|
1543
|
+
def parser_22e9(payload: str, msg: Message) -> Mapping[str, float | str | None]:
|
|
1544
|
+
if payload[2:4] == "01":
|
|
1545
|
+
return {
|
|
1546
|
+
"unknown_4": payload[4:6],
|
|
1547
|
+
"unknown_6": payload[6:8],
|
|
1548
|
+
}
|
|
1501
1549
|
return parser_22e0(payload, msg)
|
|
1502
1550
|
|
|
1503
1551
|
|
|
1504
|
-
|
|
1505
|
-
def parser_22f1(payload, msg) -> dict:
|
|
1506
|
-
# Orcon wireless remote 15RF
|
|
1507
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000007 # Absent mode // Afwezig (absence mode, aka: weg/away) - low & doesn't respond to sensors
|
|
1508
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000107 # Mode 1: Low // Stand 1 (position low)
|
|
1509
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000207 # Mode 2: Med // Stand 2 (position med)
|
|
1510
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000307 # Mode 3: High // Stand 3 (position high)
|
|
1511
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000407 # Auto
|
|
1512
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000507 # Auto
|
|
1513
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000607 # Party/boost
|
|
1514
|
-
# .I --- 37:171871 32:155617 --:------ 22F1 003 000707 # Off
|
|
1515
|
-
|
|
1516
|
-
# .I 015 --:------ --:------ 39:159057 22F1 003 000004 # TBA: off/standby?
|
|
1517
|
-
# .I 015 --:------ --:------ 39:159057 22F1 003 000104 # TBA: trickle/min-speed?
|
|
1518
|
-
# .I 015 --:------ --:------ 39:159057 22F1 003 000204 # low
|
|
1519
|
-
# .I 016 --:------ --:------ 39:159057 22F1 003 000304 # medium
|
|
1520
|
-
# .I 017 --:------ --:------ 39:159057 22F1 003 000404 # high (aka boost if timer)
|
|
1521
|
-
|
|
1522
|
-
# Scheme x: 0|x standby/off, 1|x min, 2+|x rate as % of max (Itho?)
|
|
1523
|
-
# Scheme 4: 0|4 standby/off, 1|4 auto, 2|4 low, 3|4 med, 4|4 high/boost
|
|
1524
|
-
# Scheme 7: only seen 000[2345]07 -- ? off, auto, rate x/4, +3 others?
|
|
1525
|
-
# Scheme A: only seen 000[239A]0A -- Normal, Boost (purge), HeaterOff & HeaterAuto
|
|
1526
|
-
|
|
1552
|
+
# fan_speed (switch_mode), HVAC
|
|
1553
|
+
def parser_22f1(payload: str, msg: Message) -> dict[str, Any]:
|
|
1527
1554
|
try:
|
|
1528
1555
|
assert payload[0:2] in ("00", "63")
|
|
1529
|
-
assert not payload[4:] or int(payload[2:4], 16) <= int(
|
|
1530
|
-
|
|
1531
|
-
)
|
|
1532
|
-
except AssertionError as
|
|
1533
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({
|
|
1556
|
+
assert not payload[4:] or int(payload[2:4], 16) <= int(payload[4:], 16), (
|
|
1557
|
+
"mode_idx > mode_max"
|
|
1558
|
+
)
|
|
1559
|
+
except AssertionError as err:
|
|
1560
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
1534
1561
|
|
|
1535
1562
|
if msg._addrs[0] == NON_DEV_ADDR: # and payload[4:6] == "04":
|
|
1536
1563
|
from .ramses import _22F1_MODE_ITHO as _22F1_FAN_MODE # TODO: only if 04
|
|
1537
1564
|
|
|
1538
|
-
_22f1_mode_set = ("", "04")
|
|
1565
|
+
_22f1_mode_set: tuple[str, ...] = ("", "04")
|
|
1539
1566
|
_22f1_scheme = "itho"
|
|
1540
1567
|
|
|
1541
1568
|
# elif msg._addrs[0] == NON_DEV_ADDR: # and payload[4:6] == "04":
|
|
@@ -1552,6 +1579,16 @@ def parser_22f1(payload, msg) -> dict:
|
|
|
1552
1579
|
_22f1_mode_set = ("", "0A")
|
|
1553
1580
|
_22f1_scheme = "nuaire"
|
|
1554
1581
|
|
|
1582
|
+
elif payload[4:6] == "06":
|
|
1583
|
+
from .ramses import _22F1_MODE_VASCO as _22F1_FAN_MODE
|
|
1584
|
+
|
|
1585
|
+
_22f1_mode_set = (
|
|
1586
|
+
"",
|
|
1587
|
+
"00",
|
|
1588
|
+
"06",
|
|
1589
|
+
) # "00" seen incidentally on a ClimaRad 4-button remote: OFF?
|
|
1590
|
+
_22f1_scheme = "vasco"
|
|
1591
|
+
|
|
1555
1592
|
else:
|
|
1556
1593
|
from .ramses import _22F1_MODE_ORCON as _22F1_FAN_MODE
|
|
1557
1594
|
|
|
@@ -1561,8 +1598,8 @@ def parser_22f1(payload, msg) -> dict:
|
|
|
1561
1598
|
try:
|
|
1562
1599
|
assert payload[2:4] in _22F1_FAN_MODE, f"unknown fan_mode: {payload[2:4]}"
|
|
1563
1600
|
assert payload[4:6] in _22f1_mode_set, f"unknown mode_set: {payload[4:6]}"
|
|
1564
|
-
except AssertionError as
|
|
1565
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({
|
|
1601
|
+
except AssertionError as err:
|
|
1602
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
1566
1603
|
|
|
1567
1604
|
return {
|
|
1568
1605
|
SZ_FAN_MODE: _22F1_FAN_MODE.get(payload[2:4], f"unknown_{payload[2:4]}"),
|
|
@@ -1573,69 +1610,59 @@ def parser_22f1(payload, msg) -> dict:
|
|
|
1573
1610
|
}
|
|
1574
1611
|
|
|
1575
1612
|
|
|
1576
|
-
|
|
1577
|
-
def parser_22f2(payload, msg) -> dict
|
|
1578
|
-
#
|
|
1579
|
-
# RP --- 32:155617 18:005904 --:------ 22F2 006 00-0174 01-0208
|
|
1580
|
-
# RP --- 32:155617 18:005904 --:------ 22F2 006 00-01E5 01-0201
|
|
1613
|
+
# WIP: unknown, HVAC (flow rate?)
|
|
1614
|
+
def parser_22f2(payload: str, msg: Message) -> list: # TODO: only dict
|
|
1615
|
+
# ClimeRad minibox uses 22F2 for speed feedback
|
|
1581
1616
|
|
|
1582
|
-
def _parser(seqx) -> dict:
|
|
1617
|
+
def _parser(seqx: str) -> dict:
|
|
1583
1618
|
assert seqx[:2] in ("00", "01"), f"is {seqx[:2]}, expecting 00/01"
|
|
1584
1619
|
|
|
1585
1620
|
return {
|
|
1586
1621
|
"hvac_idx": seqx[:2],
|
|
1587
|
-
"measure":
|
|
1622
|
+
"measure": hex_to_temp(seqx[2:]),
|
|
1588
1623
|
}
|
|
1589
1624
|
|
|
1590
1625
|
return [_parser(payload[i : i + 6]) for i in range(0, len(payload), 6)]
|
|
1591
1626
|
|
|
1592
1627
|
|
|
1593
|
-
|
|
1594
|
-
def parser_22f3(payload, msg) -> dict:
|
|
1595
|
-
# .I 019 --:------ --:------ 39:159057 22F3 003 00000A # 10 mins
|
|
1596
|
-
# .I 022 --:------ --:------ 39:159057 22F3 003 000014 # 20 mins
|
|
1597
|
-
# .I 026 --:------ --:------ 39:159057 22F3 003 00001E # 30 mins
|
|
1598
|
-
# .I --- 29:151550 29:237552 --:------ 22F3 007 00023C-0304-0000 # 60 mins
|
|
1599
|
-
# .I --- 29:162374 29:237552 --:------ 22F3 007 00020F-0304-0000 # 15 mins
|
|
1600
|
-
# .I --- 29:162374 29:237552 --:------ 22F3 007 00020F-0304-0000 # 15 mins
|
|
1601
|
-
|
|
1628
|
+
# fan_boost, HVAC
|
|
1629
|
+
def parser_22f3(payload: str, msg: Message) -> dict[str, Any]:
|
|
1602
1630
|
# NOTE: for boost timer for high
|
|
1603
1631
|
try:
|
|
1604
|
-
# assert payload[2:4] in ("00", "02", "12", "x52"), f"byte 1: {flag8(payload[2:4])}"
|
|
1605
1632
|
assert msg.len <= 7 or payload[14:] == "0000", f"byte 7: {payload[14:]}"
|
|
1606
|
-
except AssertionError as
|
|
1607
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({
|
|
1633
|
+
except AssertionError as err:
|
|
1634
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
1608
1635
|
|
|
1609
1636
|
new_speed = { # from now, until timer expiry
|
|
1610
1637
|
0x00: "fan_boost", # # set fan off, or 'boost' mode?
|
|
1611
1638
|
0x01: "per_request", # # set fan as per payload[6:10]?
|
|
1612
1639
|
0x02: "per_vent_speed", # set fan as per current fan mode/speed?
|
|
1613
|
-
}.get(
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1640
|
+
}.get(int(payload[2:4], 0x10) & 0x07) # 0b0000-0111
|
|
1641
|
+
|
|
1642
|
+
fallback_speed: str | None
|
|
1643
|
+
if msg.len == 7 and payload[9:10] == "06": # Vasco and ClimaRad REM
|
|
1644
|
+
fallback_speed = "per_vent_speed" # after timer expiry
|
|
1645
|
+
# set fan as per current fan mode/speed
|
|
1646
|
+
else:
|
|
1647
|
+
fallback_speed = { # after timer expiry
|
|
1648
|
+
0x08: "fan_off", # # set fan off?
|
|
1649
|
+
0x10: "per_request", # # set fan as per payload[6:10], or payload[10:]?
|
|
1650
|
+
0x18: "per_vent_speed", # set fan as per current fan mode/speed?
|
|
1651
|
+
}.get(int(payload[2:4], 0x10) & 0x38) # 0b0011-1000
|
|
1624
1652
|
|
|
1625
1653
|
units = {
|
|
1626
1654
|
0x00: "minutes",
|
|
1627
1655
|
0x40: "hours",
|
|
1628
1656
|
0x80: "index", # TODO: days, day-of-week, day-of-month?
|
|
1629
|
-
}.get(
|
|
1630
|
-
int(payload[2:4], 0x10) & 0xC0
|
|
1631
|
-
) # 0b1100-0000
|
|
1657
|
+
}.get(int(payload[2:4], 0x10) & 0xC0) # 0b1100-0000
|
|
1632
1658
|
|
|
1633
1659
|
duration = int(payload[4:6], 16) * 60 if units == "hours" else int(payload[4:6], 16)
|
|
1660
|
+
result = {}
|
|
1634
1661
|
|
|
1635
1662
|
if msg.len >= 3:
|
|
1636
1663
|
result = {
|
|
1637
1664
|
"minutes" if units != "index" else "index": duration,
|
|
1638
|
-
"flags":
|
|
1665
|
+
"flags": hex_to_flag8(payload[2:4]),
|
|
1639
1666
|
"_new_speed_mode": new_speed,
|
|
1640
1667
|
"_fallback_speed_mode": fallback_speed,
|
|
1641
1668
|
}
|
|
@@ -1644,49 +1671,59 @@ def parser_22f3(payload, msg) -> dict:
|
|
|
1644
1671
|
result["rate"] = parser_22f1(f"00{payload[6:10]}", msg).get("rate")
|
|
1645
1672
|
|
|
1646
1673
|
if msg.len >= 7: # fallback speed?
|
|
1647
|
-
result.update({
|
|
1674
|
+
result.update({"_unknown_5": payload[10:]})
|
|
1648
1675
|
|
|
1649
1676
|
return result
|
|
1650
1677
|
|
|
1651
1678
|
|
|
1652
|
-
|
|
1653
|
-
def parser_22f4(payload, msg) -> dict:
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
assert payload[6:] == "00000000000000200000"
|
|
1679
|
+
# WIP: unknown, HVAC
|
|
1680
|
+
def parser_22f4(payload: str, msg: Message) -> dict[str, Any]:
|
|
1681
|
+
if msg.len == 13 and payload[14:] == "000000000000":
|
|
1682
|
+
# ClimaRad Ventura fan & remote
|
|
1683
|
+
_pl = payload[:4] + payload[12:14] if payload[10:12] == "00" else payload[8:14]
|
|
1684
|
+
else:
|
|
1685
|
+
_pl = payload[:6]
|
|
1660
1686
|
|
|
1661
|
-
|
|
1662
|
-
"
|
|
1663
|
-
"
|
|
1687
|
+
MODE_LOOKUP = {
|
|
1688
|
+
0x00: "off",
|
|
1689
|
+
0x20: "paused",
|
|
1690
|
+
0x40: "auto",
|
|
1691
|
+
0x60: "manual",
|
|
1664
1692
|
}
|
|
1693
|
+
mode = int(_pl[2:4], 16) & 0x60
|
|
1694
|
+
assert mode in MODE_LOOKUP, mode
|
|
1665
1695
|
|
|
1696
|
+
RATE_LOOKUP = {
|
|
1697
|
+
0x00: "speed 0", # "off"?,
|
|
1698
|
+
0x01: "speed 1", # "low", or trickle?
|
|
1699
|
+
0x02: "speed 2", # "medium-low", or low?
|
|
1700
|
+
0x03: "speed 3", # "medium",
|
|
1701
|
+
0x04: "speed 4", # "medium-high", or high?
|
|
1702
|
+
0x05: "boost", # "boost", aka purge?
|
|
1703
|
+
}
|
|
1704
|
+
rate = int(_pl[4:6], 16) & 0x03
|
|
1705
|
+
assert mode != 0x60 or rate in RATE_LOOKUP, rate
|
|
1666
1706
|
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1707
|
+
return {
|
|
1708
|
+
SZ_FAN_MODE: MODE_LOOKUP[mode],
|
|
1709
|
+
SZ_FAN_RATE: RATE_LOOKUP.get(rate),
|
|
1710
|
+
}
|
|
1671
1711
|
|
|
1672
|
-
# .W --- 37:171871 32:155617 --:------ 22F7 003 0000EF # bypass off
|
|
1673
|
-
# .I --- 32:155617 37:171871 --:------ 22F7 003 000000
|
|
1674
|
-
# .W --- 37:171871 32:155617 --:------ 22F7 003 00C8EF # bypass on
|
|
1675
|
-
# .I --- 32:155617 37:171871 --:------ 22F7 003 00C800
|
|
1676
|
-
# .W --- 37:171871 32:155617 --:------ 22F7 003 00FFEF # bypass auto
|
|
1677
|
-
# .I --- 32:155617 37:171871 --:------ 22F7 003 00FFC8
|
|
1678
1712
|
|
|
1713
|
+
# bypass_mode, HVAC
|
|
1714
|
+
def parser_22f7(payload: str, msg: Message) -> dict[str, Any]:
|
|
1679
1715
|
result = {
|
|
1680
|
-
|
|
1716
|
+
SZ_BYPASS_MODE: {"00": "off", "C8": "on", "FF": "auto"}.get(payload[2:4]),
|
|
1681
1717
|
}
|
|
1682
1718
|
if msg.verb != W_ or payload[4:] not in ("", "EF"):
|
|
1683
|
-
result[
|
|
1719
|
+
result[SZ_BYPASS_STATE] = {"00": "off", "C8": "on"}.get(payload[4:])
|
|
1720
|
+
result.update(**parse_bypass_position(payload[4:])) # type: ignore[arg-type]
|
|
1684
1721
|
|
|
1685
1722
|
return result
|
|
1686
1723
|
|
|
1687
1724
|
|
|
1688
|
-
|
|
1689
|
-
def parser_22f8(payload, msg) -> dict:
|
|
1725
|
+
# WIP: unknown_mode, HVAC
|
|
1726
|
+
def parser_22f8(payload: str, msg: Message) -> dict[str, Any]:
|
|
1690
1727
|
# from: https://github.com/arjenhiemstra/ithowifi/blob/master/software/NRG_itho_wifi/src/IthoPacket.h
|
|
1691
1728
|
|
|
1692
1729
|
# message command bytes specific for AUTO RFT (536-0150)
|
|
@@ -1703,14 +1740,15 @@ def parser_22f8(payload, msg) -> dict:
|
|
|
1703
1740
|
}
|
|
1704
1741
|
|
|
1705
1742
|
|
|
1706
|
-
|
|
1707
|
-
def parser_2309(
|
|
1708
|
-
|
|
1743
|
+
# setpoint (of device/zones)
|
|
1744
|
+
def parser_2309(
|
|
1745
|
+
payload: str, msg: Message
|
|
1746
|
+
) -> PayDictT._2309 | list[PayDictT._2309] | PayDictT.EMPTY:
|
|
1709
1747
|
if msg._has_array:
|
|
1710
1748
|
return [
|
|
1711
1749
|
{
|
|
1712
1750
|
SZ_ZONE_IDX: payload[i : i + 2],
|
|
1713
|
-
SZ_SETPOINT:
|
|
1751
|
+
SZ_SETPOINT: hex_to_temp(payload[i + 2 : i + 6]),
|
|
1714
1752
|
}
|
|
1715
1753
|
for i in range(0, len(payload), 6)
|
|
1716
1754
|
]
|
|
@@ -1719,11 +1757,11 @@ def parser_2309(payload, msg) -> dict | list:
|
|
|
1719
1757
|
if msg.verb == RQ and msg.len == 1: # some RQs have a payload (why?)
|
|
1720
1758
|
return {}
|
|
1721
1759
|
|
|
1722
|
-
return {SZ_SETPOINT:
|
|
1760
|
+
return {SZ_SETPOINT: hex_to_temp(payload[2:])}
|
|
1723
1761
|
|
|
1724
1762
|
|
|
1725
|
-
|
|
1726
|
-
def parser_2349(payload, msg) ->
|
|
1763
|
+
# zone_mode # TODO: messy
|
|
1764
|
+
def parser_2349(payload: str, msg: Message) -> PayDictT._2349 | PayDictT.EMPTY:
|
|
1727
1765
|
# RQ --- 34:225071 30:258557 --:------ 2349 001 00
|
|
1728
1766
|
# RP --- 30:258557 34:225071 --:------ 2349 013 007FFF00FFFFFFFFFFFFFFFFFF
|
|
1729
1767
|
# RP --- 30:253184 34:010943 --:------ 2349 013 00064000FFFFFF00110E0507E5
|
|
@@ -1734,10 +1772,10 @@ def parser_2349(payload, msg) -> dict:
|
|
|
1734
1772
|
|
|
1735
1773
|
assert msg.len in (7, 13), f"expected len 7,13, got {msg.len}"
|
|
1736
1774
|
|
|
1737
|
-
assert payload[6:8] in ZON_MODE_MAP, f"
|
|
1738
|
-
result = {
|
|
1739
|
-
SZ_MODE: ZON_MODE_MAP.get(payload[6:8]),
|
|
1740
|
-
SZ_SETPOINT:
|
|
1775
|
+
assert payload[6:8] in ZON_MODE_MAP, f"unknown zone_mode: {payload[6:8]}"
|
|
1776
|
+
result: PayDictT._2349 = {
|
|
1777
|
+
SZ_MODE: ZON_MODE_MAP.get(payload[6:8]), # type: ignore[typeddict-item]
|
|
1778
|
+
SZ_SETPOINT: hex_to_temp(payload[2:6]),
|
|
1741
1779
|
}
|
|
1742
1780
|
|
|
1743
1781
|
if msg.len >= 7: # has a dtm if mode == "04"
|
|
@@ -1756,21 +1794,20 @@ def parser_2349(payload, msg) -> dict:
|
|
|
1756
1794
|
result[SZ_UNTIL] = None # TODO: remove?
|
|
1757
1795
|
else:
|
|
1758
1796
|
assert payload[6:8] != ZON_MODE_MAP.PERMANENT, f"{payload[6:8]} (0x03)"
|
|
1759
|
-
result[SZ_UNTIL] =
|
|
1797
|
+
result[SZ_UNTIL] = hex_to_dtm(payload[14:26])
|
|
1760
1798
|
|
|
1761
1799
|
return result
|
|
1762
1800
|
|
|
1763
1801
|
|
|
1764
|
-
|
|
1765
|
-
def parser_2389(payload, msg) -> dict:
|
|
1766
|
-
|
|
1802
|
+
# unknown_2389, from 03:
|
|
1803
|
+
def parser_2389(payload: str, msg: Message) -> dict[str, Any]:
|
|
1767
1804
|
return {
|
|
1768
|
-
|
|
1805
|
+
"_unknown": hex_to_temp(payload[2:6]),
|
|
1769
1806
|
}
|
|
1770
1807
|
|
|
1771
1808
|
|
|
1772
|
-
|
|
1773
|
-
def parser_2400(payload, msg) -> dict:
|
|
1809
|
+
# unknown_2400, from OTB, FAN
|
|
1810
|
+
def parser_2400(payload: str, msg: Message) -> dict[str, Any]:
|
|
1774
1811
|
# RP --- 32:155617 18:005904 --:------ 2400 045 00001111-1010929292921110101020110010000080100010100000009191111191910011119191111111111100 # Orcon FAN
|
|
1775
1812
|
# RP --- 10:048122 18:006402 --:------ 2400 004 0000000F
|
|
1776
1813
|
# assert payload == "0000000F", _INFORM_DEV_MSG
|
|
@@ -1780,28 +1817,26 @@ def parser_2400(payload, msg) -> dict:
|
|
|
1780
1817
|
}
|
|
1781
1818
|
|
|
1782
1819
|
|
|
1783
|
-
|
|
1784
|
-
def parser_2401(payload, msg) -> dict:
|
|
1785
|
-
|
|
1820
|
+
# unknown_2401, from OTB
|
|
1821
|
+
def parser_2401(payload: str, msg: Message) -> dict[str, Any]:
|
|
1786
1822
|
try:
|
|
1787
1823
|
assert payload[2:4] == "00", f"byte 1: {payload[2:4]}"
|
|
1788
|
-
assert (
|
|
1789
|
-
|
|
1790
|
-
)
|
|
1824
|
+
assert int(payload[4:6], 16) & 0b11110000 == 0, (
|
|
1825
|
+
f"byte 2: {hex_to_flag8(payload[4:6])}"
|
|
1826
|
+
)
|
|
1791
1827
|
assert int(payload[6:], 0x10) <= 200, f"byte 3: {payload[6:]}"
|
|
1792
|
-
except AssertionError as
|
|
1793
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({
|
|
1828
|
+
except AssertionError as err:
|
|
1829
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
1794
1830
|
|
|
1795
1831
|
return {
|
|
1796
|
-
|
|
1832
|
+
"_flags_2": hex_to_flag8(payload[4:6]),
|
|
1833
|
+
**parse_valve_demand(payload[6:8]), # ~3150|FC
|
|
1797
1834
|
"_value_2": int(payload[4:6], 0x10),
|
|
1798
|
-
"_flags_2": flag8_from_hex(payload[4:6]),
|
|
1799
|
-
"_percent_3": percent_from_hex(payload[6:]),
|
|
1800
1835
|
}
|
|
1801
1836
|
|
|
1802
1837
|
|
|
1803
|
-
|
|
1804
|
-
def parser_2410(payload, msg) -> dict:
|
|
1838
|
+
# unknown_2410, from OTB, FAN
|
|
1839
|
+
def parser_2410(payload: str, msg: Message) -> dict[str, Any]:
|
|
1805
1840
|
# RP --- 10:048122 18:006402 --:------ 2410 020 00-00000000-00000000-00000001-00000001-00000C # OTB
|
|
1806
1841
|
# RP --- 32:155617 18:005904 --:------ 2410 020 00-00003EE8-00000000-FFFFFFFF-00000000-1002A6 # Orcon Fan
|
|
1807
1842
|
|
|
@@ -1816,10 +1851,13 @@ def parser_2410(payload, msg) -> dict:
|
|
|
1816
1851
|
)
|
|
1817
1852
|
return signed, length, d_type
|
|
1818
1853
|
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1854
|
+
try:
|
|
1855
|
+
assert payload[:6] == "00" * 3, _INFORM_DEV_MSG
|
|
1856
|
+
assert payload[10:18] == "00" * 4, _INFORM_DEV_MSG
|
|
1857
|
+
assert payload[18:26] in ("00000001", "FFFFFFFF"), _INFORM_DEV_MSG
|
|
1858
|
+
assert payload[26:34] in ("00000001", "00000000"), _INFORM_DEV_MSG
|
|
1859
|
+
except AssertionError as err:
|
|
1860
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
1823
1861
|
|
|
1824
1862
|
return {
|
|
1825
1863
|
"tail": payload[34:],
|
|
@@ -1833,31 +1871,31 @@ def parser_2410(payload, msg) -> dict:
|
|
|
1833
1871
|
}
|
|
1834
1872
|
|
|
1835
1873
|
|
|
1836
|
-
|
|
1837
|
-
def parser_2411(payload, msg) -> dict:
|
|
1874
|
+
# fan_params, HVAC
|
|
1875
|
+
def parser_2411(payload: str, msg: Message) -> dict[str, Any]:
|
|
1838
1876
|
# There is a relationship between 0001 and 2411
|
|
1839
1877
|
# RQ --- 37:171871 32:155617 --:------ 0001 005 0020000A04
|
|
1840
1878
|
# RP --- 32:155617 37:171871 --:------ 0001 008 0020000A004E0B00 # 0A -> 2411|4E
|
|
1841
1879
|
# RQ --- 37:171871 32:155617 --:------ 2411 003 00004E # 11th menu option (i.e. 0x0A)
|
|
1842
1880
|
# RP --- 32:155617 37:171871 --:------ 2411 023 00004E460000000001000000000000000100000001A600
|
|
1843
1881
|
|
|
1844
|
-
def counter(x) -> int:
|
|
1882
|
+
def counter(x: str) -> int:
|
|
1845
1883
|
return int(x, 16)
|
|
1846
1884
|
|
|
1847
|
-
def centile(x) -> float:
|
|
1885
|
+
def centile(x: str) -> float:
|
|
1848
1886
|
return int(x, 16) / 10
|
|
1849
1887
|
|
|
1850
1888
|
_2411_DATA_TYPES = {
|
|
1851
1889
|
"00": (2, counter), # 4E (0-1), 54 (15-60)
|
|
1852
1890
|
"01": (2, centile), # 52 (0.0-25.0) (%)
|
|
1853
|
-
"0F": (2,
|
|
1891
|
+
"0F": (2, hex_to_percent), # xx (0.0-1.0) (%)
|
|
1854
1892
|
"10": (4, counter), # 31 (0-1800) (days)
|
|
1855
|
-
"92": (4,
|
|
1893
|
+
"92": (4, hex_to_temp), # 75 (0-30) (C)
|
|
1856
1894
|
} # TODO: _2411_TYPES.get(payload[8:10], (8, no_op))
|
|
1857
1895
|
|
|
1858
|
-
assert (
|
|
1859
|
-
payload[4:6]
|
|
1860
|
-
)
|
|
1896
|
+
assert payload[4:6] in _2411_TABLE, (
|
|
1897
|
+
f"param {payload[4:6]} is unknown"
|
|
1898
|
+
) # _INFORM_DEV_MSG
|
|
1861
1899
|
description = _2411_TABLE.get(payload[4:6], "Unknown")
|
|
1862
1900
|
|
|
1863
1901
|
result = {
|
|
@@ -1868,30 +1906,32 @@ def parser_2411(payload, msg) -> dict:
|
|
|
1868
1906
|
if msg.verb == RQ:
|
|
1869
1907
|
return result
|
|
1870
1908
|
|
|
1871
|
-
assert (
|
|
1872
|
-
payload[8:10]
|
|
1873
|
-
)
|
|
1909
|
+
assert payload[8:10] in _2411_DATA_TYPES, (
|
|
1910
|
+
f"param {payload[4:6]} has unknown data_type: {payload[8:10]}"
|
|
1911
|
+
) # _INFORM_DEV_MSG
|
|
1874
1912
|
length, parser = _2411_DATA_TYPES.get(payload[8:10], (8, lambda x: x))
|
|
1875
1913
|
|
|
1876
1914
|
result |= {
|
|
1877
|
-
"value": parser(payload[10:18][-length:]),
|
|
1878
|
-
|
|
1915
|
+
"value": parser(payload[10:18][-length:]), # type: ignore[operator]
|
|
1916
|
+
"_value_06": payload[6:10],
|
|
1879
1917
|
}
|
|
1880
1918
|
|
|
1881
1919
|
if msg.len == 9:
|
|
1882
1920
|
return result
|
|
1883
1921
|
|
|
1884
|
-
return
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1922
|
+
return (
|
|
1923
|
+
result
|
|
1924
|
+
| {
|
|
1925
|
+
"min_value": parser(payload[18:26][-length:]), # type: ignore[operator]
|
|
1926
|
+
"max_value": parser(payload[26:34][-length:]), # type: ignore[operator]
|
|
1927
|
+
"precision": parser(payload[34:42][-length:]), # type: ignore[operator]
|
|
1928
|
+
"_value_42": payload[42:],
|
|
1929
|
+
}
|
|
1930
|
+
)
|
|
1891
1931
|
|
|
1892
|
-
@parser_decorator # unknown_2420, from OTB
|
|
1893
|
-
def parser_2420(payload, msg) -> dict:
|
|
1894
1932
|
|
|
1933
|
+
# unknown_2420, from OTB
|
|
1934
|
+
def parser_2420(payload: str, msg: Message) -> dict[str, Any]:
|
|
1895
1935
|
assert payload == "00000010" + "00" * 34, _INFORM_DEV_MSG
|
|
1896
1936
|
|
|
1897
1937
|
return {
|
|
@@ -1899,18 +1939,17 @@ def parser_2420(payload, msg) -> dict:
|
|
|
1899
1939
|
}
|
|
1900
1940
|
|
|
1901
1941
|
|
|
1902
|
-
|
|
1903
|
-
def parser_2d49(payload, msg) ->
|
|
1904
|
-
|
|
1942
|
+
# _state (of cooling?), from BDR91T, hometronics CTL
|
|
1943
|
+
def parser_2d49(payload: str, msg: Message) -> PayDictT._2D49:
|
|
1905
1944
|
assert payload[2:] in ("0000", "00FF", "C800", "C8FF"), _INFORM_DEV_MSG
|
|
1906
1945
|
|
|
1907
1946
|
return {
|
|
1908
|
-
"state":
|
|
1947
|
+
"state": hex_to_bool(payload[2:4]),
|
|
1909
1948
|
}
|
|
1910
1949
|
|
|
1911
1950
|
|
|
1912
|
-
|
|
1913
|
-
def parser_2e04(payload, msg) ->
|
|
1951
|
+
# system_mode
|
|
1952
|
+
def parser_2e04(payload: str, msg: Message) -> PayDictT._2E04:
|
|
1914
1953
|
# if msg.verb == W_:
|
|
1915
1954
|
|
|
1916
1955
|
# .I --— 01:020766 --:------ 01:020766 2E04 016 FFFFFFFFFFFFFF0007FFFFFFFFFFFF04 # Manual # noqa: E501
|
|
@@ -1931,65 +1970,76 @@ def parser_2e04(payload, msg) -> dict:
|
|
|
1931
1970
|
# msg.len in (8, 16) # evohome 8, hometronics 16
|
|
1932
1971
|
assert False, f"Packet length is {msg.len} (expecting 8, 16)"
|
|
1933
1972
|
|
|
1934
|
-
result = {SZ_SYSTEM_MODE: SYS_MODE_MAP[payload[:2]]}
|
|
1973
|
+
result: PayDictT._2E04 = {SZ_SYSTEM_MODE: SYS_MODE_MAP[payload[:2]]}
|
|
1935
1974
|
if payload[:2] not in (
|
|
1936
1975
|
SYS_MODE_MAP.AUTO,
|
|
1937
1976
|
SYS_MODE_MAP.HEAT_OFF,
|
|
1938
1977
|
SYS_MODE_MAP.AUTO_WITH_RESET,
|
|
1939
1978
|
):
|
|
1940
1979
|
result.update(
|
|
1941
|
-
{SZ_UNTIL:
|
|
1980
|
+
{SZ_UNTIL: hex_to_dtm(payload[2:14]) if payload[14:16] != "00" else None}
|
|
1942
1981
|
)
|
|
1943
1982
|
return result # TODO: double-check the final "00"
|
|
1944
1983
|
|
|
1945
1984
|
|
|
1946
|
-
|
|
1947
|
-
def parser_2e10(payload, msg) -> dict:
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1985
|
+
# presence_detect, HVAC sensor, or Timed boost for Vasco D60
|
|
1986
|
+
def parser_2e10(payload: str, msg: Message) -> dict[str, Any]:
|
|
1987
|
+
assert payload in ("0001", "000000", "000100"), _INFORM_DEV_MSG
|
|
1988
|
+
presence: int = int(payload[2:4])
|
|
1951
1989
|
return {
|
|
1952
|
-
"presence_detected": bool(
|
|
1953
|
-
|
|
1990
|
+
"presence_detected": bool(presence),
|
|
1991
|
+
"_unknown_4": payload[4:],
|
|
1954
1992
|
}
|
|
1955
1993
|
|
|
1956
1994
|
|
|
1957
|
-
|
|
1958
|
-
def parser_30c9(payload, msg) -> dict:
|
|
1959
|
-
|
|
1995
|
+
# current temperature (of device, zone/s)
|
|
1996
|
+
def parser_30c9(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
1960
1997
|
if msg._has_array:
|
|
1961
1998
|
return [
|
|
1962
1999
|
{
|
|
1963
2000
|
SZ_ZONE_IDX: payload[i : i + 2],
|
|
1964
|
-
SZ_TEMPERATURE:
|
|
2001
|
+
SZ_TEMPERATURE: hex_to_temp(payload[i + 2 : i + 6]),
|
|
1965
2002
|
}
|
|
1966
2003
|
for i in range(0, len(payload), 6)
|
|
1967
2004
|
]
|
|
1968
2005
|
|
|
1969
|
-
return {SZ_TEMPERATURE:
|
|
2006
|
+
return {SZ_TEMPERATURE: hex_to_temp(payload[2:])}
|
|
1970
2007
|
|
|
1971
2008
|
|
|
1972
|
-
|
|
1973
|
-
def parser_3110(payload, msg) ->
|
|
1974
|
-
# .I --- 02:250708 --:------ 02:250708 3110 004 0000C820
|
|
1975
|
-
# .I --- 21:042656 --:------ 21:042656 3110 004
|
|
2009
|
+
# ufc_demand, HVAC (Itho autotemp / spider)
|
|
2010
|
+
def parser_3110(payload: str, msg: Message) -> PayDictT._3110:
|
|
2011
|
+
# .I --- 02:250708 --:------ 02:250708 3110 004 0000C820 # cooling, 100%
|
|
2012
|
+
# .I --- 21:042656 --:------ 21:042656 3110 004 00000010 # heating, 0%
|
|
2013
|
+
|
|
2014
|
+
SZ_COOLING = "cooling"
|
|
2015
|
+
SZ_DISABLE = "disabled"
|
|
2016
|
+
SZ_HEATING = "heating"
|
|
2017
|
+
SZ_UNKNOWN = "unknown"
|
|
1976
2018
|
|
|
1977
2019
|
try:
|
|
1978
|
-
assert payload[2:4] == "00", f"byte 1: {payload[2:4]}"
|
|
2020
|
+
assert payload[2:4] == "00", f"byte 1: {payload[2:4]}" # ?circuit_idx?
|
|
1979
2021
|
assert int(payload[4:6], 16) <= 200, f"byte 2: {payload[4:6]}"
|
|
1980
2022
|
assert payload[6:] in ("00", "10", "20"), f"byte 3: {payload[6:]}"
|
|
1981
|
-
|
|
1982
|
-
|
|
2023
|
+
assert payload[6:] in ("10", "20") or payload[4:6] == "00", (
|
|
2024
|
+
f"byte 3: {payload[6:]}"
|
|
2025
|
+
)
|
|
2026
|
+
except AssertionError as err:
|
|
2027
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
1983
2028
|
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
}
|
|
2029
|
+
mode = {
|
|
2030
|
+
0x00: SZ_DISABLE,
|
|
2031
|
+
0x10: SZ_HEATING,
|
|
2032
|
+
0x20: SZ_COOLING,
|
|
2033
|
+
}.get(int(payload[6:8], 16) & 0x30, SZ_UNKNOWN)
|
|
2034
|
+
|
|
2035
|
+
if mode not in (SZ_COOLING, SZ_HEATING):
|
|
2036
|
+
return {SZ_MODE: mode}
|
|
1989
2037
|
|
|
2038
|
+
return {SZ_MODE: mode, SZ_DEMAND: hex_to_percent(payload[4:6])}
|
|
1990
2039
|
|
|
1991
|
-
|
|
1992
|
-
|
|
2040
|
+
|
|
2041
|
+
# unknown_3120, from STA, FAN
|
|
2042
|
+
def parser_3120(payload: str, msg: Message) -> dict[str, Any]:
|
|
1993
2043
|
# .I --- 34:136285 --:------ 34:136285 3120 007 0070B0000000FF # every ~3:45:00!
|
|
1994
2044
|
# RP --- 20:008749 18:142609 --:------ 3120 007 0070B000009CFF
|
|
1995
2045
|
# .I --- 37:258565 --:------ 37:258565 3120 007 0080B0010003FF
|
|
@@ -2002,38 +2052,39 @@ def parser_3120(payload, msg) -> dict:
|
|
|
2002
2052
|
assert payload[8:10] == "00", f"byte 4: {payload[8:10]}"
|
|
2003
2053
|
assert payload[10:12] in ("00", "03", "0A", "9C"), f"byte 5: {payload[10:12]}"
|
|
2004
2054
|
assert payload[12:] == "FF", f"byte 6: {payload[12:]}"
|
|
2005
|
-
except AssertionError as
|
|
2006
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({
|
|
2055
|
+
except AssertionError as err:
|
|
2056
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
2007
2057
|
|
|
2008
2058
|
return {
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2059
|
+
"unknown_0": payload[2:10],
|
|
2060
|
+
"unknown_5": payload[10:12],
|
|
2061
|
+
"unknown_2": payload[12:],
|
|
2012
2062
|
}
|
|
2013
2063
|
|
|
2014
2064
|
|
|
2015
|
-
|
|
2016
|
-
def parser_313e(payload, msg) -> dict:
|
|
2017
|
-
# 11:00:59.412 RP --- 32:153258 18:005904 --:------ 313E 011 00-0000007937-003C80-0000
|
|
2018
|
-
# 11:02:23.961 RP --- 32:153258 18:005904 --:------ 313E 011 00-0000007B14-003C80-0000
|
|
2019
|
-
# 11:03:32.193 RP --- 32:153258 18:005904 --:------ 313E 011 00-0000007C1C-003C80-0000
|
|
2020
|
-
|
|
2065
|
+
# WIP: unknown, HVAC
|
|
2066
|
+
def parser_313e(payload: str, msg: Message) -> dict[str, Any]:
|
|
2021
2067
|
assert payload[:2] == "00"
|
|
2022
2068
|
assert payload[12:] == "003C800000"
|
|
2023
2069
|
|
|
2070
|
+
result = (
|
|
2071
|
+
msg.dtm - td(seconds=int(payload[10:12], 16), minutes=int(payload[2:10], 16))
|
|
2072
|
+
).isoformat()
|
|
2073
|
+
|
|
2024
2074
|
return {
|
|
2025
|
-
"
|
|
2026
|
-
"
|
|
2027
|
-
"
|
|
2075
|
+
"zulu": result,
|
|
2076
|
+
"value_02": payload[2:10],
|
|
2077
|
+
"value_10": payload[10:12],
|
|
2078
|
+
"value_12": payload[12:],
|
|
2028
2079
|
}
|
|
2029
2080
|
|
|
2030
2081
|
|
|
2031
|
-
|
|
2032
|
-
def parser_313f(payload, msg) ->
|
|
2033
|
-
# 2020-03-28T03:59:21.315178 045 RP --- 01:158182 04:136513 --:------ 313F 009 00FC3500A41C0307E4
|
|
2034
|
-
# 2020-03-29T04:58:30.486343 045 RP --- 01:158182 04:136485 --:------ 313F 009 00FC8400C51D0307E4
|
|
2082
|
+
# datetime
|
|
2083
|
+
def parser_313f(payload: str, msg: Message) -> PayDictT._313F: # TODO: look for TZ
|
|
2084
|
+
# 2020-03-28T03:59:21.315178 045 RP --- 01:158182 04:136513 --:------ 313F 009 00FC3500A41C0307E4
|
|
2085
|
+
# 2020-03-29T04:58:30.486343 045 RP --- 01:158182 04:136485 --:------ 313F 009 00FC8400C51D0307E4
|
|
2035
2086
|
# 2022-09-20T20:50:32.800676 065 RP --- 01:182924 18:068640 --:------ 313F 009 00F9203234140907E6
|
|
2036
|
-
# 2020-05-31T11:37:50.351511 056 I --- --:------ --:------ 12:207082 313F 009 0038021ECB1F0507E4
|
|
2087
|
+
# 2020-05-31T11:37:50.351511 056 I --- --:------ --:------ 12:207082 313F 009 0038021ECB1F0507E4
|
|
2037
2088
|
|
|
2038
2089
|
# https://www.automatedhome.co.uk/vbulletin/showthread.php?5085-My-HGI80-equivalent-Domoticz-setup-without-HGI80&p=36422&viewfull=1#post36422
|
|
2039
2090
|
# every day at ~4am TRV/RQ->CTL/RP, approx 5-10secs apart (CTL respond at any time)
|
|
@@ -2049,25 +2100,25 @@ def parser_313f(payload, msg) -> dict: # TODO: look for TZ
|
|
|
2049
2100
|
# assert (
|
|
2050
2101
|
# msg.src.type != DEV_TYPE_MAP.FAN or payload[2:4] == "7C"
|
|
2051
2102
|
# ), f"{payload[2:4]} unexpected for FAN" # DEX
|
|
2052
|
-
assert (
|
|
2053
|
-
|
|
2054
|
-
)
|
|
2103
|
+
assert msg.src.type != DEV_TYPE_MAP.RFG or payload[2:4] == "60", (
|
|
2104
|
+
"{payload[2:4]} unexpected for RFG"
|
|
2105
|
+
) # DEX
|
|
2055
2106
|
|
|
2056
2107
|
return {
|
|
2057
|
-
SZ_DATETIME:
|
|
2108
|
+
SZ_DATETIME: hex_to_dtm(payload[4:18]),
|
|
2058
2109
|
SZ_IS_DST: True if bool(int(payload[4:6], 16) & 0x80) else None,
|
|
2059
|
-
|
|
2110
|
+
"_unknown_0": payload[2:4],
|
|
2060
2111
|
}
|
|
2061
2112
|
|
|
2062
2113
|
|
|
2063
|
-
|
|
2064
|
-
def parser_3150(payload, msg) -> dict | list:
|
|
2114
|
+
# heat_demand (of device, FC domain) - valve status (%open)
|
|
2115
|
+
def parser_3150(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
2065
2116
|
# event-driven, and periodically; FC domain is maximum of all zones
|
|
2066
2117
|
# TODO: all have a valid domain will UFC/CTL respond to an RQ, for FC, for a zone?
|
|
2067
2118
|
|
|
2068
2119
|
# .I --- 04:136513 --:------ 01:158182 3150 002 01CA < often seen CA, artefact?
|
|
2069
2120
|
|
|
2070
|
-
def complex_idx(seqx, msg) -> dict:
|
|
2121
|
+
def complex_idx(seqx: str, msg: Message) -> dict[str, str]:
|
|
2071
2122
|
# assert seqx[:2] == FC or (int(seqx[:2], 16) < MAX_ZONES) # <5, 8 for UFC
|
|
2072
2123
|
idx_name = "ufx_idx" if msg.src.type == DEV_TYPE_MAP.UFC else SZ_ZONE_IDX # DEX
|
|
2073
2124
|
return {SZ_DOMAIN_ID if seqx[:1] == "F" else idx_name: seqx[:2]}
|
|
@@ -2076,47 +2127,65 @@ def parser_3150(payload, msg) -> dict | list:
|
|
|
2076
2127
|
return [
|
|
2077
2128
|
{
|
|
2078
2129
|
**complex_idx(payload[i : i + 2], msg),
|
|
2079
|
-
**
|
|
2130
|
+
**parse_valve_demand(payload[i + 2 : i + 4]),
|
|
2080
2131
|
}
|
|
2081
2132
|
for i in range(0, len(payload), 4)
|
|
2082
2133
|
]
|
|
2083
2134
|
|
|
2084
|
-
return
|
|
2135
|
+
return parse_valve_demand(payload[2:]) # TODO: check UFC/FC is == CTL/FC
|
|
2085
2136
|
|
|
2086
2137
|
|
|
2087
|
-
|
|
2088
|
-
def parser_31d9(payload, msg) -> dict:
|
|
2089
|
-
# NOTE:
|
|
2138
|
+
# fan state (ventilation status), HVAC
|
|
2139
|
+
def parser_31d9(payload: str, msg: Message) -> dict[str, Any]:
|
|
2140
|
+
# NOTE: Itho and ClimaRad use 0x00-C8 for %, whilst Nuaire uses 0x00-64
|
|
2090
2141
|
try:
|
|
2091
|
-
assert (
|
|
2092
|
-
|
|
2093
|
-
)
|
|
2094
|
-
except AssertionError as
|
|
2095
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({
|
|
2142
|
+
assert payload[4:6] == "FF" or int(payload[4:6], 16) <= 200, (
|
|
2143
|
+
f"byte 2: {payload[4:6]}"
|
|
2144
|
+
)
|
|
2145
|
+
except AssertionError as err:
|
|
2146
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
2096
2147
|
|
|
2097
2148
|
bitmap = int(payload[2:4], 16)
|
|
2098
2149
|
|
|
2099
|
-
# NOTE: 31D9[4:6] is
|
|
2150
|
+
# NOTE: 31D9[4:6] is fan_speed (ClimaRad minibox, Itho) *or* fan_mode (Orcon, Vasco)
|
|
2100
2151
|
result = {
|
|
2101
|
-
|
|
2102
|
-
SZ_FAN_MODE: payload[4:6], # orcon
|
|
2152
|
+
**parse_exhaust_fan_speed(payload[4:6]), # for itho
|
|
2153
|
+
SZ_FAN_MODE: payload[4:6], # orcon, vasco/climarad
|
|
2103
2154
|
"passive": bool(bitmap & 0x02),
|
|
2104
|
-
"damper_only": bool(bitmap & 0x04),
|
|
2155
|
+
"damper_only": bool(bitmap & 0x04), # i.e. valve only
|
|
2105
2156
|
"filter_dirty": bool(bitmap & 0x20),
|
|
2106
2157
|
"frost_cycle": bool(bitmap & 0x40),
|
|
2107
2158
|
"has_fault": bool(bitmap & 0x80),
|
|
2108
|
-
"_flags":
|
|
2159
|
+
"_flags": hex_to_flag8(payload[2:4]),
|
|
2109
2160
|
}
|
|
2110
2161
|
|
|
2162
|
+
# Fan Mode Lookup 1 for Vasco codes
|
|
2111
2163
|
if msg.len == 3: # usu: I -->20: (no seq#)
|
|
2164
|
+
if (
|
|
2165
|
+
(payload[:4] == "0000" or payload[:4] == "0080") # Senza, meaning of 0x80?
|
|
2166
|
+
and msg._addrs[0] == msg._addrs[2]
|
|
2167
|
+
and msg._addrs[1] == NON_DEV_ADDR
|
|
2168
|
+
):
|
|
2169
|
+
# _31D9_FAN_INFO for Vasco D60 HRU and ClimaRad Minibox, S-Fan, (REM: RQ only, msg.len==1)
|
|
2170
|
+
try:
|
|
2171
|
+
assert int(payload[4:6], 16) & 0xFF in _31D9_FAN_INFO_VASCO, (
|
|
2172
|
+
f"unknown 31D9 fan_mode lookup key: {payload[4:6]}"
|
|
2173
|
+
)
|
|
2174
|
+
except AssertionError as err:
|
|
2175
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
2176
|
+
fan_mode = _31D9_FAN_INFO_VASCO.get(
|
|
2177
|
+
int(payload[4:6], 16) & 0xFF, f"unknown_{payload[4:6]}"
|
|
2178
|
+
)
|
|
2179
|
+
result[SZ_FAN_MODE] = fan_mode # replace
|
|
2180
|
+
# if not replaced, 31D9 FAN_MODE is a 2 digit string HEX
|
|
2112
2181
|
return result
|
|
2113
2182
|
|
|
2114
2183
|
try:
|
|
2115
2184
|
assert payload[6:8] in ("00", "07", "0A", "FE"), f"byte 3: {payload[6:8]}"
|
|
2116
|
-
except AssertionError as
|
|
2117
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({
|
|
2185
|
+
except AssertionError as err:
|
|
2186
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
2118
2187
|
|
|
2119
|
-
result.update({
|
|
2188
|
+
result.update({"_unknown_3": payload[6:8]})
|
|
2120
2189
|
|
|
2121
2190
|
if msg.len == 4: # usu: I -->20: (no seq#)
|
|
2122
2191
|
return result
|
|
@@ -2124,45 +2193,40 @@ def parser_31d9(payload, msg) -> dict:
|
|
|
2124
2193
|
try:
|
|
2125
2194
|
assert payload[8:32] in ("00" * 12, "20" * 12), f"byte 4: {payload[8:32]}"
|
|
2126
2195
|
assert payload[32:] in ("00", "04", "08"), f"byte 16: {payload[32:]}"
|
|
2127
|
-
except AssertionError as
|
|
2128
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({
|
|
2196
|
+
except AssertionError as err:
|
|
2197
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
2129
2198
|
|
|
2130
2199
|
return {
|
|
2131
2200
|
**result,
|
|
2132
|
-
|
|
2133
|
-
|
|
2201
|
+
"_unknown_4": payload[8:32],
|
|
2202
|
+
"unknown_16": payload[32:],
|
|
2134
2203
|
}
|
|
2135
2204
|
|
|
2136
2205
|
|
|
2137
|
-
|
|
2138
|
-
def parser_31da(payload, msg) ->
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
#
|
|
2142
|
-
|
|
2143
|
-
#
|
|
2144
|
-
#
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
# assert payload[40:42] in ("00", "EF", "FF"), payload[40:42]
|
|
2162
|
-
assert payload[46:48] in ("00", "EF"), f"[46:48] {payload[46:48]}"
|
|
2163
|
-
# assert payload[48:50] == "EF", payload[48:50]
|
|
2164
|
-
except AssertionError as exc:
|
|
2165
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({exc})")
|
|
2206
|
+
# ventilation state (extended), HVAC
|
|
2207
|
+
def parser_31da(payload: str, msg: Message) -> PayDictT._31DA:
|
|
2208
|
+
# see: https://github.com/python/typing/issues/1445
|
|
2209
|
+
return { # type: ignore[typeddict-unknown-key]
|
|
2210
|
+
**parse_exhaust_fan_speed(payload[38:40]), # maybe 31D9[4:6] for some?
|
|
2211
|
+
**parse_fan_info(payload[36:38]), # 22F3-ish
|
|
2212
|
+
#
|
|
2213
|
+
**parse_air_quality(payload[2:6]), # 12C8[2:6]
|
|
2214
|
+
**parse_co2_level(payload[6:10]), # 1298[2:6]
|
|
2215
|
+
**parse_indoor_humidity(payload[10:12]), # 12A0?
|
|
2216
|
+
**parse_outdoor_humidity(payload[12:14]),
|
|
2217
|
+
**parse_exhaust_temp(payload[14:18]), # to outside
|
|
2218
|
+
**parse_supply_temp(payload[18:22]), # to home
|
|
2219
|
+
**parse_indoor_temp(payload[22:26]), # in home
|
|
2220
|
+
**parse_outdoor_temp(payload[26:30]), # 1290?
|
|
2221
|
+
**parse_capabilities(payload[30:34]),
|
|
2222
|
+
**parse_bypass_position(payload[34:36]), # 22F7-ish
|
|
2223
|
+
**parse_supply_fan_speed(payload[40:42]),
|
|
2224
|
+
**parse_remaining_mins(payload[42:46]), # mins, ~22F3[2:6]
|
|
2225
|
+
**parse_post_heater(payload[46:48]),
|
|
2226
|
+
**parse_pre_heater(payload[48:50]),
|
|
2227
|
+
**parse_supply_flow(payload[50:54]), # NOTE: is supply, not exhaust
|
|
2228
|
+
**parse_exhaust_flow(payload[54:58]), # NOTE: order switched from others
|
|
2229
|
+
}
|
|
2166
2230
|
|
|
2167
2231
|
# From an Orcon 15RF Display
|
|
2168
2232
|
# 1 Software version
|
|
@@ -2175,39 +2239,14 @@ def parser_31da(payload, msg) -> dict:
|
|
|
2175
2239
|
# 10 Bypass position SZ_BYPASS_POSITION
|
|
2176
2240
|
# 11 Exhaust fan speed (%) SZ_EXHAUST_FAN_SPEED
|
|
2177
2241
|
# 12 Fan supply speed (%) SZ_SUPPLY_FAN_SPEED
|
|
2178
|
-
# 13 Remaining after run time (
|
|
2242
|
+
# 13 Remaining after run time (min.) SZ_REMAINING_TIME - for humidity scenario
|
|
2179
2243
|
# 14 Preheater control (MaxComfort) (%) SZ_PRE_HEAT
|
|
2180
2244
|
# 16 Actual supply flow rate (m3/h) SZ_SUPPLY_FLOW (Orcon is m3/h, data is L/s)
|
|
2181
2245
|
# 17 Current discharge flow rate (m3/h) SZ_EXHAUST_FLOW
|
|
2182
2246
|
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
), # maybe 31D9[4:6] for some?
|
|
2187
|
-
SZ_FAN_INFO: _31DA_FAN_INFO[int(payload[36:38], 16) & 0x1F], # 22F3-ish
|
|
2188
|
-
SZ_REMAINING_TIME: double_from_hex(payload[42:46]), # mins, 22F3[2:6]
|
|
2189
|
-
#
|
|
2190
|
-
SZ_AIR_QUALITY: percent_from_hex(payload[2:4]), # 12C8[2:4]
|
|
2191
|
-
SZ_AIR_QUALITY_BASE: int(payload[4:6], 16), # 12C8[4:6]
|
|
2192
|
-
SZ_CO2_LEVEL: double_from_hex(payload[6:10]), # ppm, 1298[2:6]
|
|
2193
|
-
SZ_INDOOR_HUMIDITY: percent_from_hex(payload[10:12], high_res=False), # 12A0?
|
|
2194
|
-
SZ_OUTDOOR_HUMIDITY: percent_from_hex(payload[12:14], high_res=False),
|
|
2195
|
-
SZ_EXHAUST_TEMPERATURE: temp_from_hex(payload[14:18]),
|
|
2196
|
-
SZ_SUPPLY_TEMPERATURE: temp_from_hex(payload[18:22]),
|
|
2197
|
-
SZ_INDOOR_TEMPERATURE: temp_from_hex(payload[22:26]),
|
|
2198
|
-
SZ_OUTDOOR_TEMPERATURE: temp_from_hex(payload[26:30]), # 1290?
|
|
2199
|
-
SZ_SPEED_CAP: int(payload[30:34], 16),
|
|
2200
|
-
SZ_BYPASS_POSITION: percent_from_hex(payload[34:36]),
|
|
2201
|
-
SZ_SUPPLY_FAN_SPEED: percent_from_hex(payload[40:42]),
|
|
2202
|
-
SZ_POST_HEAT: percent_from_hex(payload[46:48], high_res=False),
|
|
2203
|
-
SZ_PRE_HEAT: percent_from_hex(payload[48:50], high_res=False),
|
|
2204
|
-
SZ_SUPPLY_FLOW: double_from_hex(payload[50:54], factor=100), # L/sec
|
|
2205
|
-
SZ_EXHAUST_FLOW: double_from_hex(payload[54:58], factor=100), # L/sec
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
@parser_decorator # vent_demand, HVAC
|
|
2210
|
-
def parser_31e0(payload, msg) -> dict:
|
|
2247
|
+
|
|
2248
|
+
# vent_demand, HVAC
|
|
2249
|
+
def parser_31e0(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
2211
2250
|
"""Notes are.
|
|
2212
2251
|
|
|
2213
2252
|
van means “of”.
|
|
@@ -2261,13 +2300,13 @@ def parser_31e0(payload, msg) -> dict:
|
|
|
2261
2300
|
# .I --- 32:168090 30:082155 --:------ 31E0 004 00-00C8-00
|
|
2262
2301
|
# .I --- 37:258565 37:261128 --:------ 31E0 004 00-0001-00
|
|
2263
2302
|
|
|
2264
|
-
def _parser(seqx) -> dict:
|
|
2303
|
+
def _parser(seqx: str) -> dict:
|
|
2265
2304
|
assert seqx[6:] in ("", "00", "FF")
|
|
2266
2305
|
return {
|
|
2267
2306
|
# "hvac_idx": seqx[:2],
|
|
2268
2307
|
"flags": seqx[2:4],
|
|
2269
|
-
"vent_demand":
|
|
2270
|
-
|
|
2308
|
+
"vent_demand": hex_to_percent(seqx[4:6]),
|
|
2309
|
+
"_unknown_3": payload[6:],
|
|
2271
2310
|
}
|
|
2272
2311
|
|
|
2273
2312
|
if len(payload) > 8:
|
|
@@ -2275,34 +2314,35 @@ def parser_31e0(payload, msg) -> dict:
|
|
|
2275
2314
|
return _parser(payload)
|
|
2276
2315
|
|
|
2277
2316
|
|
|
2278
|
-
|
|
2279
|
-
def parser_3200(payload, msg) ->
|
|
2280
|
-
return {SZ_TEMPERATURE:
|
|
2281
|
-
|
|
2317
|
+
# supplied boiler water (flow) temp
|
|
2318
|
+
def parser_3200(payload: str, msg: Message) -> PayDictT._3200:
|
|
2319
|
+
return {SZ_TEMPERATURE: hex_to_temp(payload[2:])}
|
|
2282
2320
|
|
|
2283
|
-
@parser_decorator # return (boiler) water temp
|
|
2284
|
-
def parser_3210(payload, msg) -> dict:
|
|
2285
|
-
return {SZ_TEMPERATURE: temp_from_hex(payload[2:])}
|
|
2286
2321
|
|
|
2322
|
+
# return (boiler) water temp
|
|
2323
|
+
def parser_3210(payload: str, msg: Message) -> PayDictT._3210:
|
|
2324
|
+
return {SZ_TEMPERATURE: hex_to_temp(payload[2:])}
|
|
2287
2325
|
|
|
2288
|
-
@parser_decorator # opentherm_msg, from OTB
|
|
2289
|
-
def parser_3220(payload, msg) -> dict:
|
|
2290
2326
|
|
|
2327
|
+
# opentherm_msg, from OTB (and some RND)
|
|
2328
|
+
def parser_3220(payload: str, msg: Message) -> dict[str, Any]:
|
|
2291
2329
|
try:
|
|
2292
2330
|
ot_type, ot_id, ot_value, ot_schema = decode_frame(payload[2:10])
|
|
2293
|
-
except AssertionError as
|
|
2294
|
-
raise AssertionError(f"OpenTherm: {
|
|
2295
|
-
except ValueError as
|
|
2296
|
-
raise
|
|
2331
|
+
except AssertionError as err:
|
|
2332
|
+
raise AssertionError(f"OpenTherm: {err}") from err
|
|
2333
|
+
except ValueError as err:
|
|
2334
|
+
raise exc.PacketPayloadInvalid(f"OpenTherm: {err}") from err
|
|
2297
2335
|
|
|
2298
2336
|
# NOTE: Unknown-DataId isn't an invalid payload & is useful to train the OTB device
|
|
2299
|
-
if ot_schema is None and ot_type != OtMsgType.UNKNOWN_DATAID:
|
|
2300
|
-
raise
|
|
2337
|
+
if ot_schema is None and ot_type != OtMsgType.UNKNOWN_DATAID: # type: ignore[unreachable]
|
|
2338
|
+
raise exc.PacketPayloadInvalid(
|
|
2339
|
+
f"OpenTherm: Unknown data-id: 0x{ot_id:02X} ({ot_id})"
|
|
2340
|
+
)
|
|
2301
2341
|
|
|
2302
2342
|
result = {
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2343
|
+
SZ_MSG_ID: ot_id,
|
|
2344
|
+
SZ_MSG_TYPE: str(ot_type),
|
|
2345
|
+
SZ_MSG_NAME: ot_value.pop(SZ_MSG_NAME, None),
|
|
2306
2346
|
}
|
|
2307
2347
|
|
|
2308
2348
|
if msg.verb == RQ: # RQs have a context: msg_id (and a payload)
|
|
@@ -2319,41 +2359,56 @@ def parser_3220(payload, msg) -> dict:
|
|
|
2319
2359
|
|
|
2320
2360
|
result.update(ot_value) # TODO: find some of these packets to review
|
|
2321
2361
|
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
assert ot_type not in _LIST or payload[6:10] in (
|
|
2325
|
-
"0000",
|
|
2326
|
-
"FFFF",
|
|
2327
|
-
), f"OpenTherm: Invalid msg-type|data-value: {ot_type}|{payload[6:10]}"
|
|
2362
|
+
result[SZ_DESCRIPTION] = ot_schema.get(EN)
|
|
2363
|
+
return result
|
|
2328
2364
|
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2365
|
+
# if msg.verb != RP:
|
|
2366
|
+
# raise
|
|
2367
|
+
|
|
2368
|
+
_LIST = (OtMsgType.DATA_INVALID, OtMsgType.UNKNOWN_DATAID, OtMsgType.RESERVED)
|
|
2369
|
+
assert ot_type not in _LIST or payload[6:10] in (
|
|
2370
|
+
"0000",
|
|
2371
|
+
"FFFF",
|
|
2372
|
+
), f"OpenTherm: Invalid msg-type|data-value: {ot_type}|{payload[6:10]}"
|
|
2373
|
+
|
|
2374
|
+
# HACK: These OT data id can pop in/out of 47AB, which is an invalid value
|
|
2375
|
+
if payload[6:] == "47AB" and ot_id in (0x12, 0x13, 0x19, 0x1A, 0x1B, 0x1C):
|
|
2376
|
+
ot_value[SZ_VALUE] = None
|
|
2377
|
+
# HACK: This OT data id can be 1980, which is an invalid value
|
|
2378
|
+
if payload[6:] == "1980" and ot_id: # CH pressure is 25.5 bar!
|
|
2379
|
+
ot_value[SZ_VALUE] = None
|
|
2380
|
+
# HACK: Done above, not in OT.decode_frame() as they isn't in the OT specification
|
|
2381
|
+
|
|
2382
|
+
if ot_type not in _LIST:
|
|
2383
|
+
assert ot_type in (
|
|
2384
|
+
OtMsgType.READ_ACK,
|
|
2385
|
+
OtMsgType.WRITE_ACK,
|
|
2386
|
+
), f"OpenTherm: Invalid msg-type for RP: {ot_type}"
|
|
2387
|
+
|
|
2388
|
+
result.update(ot_value)
|
|
2389
|
+
|
|
2390
|
+
try: # These are checking flags in payload of data-id 0x00
|
|
2391
|
+
assert ot_id != 0 or (
|
|
2392
|
+
[result[SZ_VALUE][i] for i in (2, 3, 4, 5, 6, 7)] == [0] * 6
|
|
2393
|
+
# and [result[SZ_VALUE][i] for i in (1, )] == [1]
|
|
2394
|
+
), result[SZ_VALUE]
|
|
2395
|
+
|
|
2396
|
+
assert ot_id != 0 or (
|
|
2397
|
+
[result[SZ_VALUE][8 + i] for i in (0, 4, 5, 6, 7)] == [0] * 5
|
|
2398
|
+
# and [result[SZ_VALUE][8 + i] for i in (1, 2, 3)] == [0] * 3
|
|
2399
|
+
), result[SZ_VALUE]
|
|
2400
|
+
|
|
2401
|
+
except AssertionError:
|
|
2402
|
+
_LOGGER.warning(
|
|
2403
|
+
f"{msg!r} < {_INFORM_DEV_MSG}, with a description of your system"
|
|
2404
|
+
)
|
|
2349
2405
|
|
|
2350
|
-
result[
|
|
2406
|
+
result[SZ_DESCRIPTION] = ot_schema.get(EN)
|
|
2351
2407
|
return result
|
|
2352
2408
|
|
|
2353
2409
|
|
|
2354
|
-
|
|
2355
|
-
def parser_3221(payload, msg) -> dict:
|
|
2356
|
-
|
|
2410
|
+
# unknown_3221, from OTB, FAN
|
|
2411
|
+
def parser_3221(payload: str, msg: Message) -> dict[str, Any]:
|
|
2357
2412
|
# RP --- 10:052644 18:198151 --:------ 3221 002 000F
|
|
2358
2413
|
# RP --- 10:048122 18:006402 --:------ 3221 002 0000
|
|
2359
2414
|
# RP --- 32:155617 18:005904 --:------ 3221 002 000A
|
|
@@ -2361,46 +2416,43 @@ def parser_3221(payload, msg) -> dict:
|
|
|
2361
2416
|
assert int(payload[2:], 16) <= 0xC8, _INFORM_DEV_MSG
|
|
2362
2417
|
|
|
2363
2418
|
return {
|
|
2364
|
-
|
|
2419
|
+
"_payload": payload,
|
|
2365
2420
|
SZ_VALUE: int(payload[2:], 16),
|
|
2366
2421
|
}
|
|
2367
2422
|
|
|
2368
2423
|
|
|
2369
|
-
|
|
2370
|
-
def parser_3222(payload, msg) -> dict:
|
|
2371
|
-
# 06:30:14.322 RP --- 32:155617 18:005904 --:------ 3222 004 00-00-01-00
|
|
2372
|
-
# 00:09:26.263 RP --- 32:155617 18:005904 --:------ 3222 005 00-00-02-0009
|
|
2373
|
-
# 02:42:27.090 RP --- 32:155617 18:005904 --:------ 3222 007 00-06-04- 000F100E
|
|
2374
|
-
# 22:06:45.771 RP --- 32:155617 18:005904 --:------ 3222 011 00-02-08- 0009000F000F100E
|
|
2375
|
-
# 13:30:26.792 RP --- 32:155617 18:005904 --:------ 3222 012 00-01-09- 090009000F000F100E
|
|
2376
|
-
# 06:29:40.767 RP --- 32:155617 18:005904 --:------ 3222 013 00-00-0A-00090009000F000F100E
|
|
2377
|
-
|
|
2424
|
+
# WIP: unknown, HVAC
|
|
2425
|
+
def parser_3222(payload: str, msg: Message) -> dict[str, Any]:
|
|
2378
2426
|
assert payload[:2] == "00"
|
|
2379
2427
|
|
|
2428
|
+
# e.g. RP|3222|00FE00 (payload = 3 bytes)
|
|
2380
2429
|
if msg.len == 3:
|
|
2381
|
-
assert payload[4:] == "00"
|
|
2382
|
-
|
|
2430
|
+
assert payload[4:] == "00" # aka length 0
|
|
2431
|
+
|
|
2432
|
+
return {
|
|
2433
|
+
"_value": f"0x{payload[2:4]}",
|
|
2434
|
+
}
|
|
2383
2435
|
|
|
2436
|
+
# e.g. RP|3222|000604000F100E (payload > 3 bytes)
|
|
2384
2437
|
return {
|
|
2385
|
-
"
|
|
2386
|
-
"length": payload[4:6],
|
|
2387
|
-
"
|
|
2438
|
+
"offset": f"0x{payload[2:4]}", # bytes
|
|
2439
|
+
"length": f"0x{payload[4:6]}", # bytes
|
|
2440
|
+
"_data": f"{'..' * int(payload[2:4])}{payload[6:]}",
|
|
2388
2441
|
}
|
|
2389
2442
|
|
|
2390
2443
|
|
|
2391
|
-
|
|
2392
|
-
def parser_3223(payload, msg) -> dict:
|
|
2393
|
-
|
|
2444
|
+
# unknown_3223, from OTB
|
|
2445
|
+
def parser_3223(payload: str, msg: Message) -> dict[str, Any]:
|
|
2394
2446
|
assert int(payload[2:], 16) <= 0xC8, _INFORM_DEV_MSG
|
|
2395
2447
|
|
|
2396
2448
|
return {
|
|
2397
|
-
|
|
2449
|
+
"_payload": payload,
|
|
2398
2450
|
SZ_VALUE: int(payload[2:], 16),
|
|
2399
2451
|
}
|
|
2400
2452
|
|
|
2401
2453
|
|
|
2402
|
-
|
|
2403
|
-
def parser_3b00(payload, msg) ->
|
|
2454
|
+
# actuator_sync (aka sync_tpi: TPI cycle sync)
|
|
2455
|
+
def parser_3b00(payload: str, msg: Message) -> PayDictT._3B00:
|
|
2404
2456
|
# system timing master: the device that sends I/FCC8 pkt controls the heater relay
|
|
2405
2457
|
"""Decode a 3B00 packet (actuator_sync).
|
|
2406
2458
|
|
|
@@ -2421,7 +2473,7 @@ def parser_3b00(payload, msg) -> dict:
|
|
|
2421
2473
|
# 063 I --- 01:078710 --:------ 01:078710 3B00 002 FCC8
|
|
2422
2474
|
# 064 I --- 01:078710 --:------ 01:078710 3B00 002 FCC8
|
|
2423
2475
|
|
|
2424
|
-
def complex_idx(payload, msg) -> dict: # has complex idx
|
|
2476
|
+
def complex_idx(payload: str, msg: Message) -> dict: # has complex idx
|
|
2425
2477
|
if (
|
|
2426
2478
|
msg.verb == I_
|
|
2427
2479
|
and msg.src.type in (DEV_TYPE_MAP.CTL, DEV_TYPE_MAP.PRG)
|
|
@@ -2437,19 +2489,18 @@ def parser_3b00(payload, msg) -> dict:
|
|
|
2437
2489
|
DEV_TYPE_MAP.CTL: FC,
|
|
2438
2490
|
DEV_TYPE_MAP.BDR: "00",
|
|
2439
2491
|
DEV_TYPE_MAP.PRG: FC,
|
|
2440
|
-
}.get(
|
|
2441
|
-
msg.src.type, "00"
|
|
2442
|
-
) # DEX
|
|
2492
|
+
}.get(msg.src.type, "00") # DEX
|
|
2443
2493
|
assert payload[2:] == "C8", payload[2:] # Could it be a percentage?
|
|
2444
2494
|
|
|
2445
2495
|
return {
|
|
2446
|
-
**complex_idx(payload[:2], msg),
|
|
2447
|
-
"actuator_sync":
|
|
2496
|
+
**complex_idx(payload[:2], msg), # type: ignore[typeddict-item]
|
|
2497
|
+
"actuator_sync": hex_to_bool(payload[2:]),
|
|
2448
2498
|
}
|
|
2449
2499
|
|
|
2450
2500
|
|
|
2451
|
-
|
|
2452
|
-
def parser_3ef0(payload, msg) ->
|
|
2501
|
+
# actuator_state
|
|
2502
|
+
def parser_3ef0(payload: str, msg: Message) -> PayDictT._3EF0 | PayDictT._JASPER:
|
|
2503
|
+
result: dict[str, Any]
|
|
2453
2504
|
|
|
2454
2505
|
if msg.src.type == DEV_TYPE_MAP.JIM: # Honeywell Jasper
|
|
2455
2506
|
assert msg.len == 20, f"expecting len 20, got: {msg.len}"
|
|
@@ -2462,12 +2513,15 @@ def parser_3ef0(payload, msg) -> dict:
|
|
|
2462
2513
|
assert msg.len in (3, 6, 9), f"Invalid payload length: {msg.len}"
|
|
2463
2514
|
# assert payload[:2] == "00", f"Invalid payload context: {payload[:2]}"
|
|
2464
2515
|
|
|
2516
|
+
# NOTE: some [2:4] appear to intend 0x00-0x64 (high_res=False), instead of 0x00-0xC8
|
|
2517
|
+
# NOTE: for best compatibility, all will be switched to 0x00-0xC8 (high_res=True)
|
|
2518
|
+
|
|
2465
2519
|
if msg.len == 3: # I|BDR|003 (the following are the only two payloads ever seen)
|
|
2466
2520
|
# .I --- 13:042805 --:------ 13:042805 3EF0 003 0000FF
|
|
2467
2521
|
# .I --- 13:023770 --:------ 13:023770 3EF0 003 00C8FF
|
|
2468
2522
|
assert payload[2:4] in ("00", "C8"), f"byte 1: {payload[2:4]} (not 00/C8)"
|
|
2469
2523
|
assert payload[4:6] == "FF", f"byte 2: {payload[4:6]} (not FF)"
|
|
2470
|
-
mod_level =
|
|
2524
|
+
mod_level = hex_to_percent(payload[2:4], high_res=True)
|
|
2471
2525
|
|
|
2472
2526
|
else: # msg.len >= 6: # RP|OTB|006 (to RQ|CTL/HGI/RFG)
|
|
2473
2527
|
# RP --- 10:004598 34:003611 --:------ 3EF0 006 0000100000FF
|
|
@@ -2475,7 +2529,7 @@ def parser_3ef0(payload, msg) -> dict:
|
|
|
2475
2529
|
# RP --- 10:138822 01:187666 --:------ 3EF0 006 0064100C00FF
|
|
2476
2530
|
# RP --- 10:138822 01:187666 --:------ 3EF0 006 0064100200FF
|
|
2477
2531
|
assert payload[4:6] in ("00", "10", "11"), f"byte 2: {payload[4:6]}"
|
|
2478
|
-
mod_level =
|
|
2532
|
+
mod_level = hex_to_percent(payload[2:4], high_res=True) # 00-64/C8 (or FF)
|
|
2479
2533
|
|
|
2480
2534
|
result = {
|
|
2481
2535
|
"modulation_level": mod_level, # 0008[2:4], 3EF1[10:12]
|
|
@@ -2489,13 +2543,14 @@ def parser_3ef0(payload, msg) -> dict:
|
|
|
2489
2543
|
|
|
2490
2544
|
result.update(
|
|
2491
2545
|
{
|
|
2492
|
-
"_flags_3":
|
|
2546
|
+
"_flags_3": hex_to_flag8(payload[6:8]),
|
|
2493
2547
|
"ch_active": bool(int(payload[6:8], 0x10) & 1 << 1),
|
|
2494
2548
|
"dhw_active": bool(int(payload[6:8], 0x10) & 1 << 2),
|
|
2495
|
-
"
|
|
2549
|
+
"cool_active": bool(int(payload[6:8], 0x10) & 1 << 4),
|
|
2550
|
+
"flame_on": bool(int(payload[6:8], 0x10) & 1 << 3), # flame_on
|
|
2496
2551
|
"_unknown_4": payload[8:10], # FF, 00, 01, 0A
|
|
2497
|
-
"_unknown_5": payload[10:12], # FF, 1C, ?others
|
|
2498
|
-
}
|
|
2552
|
+
"_unknown_5": payload[10:12], # FF, 13, 1C, ?others
|
|
2553
|
+
} # TODO: change to flame_active?
|
|
2499
2554
|
)
|
|
2500
2555
|
|
|
2501
2556
|
if msg.len >= 9: # I/RP|OTB|009 (R8820A only?)
|
|
@@ -2506,10 +2561,10 @@ def parser_3ef0(payload, msg) -> dict:
|
|
|
2506
2561
|
|
|
2507
2562
|
result.update(
|
|
2508
2563
|
{
|
|
2509
|
-
"_flags_6":
|
|
2564
|
+
"_flags_6": hex_to_flag8(payload[12:14]),
|
|
2510
2565
|
"ch_enabled": bool(int(payload[12:14], 0x10) & 1 << 0),
|
|
2511
2566
|
"ch_setpoint": int(payload[14:16], 0x10),
|
|
2512
|
-
"max_rel_modulation":
|
|
2567
|
+
"max_rel_modulation": hex_to_percent(payload[16:18], high_res=True),
|
|
2513
2568
|
}
|
|
2514
2569
|
)
|
|
2515
2570
|
|
|
@@ -2521,9 +2576,9 @@ def parser_3ef0(payload, msg) -> dict:
|
|
|
2521
2576
|
assert payload[4:6] in ("00", "10", "11", "FF"), f"byte 2: {payload[4:6]}"
|
|
2522
2577
|
|
|
2523
2578
|
assert "_flags_3" not in result or (
|
|
2524
|
-
payload[6:8] == "FF" or int(payload[6:8], 0x10) &
|
|
2525
|
-
), f
|
|
2526
|
-
# only
|
|
2579
|
+
payload[6:8] == "FF" or int(payload[6:8], 0x10) & 0b10100000 == 0
|
|
2580
|
+
), f"byte 3: {result['_flags_3']}"
|
|
2581
|
+
# only 10:040239 does 0b01000000, only Itho Autotemp does 0b00010000
|
|
2527
2582
|
|
|
2528
2583
|
assert "_unknown_4" not in result or (
|
|
2529
2584
|
payload[8:10] in ("FF", "00", "01", "02", "04", "0A")
|
|
@@ -2531,24 +2586,22 @@ def parser_3ef0(payload, msg) -> dict:
|
|
|
2531
2586
|
# only 10:040239 does 04
|
|
2532
2587
|
|
|
2533
2588
|
assert "_unknown_5" not in result or (
|
|
2534
|
-
payload[10:12] in ("00", "1C", "FF")
|
|
2589
|
+
payload[10:12] in ("00", "13", "1C", "FF")
|
|
2535
2590
|
), f"byte 5: {payload[10:12]}"
|
|
2536
2591
|
|
|
2537
2592
|
assert "_flags_6" not in result or (
|
|
2538
2593
|
int(payload[12:14], 0x10) & 0b11111100 == 0
|
|
2539
|
-
), f
|
|
2594
|
+
), f"byte 6: {result['_flags_6']}"
|
|
2540
2595
|
|
|
2541
|
-
except AssertionError as
|
|
2596
|
+
except AssertionError as err:
|
|
2542
2597
|
_LOGGER.warning(
|
|
2543
|
-
f"{msg!r} < {_INFORM_DEV_MSG} ({
|
|
2598
|
+
f"{msg!r} < {_INFORM_DEV_MSG} ({err}), with a description of your system"
|
|
2544
2599
|
)
|
|
2600
|
+
return result # type: ignore[return-value]
|
|
2545
2601
|
|
|
2546
|
-
return result
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
@parser_decorator # actuator_cycle
|
|
2550
|
-
def parser_3ef1(payload, msg) -> dict:
|
|
2551
2602
|
|
|
2603
|
+
# actuator_cycle
|
|
2604
|
+
def parser_3ef1(payload: str, msg: Message) -> PayDictT._3EF1 | PayDictT._JASPER:
|
|
2552
2605
|
if msg.src.type == DEV_TYPE_MAP.JIM: # Honeywell Jasper, DEX
|
|
2553
2606
|
assert msg.len == 18, f"expecting len 18, got: {msg.len}"
|
|
2554
2607
|
return {
|
|
@@ -2565,15 +2618,10 @@ def parser_3ef1(payload, msg) -> dict:
|
|
|
2565
2618
|
"blob": payload[8:],
|
|
2566
2619
|
}
|
|
2567
2620
|
|
|
2621
|
+
percent = hex_to_percent(payload[10:12])
|
|
2622
|
+
|
|
2568
2623
|
if payload[12:] == "FF": # is BDR
|
|
2569
|
-
|
|
2570
|
-
# re.compile(r"^00[0-9A-F]{10}FF").match(payload)
|
|
2571
|
-
# ), "doesn't match: " + r"^00[0-9A-F]{10}FF"
|
|
2572
|
-
assert int(payload[2:6], 16) <= 7200, f"byte 1: {payload[2:6]}"
|
|
2573
|
-
# assert payload[6:10] in ("87B3", "9DFA", "DCE1", "E638", "F8F7") or (
|
|
2574
|
-
# int(payload[6:10], 16) <= 7200
|
|
2575
|
-
# ), f"byte 3: {payload[6:10]}"
|
|
2576
|
-
assert percent_from_hex(payload[10:12]) in (0, 1), f"byte 5: {payload[10:12]}"
|
|
2624
|
+
assert percent is None or percent in (0, 1), f"byte 5: {payload[10:12]}"
|
|
2577
2625
|
|
|
2578
2626
|
else: # is OTB
|
|
2579
2627
|
# assert (
|
|
@@ -2581,114 +2629,329 @@ def parser_3ef1(payload, msg) -> dict:
|
|
|
2581
2629
|
# ), "doesn't match: " + r"^00[0-9A-F]{10}10"
|
|
2582
2630
|
assert payload[2:6] == "7FFF", f"byte 1: {payload[2:6]}"
|
|
2583
2631
|
assert payload[6:10] == "003C", f"byte 3: {payload[6:10]}" # 60 seconds
|
|
2584
|
-
assert
|
|
2632
|
+
assert percent is None or percent <= 1, f"byte 5: {payload[10:12]}"
|
|
2585
2633
|
|
|
2586
2634
|
cycle_countdown = None if payload[2:6] == "7FFF" else int(payload[2:6], 16)
|
|
2635
|
+
if cycle_countdown is not None:
|
|
2636
|
+
if cycle_countdown > 0x7FFF:
|
|
2637
|
+
cycle_countdown -= 0x10000
|
|
2638
|
+
assert cycle_countdown < 7200, f"byte 1: {payload[2:6]}" # 7200 seconds
|
|
2639
|
+
|
|
2640
|
+
actuator_countdown = None if payload[6:10] == "7FFF" else int(payload[6:10], 16)
|
|
2641
|
+
if actuator_countdown is not None:
|
|
2642
|
+
if actuator_countdown > 0x7FFF: # "87B3", "9DFA", "DCE1", "E638", "F8F7"
|
|
2643
|
+
# actuator_countdown = 0x10000 - actuator_countdown + cycle_countdown
|
|
2644
|
+
actuator_countdown = cycle_countdown # Needs work
|
|
2645
|
+
# assert actuator_countdown <= cycle_countdown, f"byte 3: {payload[6:10]}"
|
|
2587
2646
|
|
|
2588
2647
|
return {
|
|
2589
|
-
"modulation_level":
|
|
2590
|
-
"actuator_countdown":
|
|
2648
|
+
"modulation_level": percent, # 0008[2:4], 3EF0[2:4]
|
|
2649
|
+
"actuator_countdown": actuator_countdown,
|
|
2591
2650
|
"cycle_countdown": cycle_countdown,
|
|
2592
|
-
|
|
2651
|
+
"_unknown_0": payload[12:],
|
|
2593
2652
|
}
|
|
2594
2653
|
|
|
2595
2654
|
|
|
2596
|
-
|
|
2597
|
-
def parser_4401(payload, msg) -> dict:
|
|
2598
|
-
|
|
2655
|
+
# timestamp, HVAC
|
|
2656
|
+
def parser_4401(payload: str, msg: Message) -> dict[str, Any]:
|
|
2599
2657
|
if msg.verb == RP:
|
|
2600
2658
|
return {}
|
|
2601
2659
|
|
|
2602
|
-
#
|
|
2603
|
-
#
|
|
2660
|
+
# 2022-07-28T14:21:38.895354 095 W --- 37:010164 37:010151 --:------ 4401 020 10 7E-E99E90C8 00-E99E90C7-3BFF 7E-E99E90C8-000B
|
|
2661
|
+
# 2022-07-28T14:21:57.414447 076 RQ --- 20:225479 20:257336 --:------ 4401 020 10 2E-E99E90DB 00-00000000-0000 00-00000000-000B
|
|
2662
|
+
# 2022-07-28T14:21:57.625474 045 I --- 20:257336 20:225479 --:------ 4401 020 10 2E-E99E90DB 00-E99E90DA-F0FF BD-00000000-000A
|
|
2663
|
+
# 2022-07-28T14:22:02.932576 088 RQ --- 37:010188 20:257336 --:------ 4401 020 10 22-E99E90E0 00-00000000-0000 00-00000000-000B
|
|
2664
|
+
# 2022-07-28T14:22:03.053744 045 I --- 20:257336 37:010188 --:------ 4401 020 10 22-E99E90E0 00-E99E90E0-75FF BD-00000000-000A
|
|
2665
|
+
# 2022-07-28T14:22:20.516363 045 RQ --- 20:255710 20:257400 --:------ 4401 020 10 0B-E99E90F2 00-00000000-0000 00-00000000-000B
|
|
2666
|
+
# 2022-07-28T14:22:20.571640 085 I --- 20:255251 20:229597 --:------ 4401 020 10 39-E99E90F1 00-E99E90F1-5CFF 40-00000000-000A
|
|
2667
|
+
# 2022-07-28T14:22:20.648696 058 I --- 20:257400 20:255710 --:------ 4401 020 10 0B-E99E90F2 00-E99E90F1-D4FF DA-00000000-000B
|
|
2668
|
+
|
|
2669
|
+
# 2022-11-03T23:00:04.854479 088 RQ --- 20:256717 37:013150 --:------ 4401 020 10 00-00259261 00-00000000-0000 00-00000000-0063
|
|
2670
|
+
# 2022-11-03T23:00:05.102491 045 I --- 37:013150 20:256717 --:------ 4401 020 10 00-00259261 00-000C9E4C-1800 00-00000000-0063
|
|
2671
|
+
# 2022-11-03T23:00:17.820659 072 I --- 20:256112 20:255825 --:------ 4401 020 10 00-00F1EB91 00-00E8871B-B700 00-00000000-0063
|
|
2672
|
+
# 2022-11-03T23:01:25.495391 065 I --- 20:257732 20:257680 --:------ 4401 020 10 00-002E9C98 00-00107923-9E00 00-00000000-0063
|
|
2673
|
+
# 2022-11-03T23:01:33.753467 066 RQ --- 20:257732 20:256112 --:------ 4401 020 10 00-0010792C 00-00000000-0000 00-00000000-0063
|
|
2674
|
+
# 2022-11-03T23:01:33.997485 072 I --- 20:256112 20:257732 --:------ 4401 020 10 00-0010792C 00-00E88767-AD00 00-00000000-0063
|
|
2675
|
+
# 2022-11-03T23:01:52.391989 090 I --- 20:256717 20:255301 --:------ 4401 020 10 00-009870E1 00-002592CC-6300 00-00000000-0063
|
|
2676
|
+
|
|
2677
|
+
def hex_to_epoch(seqx: str) -> None | str: # seconds since 1-1-1970
|
|
2678
|
+
if seqx == "00" * 4:
|
|
2679
|
+
return None
|
|
2680
|
+
return str(
|
|
2681
|
+
dt.fromtimestamp(int(seqx, 16))
|
|
2682
|
+
) # - int(payload[22:26], 16) * 15 * 60))
|
|
2683
|
+
|
|
2684
|
+
# 10 7E-E99E90C8 00-E99E90C7-3BFF 7E-E99E90C8-000B
|
|
2685
|
+
# hex(int(dt.fromisoformat("2022-07-28T14:21:38.895354").timestamp())).upper()
|
|
2686
|
+
# '0x62E20ED2'
|
|
2687
|
+
|
|
2688
|
+
assert payload[:2] == "10", payload[:2]
|
|
2689
|
+
assert payload[12:14] == "00", payload[12:14]
|
|
2690
|
+
assert payload[36:38] == "00", payload[36:38]
|
|
2691
|
+
|
|
2692
|
+
assert msg.verb != I_ or payload[24:26] in ("00", "7C", "FF"), payload[24:26]
|
|
2693
|
+
assert msg.verb != W_ or payload[24:26] in ("7C", "FF"), payload[24:26]
|
|
2694
|
+
assert msg.verb != RQ or payload[24:26] == "00", payload[24:26]
|
|
2695
|
+
|
|
2696
|
+
assert msg.verb != RQ or payload[14:22] == "00" * 4, payload[14:22]
|
|
2697
|
+
assert msg.verb != W_ or payload[28:36] != "00" * 4, payload[28:36]
|
|
2698
|
+
|
|
2699
|
+
assert payload[38:40] in ("08", "09", "0A", "0B", "63"), payload[38:40]
|
|
2700
|
+
|
|
2701
|
+
# assert payload[2:4] == payload[26:28], f"{payload[2:4]}, {payload[26:24]}"
|
|
2604
2702
|
|
|
2605
2703
|
return {
|
|
2606
|
-
"
|
|
2607
|
-
"
|
|
2608
|
-
"
|
|
2609
|
-
"
|
|
2610
|
-
|
|
2704
|
+
"last_update_dst": payload[2:4],
|
|
2705
|
+
"time_dst": hex_to_epoch(payload[4:12]),
|
|
2706
|
+
"_unknown_12": payload[12:14], # usu.00
|
|
2707
|
+
"time_src": hex_to_epoch(payload[14:22]),
|
|
2708
|
+
"offset": payload[22:24], # *15 mins?
|
|
2709
|
+
"_unknown_24": payload[24:26],
|
|
2710
|
+
"last_update_src": payload[26:28],
|
|
2711
|
+
"time_dst_receive_src": hex_to_epoch(payload[28:36]),
|
|
2712
|
+
"_unknown_36": payload[36:38], # usu.00
|
|
2713
|
+
"hops_dst_src": payload[38:40],
|
|
2714
|
+
}
|
|
2611
2715
|
|
|
2612
2716
|
|
|
2613
|
-
|
|
2614
|
-
def parser_4e01(payload, msg) -> dict:
|
|
2615
|
-
|
|
2717
|
+
# temperatures (see: 4e02) - Itho spider/autotemp
|
|
2718
|
+
def parser_4e01(payload: str, msg: Message) -> dict[str, Any]:
|
|
2719
|
+
# .I --- 02:248945 02:250708 --:------ 4E01 018 00-7FFF7FFF7FFF09077FFF7FFF7FFF7FFF-00 # 23.11, 8-group
|
|
2720
|
+
# .I --- 02:250984 02:250704 --:------ 4E01 018 00-7FFF7FFF7FFF7FFF08387FFF7FFF7FFF-00 # 21.04
|
|
2616
2721
|
|
|
2722
|
+
num_groups = int((msg.len - 2) / 2) # e.g. (18 - 2) / 2
|
|
2723
|
+
assert num_groups * 2 == msg.len - 2, (
|
|
2724
|
+
_INFORM_DEV_MSG
|
|
2725
|
+
) # num_groups: len 018 (8-group, 2+8*4), or 026 (12-group, 2+12*4)
|
|
2617
2726
|
|
|
2618
|
-
|
|
2619
|
-
def parser_4e02(payload, msg) -> dict:
|
|
2727
|
+
x, y = 0, 2 + num_groups * 4
|
|
2620
2728
|
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2729
|
+
assert payload[x : x + 2] == "00", _INFORM_DEV_MSG
|
|
2730
|
+
assert payload[y : y + 2] == "00", _INFORM_DEV_MSG
|
|
2731
|
+
|
|
2732
|
+
return {
|
|
2733
|
+
"temperatures": [hex_to_temp(payload[i : i + 4]) for i in range(2, y, 4)],
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
|
|
2737
|
+
# setpoint_bounds (see: 4e01) - Itho spider/autotemp
|
|
2738
|
+
def parser_4e02(
|
|
2739
|
+
payload: str, msg: Message
|
|
2740
|
+
) -> dict[str, Any]: # sent a triplets, 1 min apart
|
|
2741
|
+
# .I --- 02:248945 02:250708 --:------ 4E02 034 00-7FFF7FFF7FFF07D07FFF7FFF7FFF7FFF-02-7FFF7FFF7FFF08347FFF7FFF7FFF7FFF # 20.00-21.00
|
|
2742
|
+
# .I --- 02:250984 02:250704 --:------ 4E02 034 00-7FFF7FFF7FFF076C7FFF7FFF7FFF7FFF-02-7FFF7FFF7FFF07D07FFF7FFF7FFF7FFF #
|
|
2743
|
+
|
|
2744
|
+
num_groups = int((msg.len - 2) / 4) # e.g. (34 - 2) / 4
|
|
2745
|
+
assert num_groups * 4 == msg.len - 2, (
|
|
2746
|
+
_INFORM_DEV_MSG
|
|
2747
|
+
) # num_groups: len 034 (8-group, 2+8*4), or 050 (12-group, 2+12*4)
|
|
2748
|
+
|
|
2749
|
+
x, y = 0, 2 + num_groups * 4
|
|
2750
|
+
|
|
2751
|
+
assert payload[x : x + 2] == "00", _INFORM_DEV_MSG # expect no context
|
|
2752
|
+
assert payload[y : y + 2] in (
|
|
2753
|
+
"02",
|
|
2754
|
+
"03",
|
|
2755
|
+
"04",
|
|
2756
|
+
"05",
|
|
2757
|
+
), _INFORM_DEV_MSG # mode: cool/heat?
|
|
2758
|
+
|
|
2759
|
+
setpoints = [
|
|
2760
|
+
(hex_to_temp(payload[x + i :][:4]), hex_to_temp(payload[y + i :][:4]))
|
|
2761
|
+
for i in range(2, y, 4)
|
|
2762
|
+
] # lower, upper setpoints
|
|
2763
|
+
|
|
2764
|
+
return {
|
|
2765
|
+
SZ_MODE: {"02": "cool", "03": "cool+", "04": "heat", "05": "cool+"}[
|
|
2766
|
+
payload[y : y + 2]
|
|
2767
|
+
],
|
|
2768
|
+
SZ_SETPOINT_BOUNDS: [s if s != (None, None) else None for s in setpoints],
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
|
|
2772
|
+
# hvac_4e04
|
|
2773
|
+
def parser_4e04(payload: str, msg: Message) -> dict[str, Any]:
|
|
2774
|
+
MODE = {
|
|
2775
|
+
"00": "off",
|
|
2776
|
+
"01": "heat",
|
|
2777
|
+
"02": "cool",
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
assert payload[2:4] in MODE, _INFORM_DEV_MSG
|
|
2781
|
+
assert int(payload[4:], 16) < 0x40 or payload[4:] in (
|
|
2782
|
+
"FB", # error code?
|
|
2783
|
+
"FC", # error code?
|
|
2784
|
+
"FD", # error code?
|
|
2785
|
+
"FE", # error code?
|
|
2786
|
+
"FF", # N/A?
|
|
2625
2787
|
)
|
|
2626
2788
|
|
|
2789
|
+
return {
|
|
2790
|
+
SZ_MODE: MODE.get(payload[2:4], "Unknown"),
|
|
2791
|
+
"_unknown_2": payload[4:],
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
|
|
2795
|
+
# WIP: AT outdoor low - Itho spider/autotemp
|
|
2796
|
+
def parser_4e0d(payload: str, msg: Message) -> dict[str, Any]:
|
|
2797
|
+
# .I --- 02:250704 02:250984 --:------ 4E0D 002 0100 # Itho Autotemp: only(?) master -> slave
|
|
2798
|
+
# .I --- 02:250704 02:250984 --:------ 4E0D 002 0101 # why does it have a context?
|
|
2799
|
+
|
|
2800
|
+
return {
|
|
2801
|
+
"_payload": payload,
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2627
2804
|
|
|
2628
|
-
#
|
|
2629
|
-
def
|
|
2805
|
+
# AT fault circulation - Itho spider/autotemp
|
|
2806
|
+
def parser_4e14(payload: str, msg: Message) -> dict[str, Any]:
|
|
2807
|
+
"""
|
|
2808
|
+
result = "AT fault circulation";
|
|
2809
|
+
result = (((payload[2:] & 0x01) != 0x01) ? " Fault state : no fault " : " Fault state : fault ")
|
|
2810
|
+
result = (((payload[2:] & 0x02) != 0x02) ? (text4 + "Circulation state : no fault ") : (text4 + " Circulation state : fault "))
|
|
2811
|
+
"""
|
|
2812
|
+
return {}
|
|
2813
|
+
|
|
2814
|
+
|
|
2815
|
+
# wpu_state (hvac state) - Itho spider/autotemp
|
|
2816
|
+
def parser_4e15(payload: str, msg: Message) -> dict[str, Any]:
|
|
2817
|
+
# .I --- 21:034158 02:250676 --:------ 4E15 002 0000 # WPU "off" (maybe heating, but compressor off)
|
|
2818
|
+
# .I --- 21:064743 02:250708 --:------ 4E15 002 0001 # WPU cooling active
|
|
2819
|
+
# .I --- 21:057565 02:250677 --:------ 4E15 002 0002 # WPU heating, compressor active
|
|
2820
|
+
# .I --- 21:064743 02:250708 --:------ 4E15 002 0004 # WPU in "DHW mode" boiler active
|
|
2821
|
+
# .I --- 21:033160 02:250704 --:------ 4E15 002 0005 # 0x03, and 0x06 not seen in the wild
|
|
2822
|
+
|
|
2823
|
+
if int(payload[2:], 16) & 0xF0:
|
|
2824
|
+
pass
|
|
2825
|
+
|
|
2826
|
+
# If none of these, then is 'Off'
|
|
2827
|
+
SZ_COOLING = "is_cooling"
|
|
2828
|
+
SZ_DHW_ING = "is_dhw_ing"
|
|
2829
|
+
SZ_HEATING = "is_heating"
|
|
2830
|
+
# SZ_PUMPING = "is_pumping"
|
|
2831
|
+
|
|
2832
|
+
assert int(payload[2:], 16) & 0xF8 == 0x00, (
|
|
2833
|
+
_INFORM_DEV_MSG
|
|
2834
|
+
) # check for unknown bit flags
|
|
2835
|
+
if int(payload[2:], 16) & 0x03 == 0x03: # is_cooling *and* is_heating (+/- DHW)
|
|
2836
|
+
raise TypeError # TODO: Use local exception & ?Move to higher layer
|
|
2837
|
+
assert int(payload[2:], 16) & 0x07 != 0x06, _INFORM_DEV_MSG # can't heat and DHW
|
|
2838
|
+
|
|
2839
|
+
return {
|
|
2840
|
+
"_flags": hex_to_flag8(payload[2:]),
|
|
2841
|
+
# SZ_PUMPING: bool(int(payload[2:], 16) & 0x08),
|
|
2842
|
+
SZ_DHW_ING: bool(int(payload[2:], 16) & 0x04),
|
|
2843
|
+
SZ_HEATING: bool(int(payload[2:], 16) & 0x02),
|
|
2844
|
+
SZ_COOLING: bool(int(payload[2:], 16) & 0x01),
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
|
|
2848
|
+
# TODO: hvac_4e16 - Itho spider/autotemp
|
|
2849
|
+
def parser_4e16(payload: str, msg: Message) -> dict[str, Any]:
|
|
2850
|
+
# .I --- 02:250984 02:250704 --:------ 4E16 007 00000000000000 # Itho Autotemp: slave -> master
|
|
2851
|
+
|
|
2852
|
+
assert payload == "00000000000000", _INFORM_DEV_MSG
|
|
2853
|
+
|
|
2854
|
+
return {
|
|
2855
|
+
"_payload": payload,
|
|
2856
|
+
}
|
|
2630
2857
|
|
|
2858
|
+
|
|
2859
|
+
# TODO: Fan characteristics - Itho
|
|
2860
|
+
def parser_4e20(payload: str, msg: Message) -> dict[str, Any]:
|
|
2861
|
+
"""
|
|
2862
|
+
result = "Fan characteristics: "
|
|
2863
|
+
result += [C[ABC][210] hex_to_sint32[i:i+4] for i in range(2, 34, 4)]
|
|
2864
|
+
"""
|
|
2865
|
+
return {}
|
|
2866
|
+
|
|
2867
|
+
|
|
2868
|
+
# TODO: Potentiometer control - Itho
|
|
2869
|
+
def parser_4e21(payload: str, msg: Message) -> dict[str, Any]:
|
|
2870
|
+
"""
|
|
2871
|
+
result = "Potentiometer control: "
|
|
2872
|
+
result += "Rel min: " + hex_to_sint16(data[2:4]) # 16 bit, 2's complement
|
|
2873
|
+
result += "Min of rel min: " + hex_to_sint16(data[4:6])
|
|
2874
|
+
result += "Abs min: " + hex_to_sint16(data[6:8])
|
|
2875
|
+
result += "Rel max: " + hex_to_sint16(data[8:10])
|
|
2876
|
+
result += "Max rel: " + hex_to_sint16(data[10:12])
|
|
2877
|
+
result += "Abs max: " + hex_to_sint16(data[12:14]))
|
|
2878
|
+
"""
|
|
2879
|
+
return {}
|
|
2880
|
+
|
|
2881
|
+
|
|
2882
|
+
# # faked puzzle pkt shouldn't be decorated
|
|
2883
|
+
def parser_7fff(payload: str, _: Message) -> dict[str, Any]:
|
|
2631
2884
|
if payload[:2] != "00":
|
|
2632
2885
|
_LOGGER.debug("Invalid/deprecated Puzzle packet")
|
|
2633
2886
|
return {
|
|
2634
2887
|
"msg_type": payload[:2],
|
|
2635
|
-
SZ_PAYLOAD:
|
|
2888
|
+
SZ_PAYLOAD: hex_to_str(payload[2:]),
|
|
2636
2889
|
}
|
|
2637
2890
|
|
|
2638
2891
|
if payload[2:4] not in LOOKUP_PUZZ:
|
|
2639
2892
|
_LOGGER.debug("Invalid/deprecated Puzzle packet")
|
|
2640
2893
|
return {
|
|
2641
2894
|
"msg_type": payload[2:4],
|
|
2642
|
-
"message":
|
|
2895
|
+
"message": hex_to_str(payload[4:]),
|
|
2643
2896
|
}
|
|
2644
2897
|
|
|
2645
2898
|
result: dict[str, None | str] = {}
|
|
2646
|
-
if payload[2:4]
|
|
2899
|
+
if int(payload[2:4]) >= int("20", 16):
|
|
2900
|
+
dtm = dt.fromtimestamp(int(payload[4:16], 16) / 1e7) # TZ-naive
|
|
2901
|
+
result["datetime"] = dtm.isoformat(timespec="milliseconds")
|
|
2902
|
+
elif payload[2:4] != "13":
|
|
2647
2903
|
dtm = dt.fromtimestamp(int(payload[4:16], 16) / 1000) # TZ-naive
|
|
2648
2904
|
result["datetime"] = dtm.isoformat(timespec="milliseconds")
|
|
2649
2905
|
|
|
2650
2906
|
msg_type = LOOKUP_PUZZ.get(payload[2:4], SZ_PAYLOAD)
|
|
2651
2907
|
|
|
2652
2908
|
if payload[2:4] == "11":
|
|
2653
|
-
|
|
2654
|
-
result[msg_type] = f"{
|
|
2909
|
+
mesg = hex_to_str(payload[16:])
|
|
2910
|
+
result[msg_type] = f"{mesg[:4]}|{mesg[4:6]}|{mesg[6:]}"
|
|
2655
2911
|
|
|
2656
2912
|
elif payload[2:4] == "13":
|
|
2657
|
-
result[msg_type] =
|
|
2913
|
+
result[msg_type] = hex_to_str(payload[4:])
|
|
2658
2914
|
|
|
2659
2915
|
elif payload[2:4] == "7F":
|
|
2660
2916
|
result[msg_type] = payload[4:]
|
|
2661
2917
|
|
|
2662
2918
|
else:
|
|
2663
|
-
result[msg_type] =
|
|
2919
|
+
result[msg_type] = hex_to_str(payload[16:])
|
|
2664
2920
|
|
|
2665
2921
|
return {**result, "parser": f"v{VERSION}"}
|
|
2666
2922
|
|
|
2667
2923
|
|
|
2668
|
-
|
|
2669
|
-
def parser_unknown(payload, msg) -> dict:
|
|
2924
|
+
def parser_unknown(payload: str, msg: Message) -> dict[str, Any]:
|
|
2670
2925
|
# TODO: it may be useful to generically search payloads for hex_ids, commands, etc.
|
|
2671
2926
|
|
|
2672
2927
|
# These are generic parsers
|
|
2673
2928
|
if msg.len == 2 and payload[:2] == "00":
|
|
2674
2929
|
return {
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
payload[2:], int(payload[2:], 16)
|
|
2678
|
-
),
|
|
2930
|
+
"_payload": payload,
|
|
2931
|
+
"_value": {"00": False, "C8": True}.get(payload[2:], int(payload[2:], 16)),
|
|
2679
2932
|
}
|
|
2680
2933
|
|
|
2681
2934
|
if msg.len == 3 and payload[:2] == "00":
|
|
2682
2935
|
return {
|
|
2683
|
-
|
|
2684
|
-
|
|
2936
|
+
"_payload": payload,
|
|
2937
|
+
"_value": hex_to_temp(payload[2:]),
|
|
2685
2938
|
}
|
|
2686
2939
|
|
|
2687
2940
|
raise NotImplementedError
|
|
2688
2941
|
|
|
2689
2942
|
|
|
2690
|
-
|
|
2943
|
+
_PAYLOAD_PARSERS = {
|
|
2691
2944
|
k[7:].upper(): v
|
|
2692
2945
|
for k, v in locals().items()
|
|
2693
2946
|
if callable(v) and k.startswith("parser_") and len(k) == 11
|
|
2694
2947
|
}
|
|
2948
|
+
|
|
2949
|
+
|
|
2950
|
+
def parse_payload(msg: Message) -> dict | list[dict]:
|
|
2951
|
+
result: dict | list[dict]
|
|
2952
|
+
|
|
2953
|
+
result = _PAYLOAD_PARSERS.get(msg.code, parser_unknown)(msg._pkt.payload, msg)
|
|
2954
|
+
if isinstance(result, dict) and msg.seqn.isnumeric(): # e.g. 22F1/3
|
|
2955
|
+
result["seqx_num"] = msg.seqn
|
|
2956
|
+
|
|
2957
|
+
return result
|