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_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 # MessageIndex()
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._msg_db]
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
- else:
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 an LookupError if a device_id is filtered out by a list."""
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,4 +1,4 @@
1
1
  """RAMSES RF - a RAMSES-II protocol decoder & analyser (application layer)."""
2
2
 
3
- __version__ = "0.51.8"
3
+ __version__ = "0.52.0"
4
4
  VERSION = __version__
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ramses_rf
3
- Version: 0.51.8
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=vbKS3KVPiGsDWLp5cR3SVBtXrs-TinzlxSbTgcb4G2k,19724
2
+ ramses_cli/client.py,sha256=NTLhHhTiYmp7nyE3vqIFR9zjwfot1wdIT7QuMUgubD0,20350
3
3
  ramses_cli/debug.py,sha256=vgR0lOHoYjWarN948dI617WZZGNuqHbeq6Tc16Da7b4,608
4
- ramses_cli/discovery.py,sha256=81XbmpNiCpUHVZBwo2g1eRwyJG-wZhpSsc44G3hHlFA,12972
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=ZZZgucyuU1IHsSewGukZfDg2gu8KeNaEFriWKM0TUHs,10287
11
- ramses_rf/dispatcher.py,sha256=JGkqSi1o-YhQ2rj8tNkXwYLLeJIC7F061xpHoH8sSsM,11201
12
- ramses_rf/entity_base.py,sha256=Byt8mFRUKETNiKHaL0cNaMywjLcopDG3Sldiy1Q7lAo,39213
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=s2bhkUzR42mzL4lZ1crTBsEWMOMGI8tpuHN1UZdAB74,20564
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=PUTZH-D81qyH8onRDdeZzxGwuCVDzMYTbItYAp9NKKI,125
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=WGkBTUNjRUEe-phxdtdiXVCZnTi6-i1i_YT6g689UTM,17450
21
- ramses_rf/device/heat.py,sha256=2sCsggySVcuTzyXDmgWy76QbhlU5MQWSejy3zgI5BDE,54242
22
- ramses_rf/device/hvac.py,sha256=uKljqweushvpHwRTiYfR26YST6--KAIp8X9zyiRv9HI,45607
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=5swDr_SvOs1CxBmT-iJpldf8R00mOb7gKPMiEnxLz84,8452
29
+ ramses_tx/address.py,sha256=F5ZE-EbPNNom1fW9XXUILvD7DYSMBxNJvsHVliT5gjw,8452
30
30
  ramses_tx/command.py,sha256=r9dNaofjjOQXZSUrZjsNpvEukNn4rSGy0OLr2Dyd2TI,125129
31
- ramses_tx/const.py,sha256=QmwSS4BIN3ZFrLUiiFScP1RCUHuJ782V3ycRPQTtB_c,30297
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=9lUVh8gAMXNRAolfFw2WuWANjn24AWkmscuM9Tm5imE,22036
35
- ramses_tx/gateway.py,sha256=TXLYwT6tFpmSokD29Qyj1ze7UGCxKidooeyP557Jfoo,11266
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=hl_gLfwrF79ftUNnsgNt3XGsIhM2Pts0MtZZuGjfaxk,13169
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=NGunaGCkEjhTp9t4mARK5e7kbqT-Z_JKCH7ibMYMJXU,7357
41
- ramses_tx/parsers.py,sha256=c6D3NB79vZfOGpVmrHc-EhSh4_N4tKljBp1J3HFfkXw,109950
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=DujcZe4WelHvPNKYfoz9YWmXNoCayV5UsAkiu8Y6vms,53432
46
- ramses_tx/schemas.py,sha256=18IPRdoCWXpcRg4v8Z1ehTnRronQPYGrf4AvRL-1OD0,12932
47
- ramses_tx/transport.py,sha256=MwPnkQ0L-2qJt4mIJy3-C9XmHwBDjT7Kg-1LthPByVw,58331
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=kLXw7G-2GHtLEZnlOm_5jj8TGeibQYmH_NZ9F7FVHAU,123
51
- ramses_rf-0.51.8.dist-info/METADATA,sha256=JmxHDvvXI3gRkt2zHELxS9G3ZaMyOn8bOvaraq5_0Ro,4000
52
- ramses_rf-0.51.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
- ramses_rf-0.51.8.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
54
- ramses_rf-0.51.8.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
55
- ramses_rf-0.51.8.dist-info/RECORD,,
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 is the address fields are not valid.
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
- Will raise InvalidPacketError if it is invalid.
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 to constructing bad commands
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 a unambiguous string representation of this object."""
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/code/hdr).
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 a input_file must be specified")
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
- Will raise InvalidPacketError if it is invalid.
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 = pkt.seqn
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(self._pkt.payload) # ? raise InvalidPacketError
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 = "" if self.dst is self.src else self._name(self.dst)
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
- """Return True if the message's raw payload is an array."""
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
- """Return the domain_id/zone_idx/other_idx of a message payload, if any.
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
- Used to identify the zone/domain that a message applies to. Returns an empty
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 check
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 the message, and parse the payload if so.
248
+ """Validate a message packet payload, and parse it if valid.
241
249
 
242
- Raise an exception (InvalidPacketError) if it is not valid.
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
- Raise an InvalidPayloadError if the payload is seen as invalid. Such payloads may
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
- assert payload[4:6] in _2411_TABLE, (
1916
- f"param {payload[4:6]} is unknown"
1917
- ) # _INFORM_DEV_MSG
1918
- description = _2411_TABLE.get(payload[4:6], "Unknown")
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": payload[4:6],
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
- assert payload[8:10] in _2411_DATA_TYPES, (
1929
- f"param {payload[4:6]} has unknown data_type: {payload[8:10]}"
1930
- ) # _INFORM_DEV_MSG
1931
- length, parser = _2411_DATA_TYPES.get(payload[8:10], (8, lambda x: x))
1932
-
1933
- result |= {
1934
- "value": parser(payload[10:18][-length:]), # type: ignore[operator]
1935
- "_value_06": payload[6:10],
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
- if msg.len == 9:
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 some RND)
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]{4}$",
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}(([0-9A-F]{8}){3}[0-9A-F]{4})?$",
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]{4})?$",
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 (and no None)
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
- komports = comports(include_links=True)
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
@@ -1,4 +1,4 @@
1
1
  """RAMSES RF - a RAMSES-II protocol decoder & analyser (transport layer)."""
2
2
 
3
- __version__ = "0.51.8"
3
+ __version__ = "0.52.0"
4
4
  VERSION = __version__