ramses-rf 0.22.2__py3-none-any.whl → 0.51.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ramses_cli/__init__.py +18 -0
- ramses_cli/client.py +597 -0
- ramses_cli/debug.py +20 -0
- ramses_cli/discovery.py +405 -0
- ramses_cli/utils/cat_slow.py +17 -0
- ramses_cli/utils/convert.py +60 -0
- ramses_rf/__init__.py +31 -10
- ramses_rf/binding_fsm.py +787 -0
- ramses_rf/const.py +124 -105
- ramses_rf/database.py +297 -0
- ramses_rf/device/__init__.py +69 -39
- ramses_rf/device/base.py +187 -376
- ramses_rf/device/heat.py +540 -552
- ramses_rf/device/hvac.py +286 -171
- ramses_rf/dispatcher.py +153 -177
- ramses_rf/entity_base.py +478 -361
- ramses_rf/exceptions.py +82 -0
- ramses_rf/gateway.py +378 -514
- ramses_rf/helpers.py +57 -19
- ramses_rf/py.typed +0 -0
- ramses_rf/schemas.py +148 -194
- ramses_rf/system/__init__.py +16 -23
- ramses_rf/system/faultlog.py +363 -0
- ramses_rf/system/heat.py +295 -302
- ramses_rf/system/schedule.py +312 -198
- ramses_rf/system/zones.py +318 -238
- ramses_rf/version.py +2 -8
- ramses_rf-0.51.1.dist-info/METADATA +72 -0
- ramses_rf-0.51.1.dist-info/RECORD +55 -0
- {ramses_rf-0.22.2.dist-info → ramses_rf-0.51.1.dist-info}/WHEEL +1 -2
- ramses_rf-0.51.1.dist-info/entry_points.txt +2 -0
- {ramses_rf-0.22.2.dist-info → ramses_rf-0.51.1.dist-info/licenses}/LICENSE +1 -1
- ramses_tx/__init__.py +160 -0
- {ramses_rf/protocol → ramses_tx}/address.py +65 -59
- ramses_tx/command.py +1454 -0
- ramses_tx/const.py +903 -0
- ramses_tx/exceptions.py +92 -0
- {ramses_rf/protocol → ramses_tx}/fingerprints.py +56 -15
- {ramses_rf/protocol → ramses_tx}/frame.py +132 -131
- ramses_tx/gateway.py +338 -0
- ramses_tx/helpers.py +883 -0
- {ramses_rf/protocol → ramses_tx}/logger.py +67 -53
- {ramses_rf/protocol → ramses_tx}/message.py +155 -191
- ramses_tx/opentherm.py +1260 -0
- ramses_tx/packet.py +210 -0
- ramses_tx/parsers.py +2957 -0
- ramses_tx/protocol.py +801 -0
- ramses_tx/protocol_fsm.py +672 -0
- ramses_tx/py.typed +0 -0
- {ramses_rf/protocol → ramses_tx}/ramses.py +262 -185
- {ramses_rf/protocol → ramses_tx}/schemas.py +150 -133
- ramses_tx/transport.py +1471 -0
- ramses_tx/typed_dicts.py +492 -0
- ramses_tx/typing.py +181 -0
- ramses_tx/version.py +4 -0
- ramses_rf/discovery.py +0 -398
- ramses_rf/protocol/__init__.py +0 -59
- ramses_rf/protocol/backports.py +0 -42
- ramses_rf/protocol/command.py +0 -1561
- ramses_rf/protocol/const.py +0 -697
- ramses_rf/protocol/exceptions.py +0 -111
- ramses_rf/protocol/helpers.py +0 -390
- ramses_rf/protocol/opentherm.py +0 -1170
- ramses_rf/protocol/packet.py +0 -235
- ramses_rf/protocol/parsers.py +0 -2673
- ramses_rf/protocol/protocol.py +0 -613
- ramses_rf/protocol/transport.py +0 -1011
- ramses_rf/protocol/version.py +0 -10
- ramses_rf/system/hvac.py +0 -82
- ramses_rf-0.22.2.dist-info/METADATA +0 -64
- ramses_rf-0.22.2.dist-info/RECORD +0 -42
- ramses_rf-0.22.2.dist-info/top_level.txt +0 -1
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
#
|
|
4
2
|
"""RAMSES RF - a RAMSES-II protocol decoder & analyser.
|
|
5
3
|
|
|
6
4
|
Provide the base class for commands (constructed/sent packets) and packets.
|
|
7
5
|
"""
|
|
6
|
+
|
|
8
7
|
from __future__ import annotations
|
|
9
8
|
|
|
10
9
|
import logging
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
11
|
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
14
|
-
from .
|
|
12
|
+
from . import exceptions as exc
|
|
13
|
+
from .address import ALL_DEV_ADDR, NON_DEV_ADDR, Address, pkt_addrs
|
|
14
|
+
from .const import COMMAND_REGEX, DEV_ROLE_MAP, DEV_TYPE_MAP
|
|
15
15
|
from .ramses import (
|
|
16
|
-
|
|
16
|
+
CODE_IDX_ARE_COMPLEX,
|
|
17
|
+
CODE_IDX_ARE_NONE,
|
|
18
|
+
CODE_IDX_ARE_SIMPLE,
|
|
17
19
|
CODE_IDX_DOMAIN,
|
|
18
|
-
CODE_IDX_NONE,
|
|
19
|
-
CODE_IDX_SIMPLE,
|
|
20
20
|
CODES_ONLY_FROM_CTL,
|
|
21
21
|
CODES_SCHEMA,
|
|
22
22
|
CODES_WITH_ARRAYS,
|
|
@@ -25,34 +25,31 @@ from .ramses import (
|
|
|
25
25
|
|
|
26
26
|
# TODO: add _has_idx (as func return only one type, or raise)
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
# skipcq: PY-W2000
|
|
30
28
|
from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
31
29
|
I_,
|
|
32
30
|
RP,
|
|
33
31
|
RQ,
|
|
34
32
|
W_,
|
|
33
|
+
Code,
|
|
34
|
+
)
|
|
35
|
+
from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
35
36
|
F8,
|
|
36
37
|
F9,
|
|
37
38
|
FA,
|
|
38
39
|
FC,
|
|
39
40
|
FF,
|
|
40
|
-
Code,
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from .const import VerbT
|
|
43
45
|
|
|
44
|
-
DEV_MODE = __dev_mode__ and False
|
|
45
46
|
|
|
46
47
|
_LOGGER = logging.getLogger(__name__)
|
|
47
|
-
if DEV_MODE:
|
|
48
|
-
_LOGGER.setLevel(logging.DEBUG)
|
|
49
48
|
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
_PayloadT = str
|
|
55
|
-
_VerbT = str
|
|
50
|
+
HeaderT = str
|
|
51
|
+
PayloadT = str
|
|
52
|
+
_PktIdxT = str
|
|
56
53
|
|
|
57
54
|
|
|
58
55
|
class Frame:
|
|
@@ -61,17 +58,9 @@ class Frame:
|
|
|
61
58
|
`RQ --- 01:078710 10:067219 --:------ 3220 005 0000050000`
|
|
62
59
|
"""
|
|
63
60
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
verb: _VerbT
|
|
67
|
-
seqn: str # TODO: or, better as int?
|
|
68
|
-
src: Address
|
|
69
|
-
dst: Address
|
|
61
|
+
src: Address # Address | Device
|
|
62
|
+
dst: Address # Address | Device
|
|
70
63
|
_addrs: tuple[Address, Address, Address]
|
|
71
|
-
code: _CodeT
|
|
72
|
-
len_: str
|
|
73
|
-
_len: int # int(len(payload) / 2)
|
|
74
|
-
payload: _PayloadT
|
|
75
64
|
|
|
76
65
|
def __init__(self, frame: str) -> None:
|
|
77
66
|
"""Create a frame from a string.
|
|
@@ -79,30 +68,28 @@ class Frame:
|
|
|
79
68
|
Will raise InvalidPacketError if it is invalid.
|
|
80
69
|
"""
|
|
81
70
|
|
|
82
|
-
self._frame = frame
|
|
83
|
-
if not isinstance(self._frame, str):
|
|
84
|
-
raise InvalidPacketError(f"Bad frame: not a string: {type(self._frame)}")
|
|
71
|
+
self._frame: str = frame
|
|
85
72
|
if not COMMAND_REGEX.match(self._frame):
|
|
86
|
-
raise
|
|
73
|
+
raise exc.PacketInvalid(f"Bad frame: invalid structure: >>>{frame}<<<")
|
|
87
74
|
|
|
88
75
|
fields = frame.lstrip().split(" ")
|
|
89
76
|
|
|
90
|
-
self.verb = frame[:2] #
|
|
91
|
-
self.seqn = fields[1] # frame[3:6]
|
|
92
|
-
self.code = fields[5] #
|
|
93
|
-
self.len_ = fields[6] # frame[42:45]
|
|
94
|
-
self.payload = fields[7] # frame[46:].split(" ")[0]
|
|
95
|
-
self._len = int(len(self.payload) / 2)
|
|
77
|
+
self.verb: VerbT = frame[:2] # type: ignore[assignment]
|
|
78
|
+
self.seqn: str = fields[1] # . frame[3:6]
|
|
79
|
+
self.code: Code = fields[5] # type: ignore[assignment]
|
|
80
|
+
self.len_: str = fields[6] # . frame[42:45] FIXME: len_, _len & len(payload)/2
|
|
81
|
+
self.payload: PayloadT = fields[7] # frame[46:].split(" ")[0]
|
|
82
|
+
self._len: int = int(len(self.payload) / 2)
|
|
96
83
|
|
|
97
84
|
try:
|
|
98
85
|
self.src, self.dst, *self._addrs = pkt_addrs( # type: ignore[assignment]
|
|
99
86
|
" ".join(fields[i] for i in range(2, 5)) # frame[7:36]
|
|
100
87
|
)
|
|
101
|
-
except
|
|
102
|
-
raise
|
|
88
|
+
except exc.PacketInvalid as err: # will be: InvalidAddrSetError
|
|
89
|
+
raise exc.PacketInvalid("Bad frame: invalid address set") from err
|
|
103
90
|
|
|
104
91
|
if len(self.payload) != int(self.len_) * 2:
|
|
105
|
-
raise
|
|
92
|
+
raise exc.PacketInvalid(
|
|
106
93
|
f"Bad frame: invalid payload: "
|
|
107
94
|
f"len({self.payload}) is not int('{self.len_}' * 2))"
|
|
108
95
|
)
|
|
@@ -115,54 +102,46 @@ class Frame:
|
|
|
115
102
|
self._has_ctl_: bool = None # type: ignore[assignment] # TODO: remove
|
|
116
103
|
self._has_payload_: bool = None # type: ignore[assignment]
|
|
117
104
|
|
|
118
|
-
self._repr = None
|
|
119
|
-
|
|
120
|
-
@classmethod # for internal use only
|
|
121
|
-
def _from_attrs(
|
|
122
|
-
cls, verb: _VerbT, *addrs, code: _CodeT, payload: _PayloadT, seqn=None
|
|
123
|
-
):
|
|
124
|
-
"""Create a frame from its attributes (args, kwargs)."""
|
|
105
|
+
self._repr: str = None # type: ignore[assignment]
|
|
125
106
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
try:
|
|
130
|
-
return cls(" ".join((verb, seqn, *addrs, code, len_, payload)))
|
|
131
|
-
except TypeError as exc:
|
|
132
|
-
raise InvalidPacketError(f"Bad frame: invalid attr: {exc}")
|
|
133
|
-
|
|
134
|
-
def _validate(self, *, strict_checking: bool = None) -> None:
|
|
107
|
+
# FIXME: this is messy
|
|
108
|
+
def _validate(self, *, strict_checking: bool = False) -> None:
|
|
135
109
|
"""Validate the frame: it may be a cmd or a (response) pkt.
|
|
136
110
|
|
|
137
111
|
Raise an exception InvalidPacketError (InvalidAddrSetError) if it is not valid.
|
|
138
112
|
"""
|
|
139
113
|
|
|
140
|
-
if (seqn := self._frame[3:6]) == "...":
|
|
141
|
-
raise InvalidPacketError(f"Bad frame: deprecated seqn: {seqn}")
|
|
142
|
-
|
|
143
|
-
if not strict_checking:
|
|
144
|
-
return
|
|
145
|
-
|
|
146
114
|
if len(self._frame[46:].split(" ")[0]) != int(self._frame[42:45]) * 2:
|
|
147
|
-
raise
|
|
115
|
+
raise exc.PacketInvalid("Bad frame: Payload length mismatch")
|
|
148
116
|
|
|
149
117
|
try:
|
|
150
|
-
self.src, self.dst, *self._addrs = pkt_addrs(self._frame[7:36])
|
|
151
|
-
|
|
152
|
-
|
|
118
|
+
# self.src, self.dst, *self._addrs = pkt_addrs(self._frame[7:36])
|
|
119
|
+
src, dst, *addrs = pkt_addrs(self._frame[7:36])
|
|
120
|
+
except exc.PacketInvalid as err: # will be: InvalidAddrSetError
|
|
121
|
+
raise exc.PacketInvalid("Bad frame: Invalid address set") from err
|
|
153
122
|
|
|
154
|
-
if
|
|
123
|
+
if not strict_checking:
|
|
155
124
|
return
|
|
156
125
|
|
|
157
|
-
|
|
158
|
-
|
|
126
|
+
try: # Strict checking: helps users avoid to constructing bad commands
|
|
127
|
+
if addrs[0] == NON_DEV_ADDR:
|
|
128
|
+
assert self.verb == I_, "wrong verb or dst addr should be present"
|
|
129
|
+
elif addrs[2] == NON_DEV_ADDR:
|
|
130
|
+
assert self.verb == I_ or src is not dst, (
|
|
131
|
+
"wrong verb or dst addr should not be src"
|
|
132
|
+
)
|
|
133
|
+
elif addrs[0] is addrs[2]:
|
|
134
|
+
assert self.verb == I_, "wrong verb or dst addr should not be src"
|
|
135
|
+
else:
|
|
136
|
+
assert self.verb in (I_, W_), "wrong verb or dst addr should be src"
|
|
137
|
+
except AssertionError as err:
|
|
138
|
+
raise exc.PacketInvalid(f"Bad frame: Invalid address set: {err}") from err
|
|
159
139
|
|
|
160
140
|
def __repr__(self) -> str:
|
|
161
141
|
"""Return a unambiguous string representation of this object."""
|
|
162
|
-
# repr(self) == repr(cls(repr(self)))
|
|
163
142
|
|
|
164
143
|
if self._repr is None:
|
|
165
|
-
self._repr = " ".join(
|
|
144
|
+
self._repr = " ".join( # type: ignore[unreachable]
|
|
166
145
|
(
|
|
167
146
|
self.verb,
|
|
168
147
|
self.seqn,
|
|
@@ -180,8 +159,13 @@ class Frame:
|
|
|
180
159
|
|
|
181
160
|
try:
|
|
182
161
|
return f"{self!r} # {self._hdr}" # code|ver|device_id|context
|
|
183
|
-
except AttributeError as
|
|
184
|
-
return f"{self!r} < {
|
|
162
|
+
except AttributeError as err:
|
|
163
|
+
return f"{self!r} < {err}"
|
|
164
|
+
|
|
165
|
+
def __eq__(self, other: object) -> bool:
|
|
166
|
+
if not hasattr(other, "_frame"):
|
|
167
|
+
return NotImplemented
|
|
168
|
+
return self._frame[4:] == other._frame[4:] # type: ignore[no-any-return]
|
|
185
169
|
|
|
186
170
|
@property
|
|
187
171
|
def _has_array(self) -> None | bool: # TODO: a mess - has false negatives
|
|
@@ -193,7 +177,7 @@ class Frame:
|
|
|
193
177
|
2309/30C9/000A packets).
|
|
194
178
|
"""
|
|
195
179
|
|
|
196
|
-
if self._has_array_ is not None: # HACK:
|
|
180
|
+
if self._has_array_ is not None: # HACK: overridden by detect_array(msg, prev)
|
|
197
181
|
return self._has_array_
|
|
198
182
|
|
|
199
183
|
# False -ves (array length is 1) are an acceptable compromise to extensive checking
|
|
@@ -202,7 +186,7 @@ class Frame:
|
|
|
202
186
|
# .I --- 01:145038 --:------ 01:145038 1FC9 018 07000806368E-FC3B0006368E-071FC906368E
|
|
203
187
|
# .I --- 01:145038 --:------ 01:145038 1FC9 018 FA000806368E-FC3B0006368E-FA1FC906368E
|
|
204
188
|
# .I --- 34:092243 --:------ 34:092243 1FC9 030 0030C9896853-002309896853-001060896853-0010E0896853-001FC9896853
|
|
205
|
-
if self.code == Code._1FC9:
|
|
189
|
+
if self.code == Code._1FC9: # type: ignore[unreachable]
|
|
206
190
|
self._has_array_ = self.verb != RQ # safe to treat all as array, even len=1
|
|
207
191
|
return self._has_array_ # don't do any checks for 1FC9 (they will fail)
|
|
208
192
|
|
|
@@ -230,9 +214,9 @@ class Frame:
|
|
|
230
214
|
if self._has_array_:
|
|
231
215
|
len_ = CODES_WITH_ARRAYS[self.code][0]
|
|
232
216
|
|
|
233
|
-
assert (
|
|
234
|
-
self._len
|
|
235
|
-
)
|
|
217
|
+
assert self._len % len_ == 0, (
|
|
218
|
+
f"{self} < array has length ({self._len}) that is not multiple of {len_}"
|
|
219
|
+
)
|
|
236
220
|
assert (
|
|
237
221
|
self.src.type in (DEV_TYPE_MAP.DTS, DEV_TYPE_MAP.DT2)
|
|
238
222
|
or self.src == self.dst # DEX
|
|
@@ -259,12 +243,17 @@ class Frame:
|
|
|
259
243
|
def _has_ctl(self) -> None | bool:
|
|
260
244
|
"""Return True if the packet is to/from a controller."""
|
|
261
245
|
|
|
246
|
+
# NB: the difference between these (_has_ctl, src, dst, -:-) and above (_has_ctl, src, -:-, src)
|
|
247
|
+
# 2000-01-01T03:00:00.000000 ... I --- 37:123456 --:------ 37:123456 31DA 030 00C8400518646427102AF82EE031FFFFFFC800C8C83FFF64640AFF0AFF00
|
|
248
|
+
# 2022-11-20T08:32:06.904058 063 RP --- 32:134446 37:171685 --:------ 31DA 030 00EF007FFF2F1B0226069A07EEFFE4F8000038988F0000EFEF1F2420FC00
|
|
249
|
+
# Maybe only use this for CH/DHW, and not HVAC?
|
|
250
|
+
|
|
262
251
|
if self._has_ctl_ is not None:
|
|
263
252
|
return self._has_ctl_
|
|
264
253
|
|
|
265
254
|
# TODO: handle RQ/RP to/from HGI/RFG, handle HVAC
|
|
266
255
|
|
|
267
|
-
if {self.src.type, self.dst.type} & {
|
|
256
|
+
if {self.src.type, self.dst.type} & { # type: ignore[unreachable]
|
|
268
257
|
DEV_TYPE_MAP.CTL,
|
|
269
258
|
DEV_TYPE_MAP.UFC,
|
|
270
259
|
DEV_TYPE_MAP.PRG,
|
|
@@ -317,12 +306,13 @@ class Frame:
|
|
|
317
306
|
|
|
318
307
|
# .I --- 34:021943 63:262142 --:------ 10E0 038 000001C8380A01... # unknown
|
|
319
308
|
# .I --- 32:168090 30:082155 --:------ 31E0 004 0000C800 # unknown
|
|
309
|
+
|
|
320
310
|
if self._has_ctl_ is None:
|
|
321
|
-
if DEV_MODE and DEV_TYPE_MAP.HGI not in (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
): # DEX
|
|
325
|
-
|
|
311
|
+
# if DEV_MODE and DEV_TYPE_MAP.HGI not in (
|
|
312
|
+
# self.src.type,
|
|
313
|
+
# self.dst.type,
|
|
314
|
+
# ): # DEX
|
|
315
|
+
# _LOGGER.warning(f"{self} # has_ctl - undetermined (99)")
|
|
326
316
|
self._has_ctl_ = False
|
|
327
317
|
|
|
328
318
|
return self._has_ctl_
|
|
@@ -343,7 +333,7 @@ class Frame:
|
|
|
343
333
|
if self._has_payload_ is not None:
|
|
344
334
|
return self._has_payload_
|
|
345
335
|
|
|
346
|
-
self._has_payload_ = not any(
|
|
336
|
+
self._has_payload_ = not any( # type: ignore[unreachable]
|
|
347
337
|
(
|
|
348
338
|
self._len == 1,
|
|
349
339
|
self.verb == RQ and self.code in RQ_NO_PAYLOAD,
|
|
@@ -381,44 +371,51 @@ class Frame:
|
|
|
381
371
|
Used to store packets in the entity's message DB. It is a superset of _idx.
|
|
382
372
|
"""
|
|
383
373
|
|
|
384
|
-
if self._ctx_ is None:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
374
|
+
if self._ctx_ is not None:
|
|
375
|
+
return self._ctx_
|
|
376
|
+
|
|
377
|
+
if self.code in ( # type: ignore[unreachable]
|
|
378
|
+
Code._0005,
|
|
379
|
+
Code._000C,
|
|
380
|
+
): # zone_idx, zone_type (device_role)
|
|
381
|
+
self._ctx_ = self.payload[:4]
|
|
382
|
+
elif self.code == Code._0404: # zone_idx, frag_idx
|
|
383
|
+
self._ctx_ = self._idx + self.payload[10:12]
|
|
384
|
+
else:
|
|
385
|
+
self._ctx_ = self._idx
|
|
394
386
|
return self._ctx_
|
|
395
387
|
|
|
396
388
|
@property
|
|
397
|
-
def _hdr(self) ->
|
|
389
|
+
def _hdr(self) -> HeaderT: # incl. self._ctx
|
|
398
390
|
"""Return the QoS header (fingerprint) of this packet (i.e. device_id/code/hdr).
|
|
399
391
|
|
|
400
392
|
Used for QoS (timeouts, retries), callbacks, etc.
|
|
401
393
|
"""
|
|
402
394
|
|
|
403
|
-
if self._hdr_ is None:
|
|
404
|
-
self._hdr_
|
|
405
|
-
|
|
395
|
+
if self._hdr_ is not None:
|
|
396
|
+
return self._hdr_
|
|
397
|
+
|
|
398
|
+
# FIXME: HACK: sometimes RecursionError
|
|
399
|
+
self._hdr_ = "|".join((self.code, self.verb)) # type: ignore[unreachable]
|
|
400
|
+
self._hdr_ = pkt_header(self)
|
|
406
401
|
return self._hdr_
|
|
407
402
|
|
|
408
403
|
@property
|
|
409
|
-
def _idx(self) -> bool | str:
|
|
404
|
+
def _idx(self) -> bool | str: # FIXME: a mess
|
|
410
405
|
"""Return the payload's index, if any (e.g. zone_idx, domain_id or log_idx).
|
|
411
406
|
|
|
412
407
|
Used to route a packet to the correct entity's (i.e. zone/domain) msg handler.
|
|
413
408
|
"""
|
|
414
409
|
|
|
415
|
-
if self._idx_ is None:
|
|
416
|
-
self._idx_
|
|
410
|
+
if self._idx_ is not None:
|
|
411
|
+
return self._idx_
|
|
412
|
+
|
|
413
|
+
self._idx_ = _pkt_idx(self) or False # type: ignore[unreachable]
|
|
417
414
|
return self._idx_
|
|
418
415
|
|
|
419
416
|
|
|
420
417
|
# TODO: a mess - has false negatives
|
|
421
|
-
def _pkt_idx(pkt) -> None | bool | str: # _has_array, _has_ctl
|
|
418
|
+
def _pkt_idx(pkt: Frame) -> None | bool | str: # _has_array, _has_ctl
|
|
422
419
|
"""Return the payload's 2-byte context (e.g. zone_idx, domain_id or log_idx).
|
|
423
420
|
|
|
424
421
|
May return a 2-byte string (usu. pkt.payload[:2]), or:
|
|
@@ -440,14 +437,14 @@ def _pkt_idx(pkt) -> None | bool | str: # _has_array, _has_ctl
|
|
|
440
437
|
|
|
441
438
|
if pkt.code == Code._000C: # zone_idx/domain_id (complex, payload[0:4])
|
|
442
439
|
if pkt.payload[2:4] == DEV_ROLE_MAP.APP: # "000F"
|
|
443
|
-
return FC
|
|
440
|
+
return str(FC) # mypy
|
|
444
441
|
if pkt.payload[0:4] == f"01{DEV_ROLE_MAP.HTG}": # "010E"
|
|
445
|
-
return F9
|
|
442
|
+
return str(F9) # mypy
|
|
446
443
|
if pkt.payload[2:4] in (
|
|
447
444
|
DEV_ROLE_MAP.DHW,
|
|
448
445
|
DEV_ROLE_MAP.HTG,
|
|
449
446
|
): # "000D", "000E"
|
|
450
|
-
return FA
|
|
447
|
+
return str(FA) # mypy
|
|
451
448
|
return pkt.payload[:2]
|
|
452
449
|
|
|
453
450
|
if pkt.code == Code._0404: # assumes only 1 DHW zone (can be 2, but never seen)
|
|
@@ -462,15 +459,16 @@ def _pkt_idx(pkt) -> None | bool | str: # _has_array, _has_ctl
|
|
|
462
459
|
if pkt.code == Code._3220: # msg_id/data_id (payload[4:6])
|
|
463
460
|
return pkt.payload[4:6]
|
|
464
461
|
|
|
465
|
-
if pkt.code in
|
|
462
|
+
if pkt.code in CODE_IDX_ARE_COMPLEX: # these should be handled above
|
|
466
463
|
raise NotImplementedError(f"{pkt} # CODE_IDX_COMPLEX") # a coding error
|
|
467
464
|
|
|
468
465
|
# mutex 1/4, CODE_IDX_NONE: always returns False
|
|
469
|
-
if pkt.code in
|
|
470
|
-
if
|
|
471
|
-
pkt.
|
|
466
|
+
if pkt.code in CODE_IDX_ARE_NONE: # returns False
|
|
467
|
+
if (
|
|
468
|
+
CODES_SCHEMA[pkt.code].get(pkt.verb, "")[:3] == "^00"
|
|
469
|
+
and pkt.payload[:2] != "00"
|
|
472
470
|
):
|
|
473
|
-
raise
|
|
471
|
+
raise exc.PacketPayloadInvalid(
|
|
474
472
|
f"Packet idx is {pkt.payload[:2]}, but expecting no idx (00) (0xAA)"
|
|
475
473
|
)
|
|
476
474
|
return False
|
|
@@ -482,13 +480,13 @@ def _pkt_idx(pkt) -> None | bool | str: # _has_array, _has_ctl
|
|
|
482
480
|
# TODO: is this needed?: exceptions to CODE_IDX_SIMPLE
|
|
483
481
|
if pkt.payload[:2] in (F8, F9, FA, FC): # TODO: F6, F7?, FB, FD
|
|
484
482
|
if pkt.code not in CODE_IDX_DOMAIN:
|
|
485
|
-
raise
|
|
483
|
+
raise exc.PacketPayloadInvalid(
|
|
486
484
|
f"Packet idx is {pkt.payload[:2]}, but not expecting a domain id"
|
|
487
485
|
)
|
|
488
486
|
return pkt.payload[:2]
|
|
489
487
|
|
|
490
488
|
if (
|
|
491
|
-
pkt._has_ctl
|
|
489
|
+
pkt._has_ctl # TODO: exclude HVAC?
|
|
492
490
|
): # risk of false -ves, TODO: pkt.src.type == DEV_TYPE_MAP.HGI too? # DEX
|
|
493
491
|
# 02: 22C9: would be picked up as an array, if len==1 counted
|
|
494
492
|
# 03: # .I 028 03:094242 --:------ 03:094242 30C9 003 010B22 # ctl
|
|
@@ -496,15 +494,15 @@ def _pkt_idx(pkt) -> None | bool | str: # _has_array, _has_ctl
|
|
|
496
494
|
# 23: 0009|10A0
|
|
497
495
|
return pkt.payload[:2] # tcs._max_zones checked elsewhere
|
|
498
496
|
|
|
499
|
-
|
|
500
|
-
|
|
497
|
+
if pkt.code in (Code._31D9, Code._31DA):
|
|
498
|
+
return pkt.payload[:2]
|
|
501
499
|
|
|
502
500
|
if pkt.payload[:2] != "00":
|
|
503
|
-
raise
|
|
501
|
+
raise exc.PacketPayloadInvalid(
|
|
504
502
|
f"Packet idx is {pkt.payload[:2]}, but expecting no idx (00) (0xAB)"
|
|
505
503
|
) # TODO: add a test for this
|
|
506
504
|
|
|
507
|
-
if pkt.code in
|
|
505
|
+
if pkt.code in CODE_IDX_ARE_SIMPLE:
|
|
508
506
|
return None # False # TODO: return None (less precise) or risk false -ves?
|
|
509
507
|
|
|
510
508
|
# mutex 4/4, CODE_IDX_UNKNOWN: an unknown code
|
|
@@ -512,9 +510,7 @@ def _pkt_idx(pkt) -> None | bool | str: # _has_array, _has_ctl
|
|
|
512
510
|
return None
|
|
513
511
|
|
|
514
512
|
|
|
515
|
-
def pkt_header(
|
|
516
|
-
pkt, rx_header: bool = None
|
|
517
|
-
) -> None | _HeaderT: # NOTE: used in command.py
|
|
513
|
+
def pkt_header(pkt: Frame, /, rx_header: bool = False) -> None | HeaderT:
|
|
518
514
|
"""Return the header of a packet (all packets have a header).
|
|
519
515
|
|
|
520
516
|
Used for QoS, and others.
|
|
@@ -532,10 +528,10 @@ def pkt_header(
|
|
|
532
528
|
|
|
533
529
|
if pkt.code == Code._1FC9:
|
|
534
530
|
# .I --- 34:021943 --:------ 34:021943 1FC9 024 00-2309-8855B7 00-1FC9-8855B7
|
|
535
|
-
# .W --- 01:145038 34:021943 --:------ 1FC9 006 00-2309-06368E #
|
|
531
|
+
# .W --- 01:145038 34:021943 --:------ 1FC9 006 00-2309-06368E # won't know src until it arrives
|
|
536
532
|
# .I --- 34:021943 01:145038 --:------ 1FC9 006 00-2309-8855B7
|
|
537
533
|
if not rx_header:
|
|
538
|
-
device_id =
|
|
534
|
+
device_id = ALL_DEV_ADDR.id if pkt.src == pkt.dst else pkt.dst.id
|
|
539
535
|
return "|".join((pkt.code, pkt.verb, device_id))
|
|
540
536
|
if pkt.src == pkt.dst: # and pkt.verb == I_:
|
|
541
537
|
return "|".join((pkt.code, W_, pkt.src.id))
|
|
@@ -545,15 +541,20 @@ def pkt_header(
|
|
|
545
541
|
# return "|".join((pkt.code, RP, pkt.dst.id))
|
|
546
542
|
return None
|
|
547
543
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
544
|
+
# RQ and W use the dst.id rather than the src.id, as:
|
|
545
|
+
# - cmd.src could be 18:000730, and echo .src will have changed to (say) 18:123456
|
|
546
|
+
# - cmd.dst is the effector
|
|
551
547
|
|
|
552
|
-
|
|
553
|
-
|
|
548
|
+
if rx_header:
|
|
549
|
+
if pkt.verb in (I_, RP) or pkt.src == pkt.dst: # say: xxxx| W|00:000000|xx
|
|
550
|
+
return None # no response expected
|
|
551
|
+
header = "|".join((pkt.code, RP if pkt.verb == RQ else I_, pkt.dst.id))
|
|
552
|
+
|
|
553
|
+
elif pkt.verb in (I_, RP) or pkt.src == pkt.dst:
|
|
554
|
+
header = "|".join((pkt.code, pkt.verb, pkt.src.id))
|
|
554
555
|
|
|
555
|
-
else:
|
|
556
|
-
header = "|".join((pkt.code,
|
|
556
|
+
else:
|
|
557
|
+
header = "|".join((pkt.code, pkt.verb, pkt.dst.id))
|
|
557
558
|
|
|
558
559
|
try:
|
|
559
560
|
return f"{header}|{pkt._ctx}" if isinstance(pkt._ctx, str) else header
|