ramses-rf 0.51.8__py3-none-any.whl → 0.52.0__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/client.py +21 -9
- ramses_cli/discovery.py +1 -1
- ramses_rf/database.py +322 -89
- ramses_rf/device/base.py +10 -5
- ramses_rf/device/heat.py +15 -7
- ramses_rf/device/hvac.py +100 -94
- ramses_rf/dispatcher.py +8 -6
- ramses_rf/entity_base.py +477 -116
- ramses_rf/gateway.py +16 -6
- ramses_rf/version.py +1 -1
- {ramses_rf-0.51.8.dist-info → ramses_rf-0.52.0.dist-info}/METADATA +1 -1
- {ramses_rf-0.51.8.dist-info → ramses_rf-0.52.0.dist-info}/RECORD +26 -26
- ramses_tx/address.py +1 -1
- ramses_tx/const.py +1 -1
- ramses_tx/frame.py +4 -4
- ramses_tx/gateway.py +3 -1
- ramses_tx/message.py +22 -14
- ramses_tx/packet.py +1 -1
- ramses_tx/parsers.py +48 -27
- ramses_tx/ramses.py +11 -3
- ramses_tx/schemas.py +8 -2
- ramses_tx/transport.py +9 -7
- ramses_tx/version.py +1 -1
- {ramses_rf-0.51.8.dist-info → ramses_rf-0.52.0.dist-info}/WHEEL +0 -0
- {ramses_rf-0.51.8.dist-info → ramses_rf-0.52.0.dist-info}/entry_points.txt +0 -0
- {ramses_rf-0.51.8.dist-info → ramses_rf-0.52.0.dist-info}/licenses/LICENSE +0 -0
ramses_rf/gateway.py
CHANGED
|
@@ -129,7 +129,7 @@ class Gateway(Engine):
|
|
|
129
129
|
self.devices: list[Device] = []
|
|
130
130
|
self.device_by_id: dict[DeviceIdT, Device] = {}
|
|
131
131
|
|
|
132
|
-
self.msg_db: MessageIndex | None = None
|
|
132
|
+
self.msg_db: MessageIndex | None = None
|
|
133
133
|
|
|
134
134
|
def __repr__(self) -> str:
|
|
135
135
|
if not self.ser_name:
|
|
@@ -173,6 +173,11 @@ class Gateway(Engine):
|
|
|
173
173
|
**self._packet_log,
|
|
174
174
|
)
|
|
175
175
|
|
|
176
|
+
# initialize SQLite index, set in _tx/Engine
|
|
177
|
+
if self._sqlite_index: # TODO(eb): default to ON in Q4 2025
|
|
178
|
+
self.create_sqlite_message_index() # if activated in ramses_cc > Engine
|
|
179
|
+
|
|
180
|
+
# temporarily turn on discovery, remember original state
|
|
176
181
|
self.config.disable_discovery, disable_discovery = (
|
|
177
182
|
True,
|
|
178
183
|
self.config.disable_discovery,
|
|
@@ -184,6 +189,7 @@ class Gateway(Engine):
|
|
|
184
189
|
if cached_packets:
|
|
185
190
|
await self._restore_cached_packets(cached_packets)
|
|
186
191
|
|
|
192
|
+
# reset discovery to original state
|
|
187
193
|
self.config.disable_discovery = disable_discovery
|
|
188
194
|
|
|
189
195
|
if (
|
|
@@ -193,6 +199,9 @@ class Gateway(Engine):
|
|
|
193
199
|
):
|
|
194
200
|
initiate_discovery(self.devices, self.systems)
|
|
195
201
|
|
|
202
|
+
def create_sqlite_message_index(self) -> None:
|
|
203
|
+
self.msg_db = MessageIndex() # start the index
|
|
204
|
+
|
|
196
205
|
async def stop(self) -> None:
|
|
197
206
|
"""Stop the Gateway and tidy up."""
|
|
198
207
|
|
|
@@ -248,12 +257,13 @@ class Gateway(Engine):
|
|
|
248
257
|
# return True
|
|
249
258
|
return include_expired or not msg._expired
|
|
250
259
|
|
|
251
|
-
msgs = [m for device in self.devices for m in device.
|
|
260
|
+
msgs = [m for device in self.devices for m in device._msg_list]
|
|
252
261
|
|
|
253
262
|
for system in self.systems:
|
|
254
263
|
msgs.extend(list(system._msgs.values()))
|
|
255
264
|
msgs.extend([m for z in system.zones for m in z._msgs.values()])
|
|
256
|
-
# msgs.extend([m for z in system.dhw for m in z._msgs.values()]) # TODO
|
|
265
|
+
# msgs.extend([m for z in system.dhw for m in z._msgs.values()]) # TODO: DHW
|
|
266
|
+
# Related to/Fixes ramses_cc Issue 249 non-existing via-device _HW ?
|
|
257
267
|
|
|
258
268
|
if self.msg_db:
|
|
259
269
|
pkts = {
|
|
@@ -261,8 +271,8 @@ class Gateway(Engine):
|
|
|
261
271
|
for msg in self.msg_db.all(include_expired=True)
|
|
262
272
|
if wanted_msg(msg, include_expired=include_expired)
|
|
263
273
|
}
|
|
264
|
-
|
|
265
|
-
|
|
274
|
+
else: # deprecated, to be removed in Q1 2026
|
|
275
|
+
# _LOGGER.warning("Missing MessageIndex")
|
|
266
276
|
pkts = { # BUG: assumes pkts have unique dtms: may be untrue for contrived logs
|
|
267
277
|
f"{repr(msg._pkt)[:26]}": f"{repr(msg._pkt)[27:]}"
|
|
268
278
|
for msg in msgs
|
|
@@ -359,7 +369,7 @@ class Gateway(Engine):
|
|
|
359
369
|
"""
|
|
360
370
|
|
|
361
371
|
def check_filter_lists(dev_id: DeviceIdT) -> None: # may: LookupError
|
|
362
|
-
"""Raise
|
|
372
|
+
"""Raise a LookupError if a device_id is filtered out by a list."""
|
|
363
373
|
|
|
364
374
|
if dev_id in self._unwanted: # TODO: shouldn't invalidate a msg
|
|
365
375
|
raise LookupError(f"Can't create {dev_id}: it is unwanted or invalid")
|
ramses_rf/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ramses_rf
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.52.0
|
|
4
4
|
Summary: A stateful RAMSES-II protocol decoder & analyser.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ramses-rf/ramses_rf
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/ramses-rf/ramses_rf/issues
|
|
@@ -1,55 +1,55 @@
|
|
|
1
1
|
ramses_cli/__init__.py,sha256=uvGzWqOf4avvgzxJNSLFWEelIWqSZ-AeLAZzg5x58bc,397
|
|
2
|
-
ramses_cli/client.py,sha256=
|
|
2
|
+
ramses_cli/client.py,sha256=NTLhHhTiYmp7nyE3vqIFR9zjwfot1wdIT7QuMUgubD0,20350
|
|
3
3
|
ramses_cli/debug.py,sha256=vgR0lOHoYjWarN948dI617WZZGNuqHbeq6Tc16Da7b4,608
|
|
4
|
-
ramses_cli/discovery.py,sha256=
|
|
4
|
+
ramses_cli/discovery.py,sha256=MWoahBnAAVzfK2S7EDLsY2WYqN_ZK9L-lktrj8_4cb0,12978
|
|
5
5
|
ramses_cli/utils/cat_slow.py,sha256=AhUpM5gnegCitNKU-JGHn-DrRzSi-49ZR1Qw6lxe_t8,607
|
|
6
6
|
ramses_cli/utils/convert.py,sha256=D_YiCyX5na9pgC-_NhBlW9N1dgRKUK-uLtLBfofjzZM,1804
|
|
7
7
|
ramses_rf/__init__.py,sha256=VG3E9GHbtC6lx6E1DMQJeFitHnydMKJyPxQBethdrzg,1193
|
|
8
8
|
ramses_rf/binding_fsm.py,sha256=uZAOl3i19KCXqqlaLJWkEqMMP7NJBhVPW3xTikQD1fY,25996
|
|
9
9
|
ramses_rf/const.py,sha256=L3z31CZ-xqno6oZp_h-67CB_5tDDqTwSWXsqRtsjMcs,5460
|
|
10
|
-
ramses_rf/database.py,sha256=
|
|
11
|
-
ramses_rf/dispatcher.py,sha256=
|
|
12
|
-
ramses_rf/entity_base.py,sha256=
|
|
10
|
+
ramses_rf/database.py,sha256=21M1XV7qTQaydqInctf7vX-PAVdrM4xmJoaXX8oB87s,20748
|
|
11
|
+
ramses_rf/dispatcher.py,sha256=UqilAoL6T85ZFPkcpvcfuidNbusONngQoCVWHV6vOzk,11277
|
|
12
|
+
ramses_rf/entity_base.py,sha256=eLGhJTv8FDHjXmsywUhn1imEhdo0bFgAHP_KLNl_HiE,56004
|
|
13
13
|
ramses_rf/exceptions.py,sha256=mt_T7irqHSDKir6KLaf6oDglUIdrw0S40JbOrWJk5jc,3657
|
|
14
|
-
ramses_rf/gateway.py,sha256=
|
|
14
|
+
ramses_rf/gateway.py,sha256=1MRmqpDlhE1SFm25dSvnSTX_dUoi9Pz--qycJIJyFAU,21151
|
|
15
15
|
ramses_rf/helpers.py,sha256=TNk_QkpIOB3alOp1sqnA9LOzi4fuDCeapNlW3zTzNas,4250
|
|
16
16
|
ramses_rf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
ramses_rf/schemas.py,sha256=UhvRhV4nZ3kvrLLM3wwIguQUIjIgd_AKvp2wkTSpNEA,13468
|
|
18
|
-
ramses_rf/version.py,sha256=
|
|
18
|
+
ramses_rf/version.py,sha256=ggzTkhC_7XHqI8ONDXEhJk9tXE1LLvMfYJyCrJzF8KI,125
|
|
19
19
|
ramses_rf/device/__init__.py,sha256=sUbH5dhbYFXSoM_TPFRutpRutBRpup7_cQ9smPtDTy8,4858
|
|
20
|
-
ramses_rf/device/base.py,sha256=
|
|
21
|
-
ramses_rf/device/heat.py,sha256=
|
|
22
|
-
ramses_rf/device/hvac.py,sha256=
|
|
20
|
+
ramses_rf/device/base.py,sha256=Yx0LZwMEb49naY8FolZ8HEBFb6XCPQBTVN_TWyO2nKg,17777
|
|
21
|
+
ramses_rf/device/heat.py,sha256=UmZuA-5czrfoClVEeUAPWgze5obWNQpYI7ZPQpVJB6s,54704
|
|
22
|
+
ramses_rf/device/hvac.py,sha256=of8wWHhJOX0KcvVqqlyJLArKrv1ST7rlbL7kLV9v_0Q,45603
|
|
23
23
|
ramses_rf/system/__init__.py,sha256=uZLKio3gLlBzePa2aDQ1nxkcp1YXOGrn6iHTG8LiNIw,711
|
|
24
24
|
ramses_rf/system/faultlog.py,sha256=GdGmVGT3137KsTlV_nhccgIFEmYu6DFsLTn4S-8JSok,12799
|
|
25
25
|
ramses_rf/system/heat.py,sha256=3jaFEChU-HlWCRMY1y7u09s7AH4hT0pC63hnqwdmZOc,39223
|
|
26
26
|
ramses_rf/system/schedule.py,sha256=Ts6tdZPTQLV5NkgwA73tPa5QUsnZNIIuYoKC-8VsXDk,18808
|
|
27
27
|
ramses_rf/system/zones.py,sha256=9AH7ooN5QfiqvWuor2P1Dn8aILjQb2RWL9rWqDH1IjA,36075
|
|
28
28
|
ramses_tx/__init__.py,sha256=qNMTe8hBkIuecvtCiekUB0pKdD8atb0SjWxVNVe3yhE,3538
|
|
29
|
-
ramses_tx/address.py,sha256=
|
|
29
|
+
ramses_tx/address.py,sha256=F5ZE-EbPNNom1fW9XXUILvD7DYSMBxNJvsHVliT5gjw,8452
|
|
30
30
|
ramses_tx/command.py,sha256=r9dNaofjjOQXZSUrZjsNpvEukNn4rSGy0OLr2Dyd2TI,125129
|
|
31
|
-
ramses_tx/const.py,sha256=
|
|
31
|
+
ramses_tx/const.py,sha256=pnxq5upXvLUizv9Ye_I1llD9rAa3wddHgsSkc91AIUc,30300
|
|
32
32
|
ramses_tx/exceptions.py,sha256=FJSU9YkvpKjs3yeTqUJX1o3TPFSe_B01gRGIh9b3PNc,2632
|
|
33
33
|
ramses_tx/fingerprints.py,sha256=nfftA1E62HQnb-eLt2EqjEi_la0DAoT0wt-PtTMie0s,11974
|
|
34
|
-
ramses_tx/frame.py,sha256=
|
|
35
|
-
ramses_tx/gateway.py,sha256=
|
|
34
|
+
ramses_tx/frame.py,sha256=GzNsXr15YLeidJYGtk_xPqsZQh4ehDDlUCtT6rTDhT8,22046
|
|
35
|
+
ramses_tx/gateway.py,sha256=ztPg3fMgn-fM15dLxrw5PG34SCxD7eR3pdz1xwvz7Ag,11345
|
|
36
36
|
ramses_tx/helpers.py,sha256=J4OCRckp3JshGQTvvqEskFjB1hPS7uA_opVsuIqmZds,32915
|
|
37
37
|
ramses_tx/logger.py,sha256=qYbUoNPnPaFWKVsYvLG6uTVuPTdZ8HsMzBbGx0DpBqc,10177
|
|
38
|
-
ramses_tx/message.py,sha256
|
|
38
|
+
ramses_tx/message.py,sha256=-moQ8v3HVlNSl-x3U0DDfDcj8WQ7vLqclMNxsohbmnw,13449
|
|
39
39
|
ramses_tx/opentherm.py,sha256=58PXz9l5x8Ou6Fm3y-R_UnGHCYahoi2RKIDdYStUMzk,42378
|
|
40
|
-
ramses_tx/packet.py,sha256=
|
|
41
|
-
ramses_tx/parsers.py,sha256=
|
|
40
|
+
ramses_tx/packet.py,sha256=_qHiPFWpQpKueZOgf1jJ93Y09iZjo3LZWStLglVkXg4,7370
|
|
41
|
+
ramses_tx/parsers.py,sha256=e6IwVEMLv2EL8gbZaM2s-qA3E2AN8dCwdIgfEbFo730,111029
|
|
42
42
|
ramses_tx/protocol.py,sha256=9R3aCzuiWEyXmugmB5tyR_RhBlgfnpUXj7AsMP9BzzU,28867
|
|
43
43
|
ramses_tx/protocol_fsm.py,sha256=ZKtehCr_4TaDdfdlfidFLJaOVTYtaEq5h4tLqNIhb9s,26827
|
|
44
44
|
ramses_tx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
|
-
ramses_tx/ramses.py,sha256=
|
|
46
|
-
ramses_tx/schemas.py,sha256=
|
|
47
|
-
ramses_tx/transport.py,sha256=
|
|
45
|
+
ramses_tx/ramses.py,sha256=NG81GBNZlap-Gi9ac-r6OFE-KaHvXsgPDWy-I2Irr-4,53698
|
|
46
|
+
ramses_tx/schemas.py,sha256=IYCDH0jp3446Gtl_378aBmWyXN90e-uYudfkZkOKR24,13147
|
|
47
|
+
ramses_tx/transport.py,sha256=bGprlfuuwBgQ1bmBRSrcicuk7s-jVqyuKpZCfQ-sSpw,58469
|
|
48
48
|
ramses_tx/typed_dicts.py,sha256=w-0V5t2Q3GiNUOrRAWiW9GtSwbta_7luME6GfIb1zhI,10869
|
|
49
49
|
ramses_tx/typing.py,sha256=eF2SlPWhNhEFQj6WX2AhTXiyRQVXYnFutiepllYl2rI,5042
|
|
50
|
-
ramses_tx/version.py,sha256=
|
|
51
|
-
ramses_rf-0.
|
|
52
|
-
ramses_rf-0.
|
|
53
|
-
ramses_rf-0.
|
|
54
|
-
ramses_rf-0.
|
|
55
|
-
ramses_rf-0.
|
|
50
|
+
ramses_tx/version.py,sha256=8xCh-Ia7WXZkWWEs7lVuk13x3NbC3HoNuHvR_G1KCB4,123
|
|
51
|
+
ramses_rf-0.52.0.dist-info/METADATA,sha256=ADZlDrTcgK0-xOW4D1A-2CRfakYnaGOOClGRshQYBwg,4000
|
|
52
|
+
ramses_rf-0.52.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
53
|
+
ramses_rf-0.52.0.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
|
|
54
|
+
ramses_rf-0.52.0.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
|
|
55
|
+
ramses_rf-0.52.0.dist-info/RECORD,,
|
ramses_tx/address.py
CHANGED
|
@@ -195,7 +195,7 @@ def pkt_addrs(addr_fragment: str) -> tuple[Address, Address, Address, Address, A
|
|
|
195
195
|
|
|
196
196
|
returns: src_addr, dst_addr, addr_0, addr_1, addr_2
|
|
197
197
|
|
|
198
|
-
Will raise an InvalidAddrSetError
|
|
198
|
+
Will raise an InvalidAddrSetError if the address fields are not valid.
|
|
199
199
|
"""
|
|
200
200
|
# for debug: print(pkt_addrs.cache_info())
|
|
201
201
|
|
ramses_tx/const.py
CHANGED
|
@@ -788,7 +788,7 @@ class MsgId(StrEnum):
|
|
|
788
788
|
_7F = "7F"
|
|
789
789
|
|
|
790
790
|
|
|
791
|
-
# StrEnum is intended include all known codes, see: test suite, code schema in ramses.py
|
|
791
|
+
# StrEnum is intended to include all known codes, see: test suite, code schema in ramses.py
|
|
792
792
|
@verify(EnumCheck.UNIQUE)
|
|
793
793
|
class Code(StrEnum):
|
|
794
794
|
_0001 = "0001"
|
ramses_tx/frame.py
CHANGED
|
@@ -65,7 +65,7 @@ class Frame:
|
|
|
65
65
|
def __init__(self, frame: str) -> None:
|
|
66
66
|
"""Create a frame from a string.
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
:raises InvalidPacketError: if provided string is invalid.
|
|
69
69
|
"""
|
|
70
70
|
|
|
71
71
|
self._frame: str = frame
|
|
@@ -123,7 +123,7 @@ class Frame:
|
|
|
123
123
|
if not strict_checking:
|
|
124
124
|
return
|
|
125
125
|
|
|
126
|
-
try: # Strict checking: helps users avoid
|
|
126
|
+
try: # Strict checking: helps users avoid constructing bad commands
|
|
127
127
|
if addrs[0] == NON_DEV_ADDR:
|
|
128
128
|
assert self.verb == I_, "wrong verb or dst addr should be present"
|
|
129
129
|
elif addrs[2] == NON_DEV_ADDR:
|
|
@@ -138,7 +138,7 @@ class Frame:
|
|
|
138
138
|
raise exc.PacketInvalid(f"Bad frame: Invalid address set: {err}") from err
|
|
139
139
|
|
|
140
140
|
def __repr__(self) -> str:
|
|
141
|
-
"""Return
|
|
141
|
+
"""Return an unambiguous string representation of this object."""
|
|
142
142
|
|
|
143
143
|
if self._repr is None:
|
|
144
144
|
self._repr = " ".join( # type: ignore[unreachable]
|
|
@@ -387,7 +387,7 @@ class Frame:
|
|
|
387
387
|
|
|
388
388
|
@property
|
|
389
389
|
def _hdr(self) -> HeaderT: # incl. self._ctx
|
|
390
|
-
"""Return the QoS header (fingerprint) of this packet (i.e. device_id
|
|
390
|
+
"""Return the QoS header (fingerprint) of this packet (i.e. device_id|code|verb).
|
|
391
391
|
|
|
392
392
|
Used for QoS (timeouts, retries), callbacks, etc.
|
|
393
393
|
"""
|
ramses_tx/gateway.py
CHANGED
|
@@ -37,6 +37,7 @@ from .schemas import (
|
|
|
37
37
|
SZ_PACKET_LOG,
|
|
38
38
|
SZ_PORT_CONFIG,
|
|
39
39
|
SZ_PORT_NAME,
|
|
40
|
+
SZ_SQLITE_INDEX,
|
|
40
41
|
PktLogConfigT,
|
|
41
42
|
PortConfigT,
|
|
42
43
|
select_device_filter_mode,
|
|
@@ -91,7 +92,7 @@ class Engine:
|
|
|
91
92
|
if input_file:
|
|
92
93
|
self._disable_sending = True
|
|
93
94
|
elif not port_name:
|
|
94
|
-
raise TypeError("Either a port_name or
|
|
95
|
+
raise TypeError("Either a port_name or an input_file must be specified")
|
|
95
96
|
|
|
96
97
|
self.ser_name = port_name
|
|
97
98
|
self._input_file = input_file
|
|
@@ -112,6 +113,7 @@ class Engine:
|
|
|
112
113
|
self._include,
|
|
113
114
|
self._exclude,
|
|
114
115
|
)
|
|
116
|
+
self._sqlite_index = kwargs.pop(SZ_SQLITE_INDEX)
|
|
115
117
|
self._kwargs: dict[str, Any] = kwargs # HACK
|
|
116
118
|
|
|
117
119
|
self._engine_lock = Lock() # FIXME: threading lock, or asyncio lock?
|
ramses_tx/message.py
CHANGED
|
@@ -54,7 +54,7 @@ class MessageBase:
|
|
|
54
54
|
def __init__(self, pkt: Packet) -> None:
|
|
55
55
|
"""Create a message from a valid packet.
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
:raises InvalidPacketError if message payload is invalid.
|
|
58
58
|
"""
|
|
59
59
|
|
|
60
60
|
self._pkt = pkt
|
|
@@ -66,11 +66,15 @@ class MessageBase:
|
|
|
66
66
|
self.dtm: dt = pkt.dtm
|
|
67
67
|
|
|
68
68
|
self.verb: VerbT = pkt.verb
|
|
69
|
-
self.seqn: str =
|
|
69
|
+
self.seqn: str = (
|
|
70
|
+
pkt.seqn
|
|
71
|
+
) # the msg is part of a set for 1 Code, received in order
|
|
70
72
|
self.code: Code = pkt.code
|
|
71
73
|
self.len: int = pkt._len
|
|
72
74
|
|
|
73
|
-
self._payload = self._validate(
|
|
75
|
+
self._payload = self._validate(
|
|
76
|
+
self._pkt.payload
|
|
77
|
+
) # ? may raise InvalidPacketError
|
|
74
78
|
|
|
75
79
|
self._str: str = None # type: ignore[assignment]
|
|
76
80
|
|
|
@@ -92,7 +96,9 @@ class MessageBase:
|
|
|
92
96
|
|
|
93
97
|
if self.src.id == self._addrs[0].id: # type: ignore[unreachable]
|
|
94
98
|
name_0 = self._name(self.src)
|
|
95
|
-
name_1 =
|
|
99
|
+
name_1 = (
|
|
100
|
+
"" if self.dst is self.src else self._name(self.dst)
|
|
101
|
+
) # use 'is', issue_cc 318
|
|
96
102
|
else:
|
|
97
103
|
name_0 = ""
|
|
98
104
|
name_1 = self._name(self.src)
|
|
@@ -139,16 +145,18 @@ class MessageBase:
|
|
|
139
145
|
|
|
140
146
|
@property
|
|
141
147
|
def _has_array(self) -> bool:
|
|
142
|
-
"""
|
|
148
|
+
"""
|
|
149
|
+
:return: True if the message's raw payload is an array.
|
|
150
|
+
"""
|
|
143
151
|
|
|
144
152
|
return bool(self._pkt._has_array)
|
|
145
153
|
|
|
146
154
|
@property
|
|
147
155
|
def _idx(self) -> dict[str, str]:
|
|
148
|
-
"""
|
|
156
|
+
"""Get the domain_id/zone_idx/other_idx of a message payload, if any.
|
|
157
|
+
Used to identify the zone/domain that a message applies to.
|
|
149
158
|
|
|
150
|
-
|
|
151
|
-
dict if there is none such, or None if undetermined.
|
|
159
|
+
:return: an empty dict if there is none such, or None if undetermined.
|
|
152
160
|
"""
|
|
153
161
|
|
|
154
162
|
# .I --- 01:145038 --:------ 01:145038 3B00 002 FCC8
|
|
@@ -229,7 +237,7 @@ class MessageBase:
|
|
|
229
237
|
assert isinstance(self._pkt._idx, str) # mypy hint
|
|
230
238
|
return {IDX_NAMES[Code._22C9]: self._pkt._idx}
|
|
231
239
|
|
|
232
|
-
assert isinstance(self._pkt._idx, str) # mypy
|
|
240
|
+
assert isinstance(self._pkt._idx, str) # mypy hint
|
|
233
241
|
idx_name = SZ_DOMAIN_ID if self._pkt._idx[:1] == "F" else SZ_ZONE_IDX
|
|
234
242
|
index_name = IDX_NAMES.get(self.code, idx_name)
|
|
235
243
|
|
|
@@ -237,9 +245,10 @@ class MessageBase:
|
|
|
237
245
|
|
|
238
246
|
# TODO: needs work...
|
|
239
247
|
def _validate(self, raw_payload: str) -> dict | list[dict]: # type: ignore[type-arg]
|
|
240
|
-
"""Validate
|
|
248
|
+
"""Validate a message packet payload, and parse it if valid.
|
|
241
249
|
|
|
242
|
-
|
|
250
|
+
:return: a dict containing key: value pairs, or a list of those created from the payload
|
|
251
|
+
:raises an InvalidPacketError exception if it is not valid.
|
|
243
252
|
"""
|
|
244
253
|
|
|
245
254
|
try: # parse the payload
|
|
@@ -249,10 +258,9 @@ class MessageBase:
|
|
|
249
258
|
if not self._has_payload and (
|
|
250
259
|
self.verb == RQ and self.code not in RQ_IDX_COMPLEX
|
|
251
260
|
):
|
|
252
|
-
# _LOGGER.error("%s", msg)
|
|
253
261
|
return {}
|
|
254
262
|
|
|
255
|
-
result = parse_payload(self)
|
|
263
|
+
result = parse_payload(self) # invoke the code parsers
|
|
256
264
|
|
|
257
265
|
if isinstance(result, list):
|
|
258
266
|
return result
|
|
@@ -353,7 +361,7 @@ def re_compile_re_match(regex: str, string: str) -> bool: # Optional[Match[Any]
|
|
|
353
361
|
def _check_msg_payload(msg: MessageBase, payload: str) -> None:
|
|
354
362
|
"""Validate the packet's payload against its verb/code pair.
|
|
355
363
|
|
|
356
|
-
|
|
364
|
+
:raises InvalidPayloadError if the payload is seen as invalid. Such payloads may
|
|
357
365
|
actually be valid, in which case the rules (likely the regex) will need updating.
|
|
358
366
|
"""
|
|
359
367
|
|
ramses_tx/packet.py
CHANGED
|
@@ -104,7 +104,7 @@ class Packet(Frame):
|
|
|
104
104
|
return f"{dtm} ... {self}{hdr}"
|
|
105
105
|
|
|
106
106
|
def __str__(self) -> str:
|
|
107
|
-
"""Return a brief readable string representation of this object."""
|
|
107
|
+
"""Return a brief readable string representation of this object aka 'header'."""
|
|
108
108
|
# e.g.: 000A|RQ|01:145038|08
|
|
109
109
|
return super().__repr__() # TODO: self._hdr
|
|
110
110
|
|
ramses_tx/parsers.py
CHANGED
|
@@ -1912,42 +1912,59 @@ def parser_2411(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1912
1912
|
"92": (4, hex_to_temp), # 75 (0-30) (C)
|
|
1913
1913
|
} # TODO: _2411_TYPES.get(payload[8:10], (8, no_op))
|
|
1914
1914
|
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1915
|
+
# Handle unknown parameters gracefully instead of asserting
|
|
1916
|
+
param_id = payload[4:6]
|
|
1917
|
+
try:
|
|
1918
|
+
description = _2411_TABLE.get(param_id, "Unknown")
|
|
1919
|
+
if param_id not in _2411_TABLE:
|
|
1920
|
+
_LOGGER.warning(
|
|
1921
|
+
f"2411 message received with unknown parameter ID: {param_id}. "
|
|
1922
|
+
f"This parameter is not in the known parameter schema. "
|
|
1923
|
+
f"Message: {msg!r}"
|
|
1924
|
+
)
|
|
1925
|
+
except Exception as err:
|
|
1926
|
+
_LOGGER.warning(f"Error looking up 2411 parameter {param_id}: {err}")
|
|
1927
|
+
description = "Unknown"
|
|
1919
1928
|
|
|
1920
1929
|
result = {
|
|
1921
|
-
"parameter":
|
|
1930
|
+
"parameter": param_id,
|
|
1922
1931
|
"description": description,
|
|
1923
1932
|
}
|
|
1924
1933
|
|
|
1925
1934
|
if msg.verb == RQ:
|
|
1926
1935
|
return result
|
|
1927
1936
|
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
+
try:
|
|
1938
|
+
assert payload[8:10] in _2411_DATA_TYPES, (
|
|
1939
|
+
f"param {param_id} has unknown data_type: {payload[8:10]}"
|
|
1940
|
+
) # _INFORM_DEV_MSG
|
|
1941
|
+
length, parser = _2411_DATA_TYPES.get(payload[8:10], (8, lambda x: x))
|
|
1942
|
+
|
|
1943
|
+
result |= {
|
|
1944
|
+
"value": parser(payload[10:18][-length:]), # type: ignore[operator]
|
|
1945
|
+
"_value_06": payload[6:10],
|
|
1946
|
+
}
|
|
1937
1947
|
|
|
1938
|
-
|
|
1948
|
+
if msg.len == 9:
|
|
1949
|
+
return result
|
|
1950
|
+
|
|
1951
|
+
return (
|
|
1952
|
+
result
|
|
1953
|
+
| {
|
|
1954
|
+
"min_value": parser(payload[18:26][-length:]), # type: ignore[operator]
|
|
1955
|
+
"max_value": parser(payload[26:34][-length:]), # type: ignore[operator]
|
|
1956
|
+
"precision": parser(payload[34:42][-length:]), # type: ignore[operator]
|
|
1957
|
+
"_value_42": payload[42:],
|
|
1958
|
+
# Flexible footer - capture everything after precision
|
|
1959
|
+
# eg. older Orcon models may have a footer of 2 bytes
|
|
1960
|
+
}
|
|
1961
|
+
)
|
|
1962
|
+
except AssertionError as err:
|
|
1963
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
1964
|
+
# Return partial result for unknown parameters
|
|
1965
|
+
result["value"] = ""
|
|
1939
1966
|
return result
|
|
1940
1967
|
|
|
1941
|
-
return (
|
|
1942
|
-
result
|
|
1943
|
-
| {
|
|
1944
|
-
"min_value": parser(payload[18:26][-length:]), # type: ignore[operator]
|
|
1945
|
-
"max_value": parser(payload[26:34][-length:]), # type: ignore[operator]
|
|
1946
|
-
"precision": parser(payload[34:42][-length:]), # type: ignore[operator]
|
|
1947
|
-
"_value_42": payload[42:],
|
|
1948
|
-
}
|
|
1949
|
-
)
|
|
1950
|
-
|
|
1951
1968
|
|
|
1952
1969
|
# unknown_2420, from OTB
|
|
1953
1970
|
def parser_2420(payload: str, msg: Message) -> dict[str, Any]:
|
|
@@ -2347,7 +2364,7 @@ def parser_3210(payload: str, msg: Message) -> PayDictT._3210:
|
|
|
2347
2364
|
return {SZ_TEMPERATURE: hex_to_temp(payload[2:])}
|
|
2348
2365
|
|
|
2349
2366
|
|
|
2350
|
-
# opentherm_msg, from OTB (and
|
|
2367
|
+
# opentherm_msg, from OTB (and OT_RND)
|
|
2351
2368
|
def parser_3220(payload: str, msg: Message) -> dict[str, Any]:
|
|
2352
2369
|
try:
|
|
2353
2370
|
ot_type, ot_id, ot_value, ot_schema = decode_frame(payload[2:10])
|
|
@@ -2971,8 +2988,12 @@ _PAYLOAD_PARSERS = {
|
|
|
2971
2988
|
|
|
2972
2989
|
|
|
2973
2990
|
def parse_payload(msg: Message) -> dict | list[dict]:
|
|
2991
|
+
"""
|
|
2992
|
+
Apply the appropriate parser defined in this module to the message.
|
|
2993
|
+
:param msg: a Message object containing packet data and extra attributes
|
|
2994
|
+
:return: a dict of key: value pairs or a list of such dicts, e.g. {'temperature': 21.5}
|
|
2995
|
+
"""
|
|
2974
2996
|
result: dict | list[dict]
|
|
2975
|
-
|
|
2976
2997
|
result = _PAYLOAD_PARSERS.get(msg.code, parser_unknown)(msg._pkt.payload, msg)
|
|
2977
2998
|
if isinstance(result, dict) and msg.seqn.isnumeric(): # e.g. 22F1/3
|
|
2978
2999
|
result["seqx_num"] = msg.seqn
|
ramses_tx/ramses.py
CHANGED
|
@@ -478,10 +478,10 @@ CODES_SCHEMA: dict[Code, dict[str, Any]] = { # rf_unknown
|
|
|
478
478
|
},
|
|
479
479
|
Code._2411: { # fan_params, HVAC
|
|
480
480
|
SZ_NAME: "fan_params",
|
|
481
|
-
I_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}([0-9A-F]{8}){4}[0-9A-F]{
|
|
481
|
+
I_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}([0-9A-F]{8}){4}[0-9A-F]{2,6}$", # Allow 2-6 byte footer
|
|
482
482
|
RQ: r"^(00|01|15|16|17|21)00[0-9A-F]{2}((00){19})?$",
|
|
483
|
-
RP: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}(
|
|
484
|
-
W_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}(
|
|
483
|
+
RP: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}([0-9A-F]{8}){3}[0-9A-F]{2,6}$", # 4 blocks + footer
|
|
484
|
+
W_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}([0-9A-F]{8}){3}[0-9A-F]{2,6}$", # Same as RP
|
|
485
485
|
},
|
|
486
486
|
Code._2420: { # unknown_2420, from OTB
|
|
487
487
|
SZ_NAME: "message_2420",
|
|
@@ -1267,6 +1267,14 @@ _22F1_SCHEMES: dict[str, dict[str, str]] = {
|
|
|
1267
1267
|
|
|
1268
1268
|
# unclear if true for only Orcon/*all* models
|
|
1269
1269
|
_2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
1270
|
+
"01": { # all?
|
|
1271
|
+
SZ_DESCRIPTION: "Support",
|
|
1272
|
+
SZ_MIN_VALUE: 0xFF, # None?
|
|
1273
|
+
SZ_MAX_VALUE: 0xFF,
|
|
1274
|
+
SZ_PRECISION: 1,
|
|
1275
|
+
SZ_DATA_TYPE: "00",
|
|
1276
|
+
SZ_DATA_UNIT: "",
|
|
1277
|
+
},
|
|
1270
1278
|
"31": { # slot 09 (FANs produced after 2021)
|
|
1271
1279
|
SZ_DESCRIPTION: "Time to change filter (days)",
|
|
1272
1280
|
SZ_MIN_VALUE: 0,
|
ramses_tx/schemas.py
CHANGED
|
@@ -338,7 +338,6 @@ SCH_GLOBAL_TRAITS_DICT, SCH_TRAITS = sch_global_traits_dict_factory()
|
|
|
338
338
|
#
|
|
339
339
|
# Device lists (Engine configuration)
|
|
340
340
|
|
|
341
|
-
|
|
342
341
|
DeviceIdT = NewType("DeviceIdT", str) # TypeVar('DeviceIdT', bound=str) #
|
|
343
342
|
DevIndexT = NewType("DevIndexT", str)
|
|
344
343
|
DeviceListT: TypeAlias = dict[DeviceIdT, DeviceTraitsT]
|
|
@@ -397,10 +396,14 @@ def select_device_filter_mode(
|
|
|
397
396
|
|
|
398
397
|
#
|
|
399
398
|
# 5/5: Gateway (engine) configuration
|
|
399
|
+
|
|
400
400
|
SZ_DISABLE_SENDING: Final = "disable_sending"
|
|
401
401
|
SZ_DISABLE_QOS: Final = "disable_qos"
|
|
402
402
|
SZ_ENFORCE_KNOWN_LIST: Final[str] = f"enforce_{SZ_KNOWN_LIST}"
|
|
403
403
|
SZ_EVOFW_FLAG: Final = "evofw_flag"
|
|
404
|
+
SZ_SQLITE_INDEX: Final = (
|
|
405
|
+
"sqlite_index" # temporary 0.52.x SQLite dev config option in ramses_cc
|
|
406
|
+
)
|
|
404
407
|
SZ_USE_REGEX: Final = "use_regex"
|
|
405
408
|
|
|
406
409
|
SCH_ENGINE_DICT = {
|
|
@@ -408,10 +411,13 @@ SCH_ENGINE_DICT = {
|
|
|
408
411
|
vol.Optional(SZ_DISABLE_QOS, default=None): vol.Any(
|
|
409
412
|
None, # None is selective QoS (e.g. QoS only for bindings, schedule, etc.)
|
|
410
413
|
bool,
|
|
411
|
-
), # in long term, this default to be True (
|
|
414
|
+
), # in the long term, this default to be True (not None)
|
|
412
415
|
vol.Optional(SZ_ENFORCE_KNOWN_LIST, default=False): bool,
|
|
413
416
|
vol.Optional(SZ_EVOFW_FLAG): vol.Any(None, str),
|
|
414
417
|
# vol.Optional(SZ_PORT_CONFIG): SCH_SERIAL_PORT_CONFIG,
|
|
418
|
+
vol.Optional(
|
|
419
|
+
SZ_SQLITE_INDEX, default=False
|
|
420
|
+
): bool, # temporary 0.52.x dev config option
|
|
415
421
|
vol.Optional(SZ_USE_REGEX): dict, # vol.All(ConvertNullToDict(), dict),
|
|
416
422
|
vol.Optional(SZ_COMMS_PARAMS): SCH_COMMS_PARAMS,
|
|
417
423
|
}
|
ramses_tx/transport.py
CHANGED
|
@@ -44,7 +44,7 @@ import sys
|
|
|
44
44
|
from collections import deque
|
|
45
45
|
from collections.abc import Awaitable, Callable, Iterable
|
|
46
46
|
from datetime import datetime as dt, timedelta as td
|
|
47
|
-
from functools import wraps
|
|
47
|
+
from functools import partial, wraps
|
|
48
48
|
from io import TextIOWrapper
|
|
49
49
|
from string import printable
|
|
50
50
|
from time import perf_counter
|
|
@@ -142,8 +142,7 @@ else: # is linux
|
|
|
142
142
|
|
|
143
143
|
def list_links(devices: set[str]) -> list[str]:
|
|
144
144
|
"""Search for symlinks to ports already listed in devices."""
|
|
145
|
-
|
|
146
|
-
links = []
|
|
145
|
+
links: list[str] = []
|
|
147
146
|
for device in glob.glob("/dev/*") + glob.glob("/dev/serial/by-id/*"):
|
|
148
147
|
if os.path.islink(device) and os.path.realpath(device) in devices:
|
|
149
148
|
links.append(device)
|
|
@@ -174,7 +173,7 @@ else: # is linux
|
|
|
174
173
|
return result
|
|
175
174
|
|
|
176
175
|
|
|
177
|
-
def is_hgi80(serial_port: SerPortNameT) -> bool | None:
|
|
176
|
+
async def is_hgi80(serial_port: SerPortNameT) -> bool | None:
|
|
178
177
|
"""Return True/False if the device attached to the port has the attrs of an HGI80.
|
|
179
178
|
|
|
180
179
|
Return None if it's not possible to tell (falsy should assume is evofw3).
|
|
@@ -209,7 +208,10 @@ def is_hgi80(serial_port: SerPortNameT) -> bool | None:
|
|
|
209
208
|
|
|
210
209
|
# otherwise, we can look at device attrs via comports()...
|
|
211
210
|
try:
|
|
212
|
-
|
|
211
|
+
loop = asyncio.get_running_loop()
|
|
212
|
+
komports = await loop.run_in_executor(
|
|
213
|
+
None, partial(comports, include_links=True)
|
|
214
|
+
)
|
|
213
215
|
except ImportError as err:
|
|
214
216
|
raise exc.TransportSerialError(f"Unable to find {serial_port}: {err}") from err
|
|
215
217
|
|
|
@@ -841,8 +843,6 @@ class PortTransport(_RegHackMixin, _FullTransport, _PortTransportAbstractor): #
|
|
|
841
843
|
self._leak_sem(), name="PortTransport._leak_sem()"
|
|
842
844
|
)
|
|
843
845
|
|
|
844
|
-
self._is_hgi80 = is_hgi80(self.serial.name)
|
|
845
|
-
|
|
846
846
|
self._loop.create_task(
|
|
847
847
|
self._create_connection(), name="PortTransport._create_connection()"
|
|
848
848
|
)
|
|
@@ -855,6 +855,8 @@ class PortTransport(_RegHackMixin, _FullTransport, _PortTransportAbstractor): #
|
|
|
855
855
|
|
|
856
856
|
# signature also serves to discover the HGI's device_id (& for pkt log, if any)
|
|
857
857
|
|
|
858
|
+
self._is_hgi80 = await is_hgi80(self.serial.name)
|
|
859
|
+
|
|
858
860
|
async def connect_sans_signature() -> None:
|
|
859
861
|
"""Call connection_made() without sending/waiting for a signature."""
|
|
860
862
|
|
ramses_tx/version.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|