ramses-rf 0.52.3__py3-none-any.whl → 0.52.4__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/database.py CHANGED
@@ -130,11 +130,15 @@ class MessageIndex:
130
130
  def stop(self) -> None:
131
131
  """Stop the housekeeper loop."""
132
132
 
133
- if self._housekeeping_task and (not self._housekeeping_task.done()):
133
+ if (
134
+ self.maintain
135
+ and self._housekeeping_task
136
+ and (not self._housekeeping_task.done())
137
+ ):
134
138
  self._housekeeping_task.cancel() # stop the housekeeper
135
139
 
136
140
  self._cx.commit() # just in case
137
- # self._cx.close() # may still need to do queries after engine has stopped?
141
+ self._cx.close() # may still need to do queries after engine has stopped?
138
142
 
139
143
  @property
140
144
  def msgs(self) -> MsgDdT:
@@ -193,7 +197,7 @@ class MessageIndex:
193
197
  """
194
198
  dtm = dt_now - _cutoff # .isoformat(timespec="microseconds") < needed?
195
199
 
196
- self._cu.execute("SELECT dtm FROM messages WHERE dtm => ?", (dtm,))
200
+ self._cu.execute("SELECT dtm FROM messages WHERE dtm >= ?", (dtm,))
197
201
  rows = self._cu.fetchall() # fetch dtm of current messages to retain
198
202
 
199
203
  try: # make this operation atomic, i.e. update self._msgs only on success
ramses_rf/device/heat.py CHANGED
@@ -450,7 +450,7 @@ class UfhController(Parent, DeviceHeat): # UFC (02):
450
450
  # )
451
451
  # self._send_cmd(cmd)
452
452
 
453
- elif msg.code == Code._0008: # relay_demand, TODO: use msgIndex DB?
453
+ elif msg.code == Code._0008: # relay_demand
454
454
  if msg.payload.get(SZ_DOMAIN_ID) == FC:
455
455
  self._relay_demand = msg
456
456
  else: # FA
ramses_rf/entity_base.py CHANGED
@@ -224,7 +224,10 @@ class _MessageDB(_Entity):
224
224
 
225
225
  # As of 0.52.1 we use SQLite MessageIndex, see ramses_rf/database.py
226
226
  # _msgz_ (nested) was only used in this module. Note:
227
- # _msgz (now rebuilt from _msgs) also used in: client, base, device.heat
227
+ # _msgz (now rebuilt from _msgs) is also used in:
228
+ # - client.py: for code in device._msgz.values()
229
+ # - base.py: Code._1060 in self._msgz
230
+ # [x] device.heat (no longer used)
228
231
 
229
232
  def _handle_msg(self, msg: Message) -> None:
230
233
  """Store a msg in the DBs.
@@ -244,14 +247,14 @@ class _MessageDB(_Entity):
244
247
 
245
248
  if self._gwy.msg_db: # central SQLite MessageIndex
246
249
  _LOGGER.debug(
247
- "For %s (z_id %s) add msg %s, src %s, dst %s to msg_db.",
250
+ "For %s (_z_id %s) add msg %s, src %s, dst %s to msg_db.",
248
251
  self.id,
249
252
  self._z_id,
250
253
  msg,
251
254
  msg.src,
252
255
  msg.dst,
253
256
  )
254
- debug_code: Code = Code._3150
257
+ debug_code: Code = Code._3150 # for debugging only log these, pick your own
255
258
  if msg.code == debug_code and msg.src.id.startswith("01:"):
256
259
  _LOGGER.debug(
257
260
  "Added msg from %s with code %s to _gwy.msg_db. hdr=%s",
@@ -293,6 +296,7 @@ class _MessageDB(_Entity):
293
296
  def _msg_list(self) -> list[Message]:
294
297
  """Return a flattened list of all messages logged on this device."""
295
298
  # (only) used in gateway.py#get_state() and in tests/tests/test_eavesdrop_schema.py
299
+ # TODO remove _msg_list Q1 2026
296
300
  if self._gwy.msg_db:
297
301
  msg_list_qry: list[Message] = []
298
302
  code_list = self._msg_dev_qry()
@@ -303,16 +307,21 @@ class _MessageDB(_Entity):
303
307
  msg_list_qry.append(self._msgs[c])
304
308
  else:
305
309
  # evohome has these errors
306
- # _msg_list could not fetch self._msgs[7FFF] for 18:072981 (z_id 18:072981)
310
+ # _msg_list could not fetch self._msgs[7FFF] for 18:072981 (_z_id 18:072981)
307
311
  _LOGGER.debug(
308
- "_msg_list could not fetch self._msgs[%s] for %s (z_id %s)",
312
+ "_msg_list could not fetch self._msgs[%s] for %s (_z_id %s)",
309
313
  c,
310
314
  self.id,
311
315
  self._z_id,
312
316
  )
313
317
  return msg_list_qry
314
318
  # else create from legacy nested dict
315
- return [m for c in self._msgz.values() for v in c.values() for m in v.values()]
319
+ return [
320
+ msg
321
+ for code in self._msgz.values()
322
+ for ctx in code.values()
323
+ for msg in ctx.values()
324
+ ]
316
325
 
317
326
  def _add_record(
318
327
  self, address: Address, code: Code | None = None, verb: str = " I"
@@ -424,7 +433,7 @@ class _MessageDB(_Entity):
424
433
  **kwargs: Any,
425
434
  ) -> dict | list | None:
426
435
  """
427
- Query the message dict or the SQLite index for the most recent
436
+ Query the _msgz message dict or the SQLite MessageIndex for the most recent
428
437
  key: value pairs(s) for a given code.
429
438
 
430
439
  :param code: filter messages by Code or a tuple of Codes, optional
@@ -517,7 +526,7 @@ class _MessageDB(_Entity):
517
526
  or (idx == SZ_DOMAIN_ID)
518
527
  ), (
519
528
  f"full dict:{msg_dict}, payload:{msg.payload} < Coding error: key='{idx}', val='{val}'"
520
- ) # should not be there (TODO(eb): BUG but occurs when using SQLite MessageIndex)
529
+ ) # should not be there
521
530
 
522
531
  if (
523
532
  key == "*" or not key
@@ -535,21 +544,35 @@ class _MessageDB(_Entity):
535
544
  """
536
545
  Retrieve from the MessageIndex a list of Code keys involving this device.
537
546
 
538
- :return: list of Codes or empty list when query returned empty
547
+ :return: list of Codes or an empty list when the query returned empty
539
548
  """
540
549
 
541
550
  if self._gwy.msg_db:
542
551
  # SQLite query on MessageIndex
543
552
  res: list[Code] = []
544
- if self.id[_ID_SLICE:] == "_HW":
553
+
554
+ if len(self.id) == 9:
555
+ # fetch a ctl's message codes (add all its children?)
556
+ sql = """
557
+ SELECT code from messages WHERE
558
+ verb in (' I', 'RP')
559
+ AND (src = ? OR dst = ?)
560
+ AND ctx LIKE ?
561
+ """
562
+ _ctx_qry = "%"
563
+
564
+ elif self.id[_ID_SLICE:] == "_HW":
565
+ # fetch a DHW entity's message codes
545
566
  sql = """
546
567
  SELECT code from messages WHERE
547
568
  verb in (' I', 'RP')
548
569
  AND (src = ? OR dst = ?)
549
570
  AND (ctx IN ('FC', 'FA', 'F9', 'FA') OR plk LIKE ?)
550
571
  """
551
- _ctx_qry = "%dhw_idx%" # syntax error ?
572
+ _ctx_qry = "%dhw_idx%"
573
+
552
574
  else:
575
+ # fetch a zone's message codes
553
576
  sql = """
554
577
  SELECT code from messages WHERE
555
578
  verb in (' I', 'RP')
@@ -562,12 +585,12 @@ class _MessageDB(_Entity):
562
585
  sql, (self.id[:_ID_SLICE], self.id[:_ID_SLICE], _ctx_qry)
563
586
  ):
564
587
  _LOGGER.debug(
565
- "Fetched from index: %s for %s (z_id %s)",
588
+ "Fetched from index: %s for %s (_z_id %s)",
566
589
  rec[0],
567
590
  self.id,
568
591
  self._z_id,
569
592
  )
570
- # Example: "Fetched from index: code 1FD4 for 01:123456 (z_id 01)"
593
+ # Example: "Fetched from index: code 1FD4 for 01:123456 (_z_id 01)"
571
594
  res.append(Code(str(rec[0])))
572
595
  return res
573
596
  else:
@@ -743,7 +766,18 @@ class _MessageDB(_Entity):
743
766
  # for r in results:
744
767
  # print(r)
745
768
 
746
- if self.id[_ID_SLICE:] == "_HW":
769
+ if len(self.id) == 9:
770
+ # fetch a ctl's message dtms (add all its children?)
771
+ sql = """
772
+ SELECT dtm from messages WHERE
773
+ verb in (' I', 'RP')
774
+ AND (src = ? OR dst = ?)
775
+ AND ctx LIKE ?
776
+ """
777
+ _ctx_qry = "%"
778
+
779
+ elif self.id[_ID_SLICE:] == "_HW":
780
+ # fetch a DHW entity's message dtms
747
781
  sql = """
748
782
  SELECT dtm from messages WHERE
749
783
  verb in (' I', 'RP')
@@ -753,6 +787,7 @@ class _MessageDB(_Entity):
753
787
  _ctx_qry = "%dhw_idx%"
754
788
  # TODO add Children messages? self.ctl.dhw
755
789
  else:
790
+ # fetch a zone's message dtms
756
791
  sql = """
757
792
  SELECT dtm from messages WHERE
758
793
  verb in (' I', 'RP')
@@ -769,14 +804,14 @@ class _MessageDB(_Entity):
769
804
  }
770
805
  # if CTL, remove 3150, 3220 heat_demand, both are only stored on children
771
806
  # HACK
772
- if self.id[:3] == "01:" and self._SLUG == "CTL":
773
- # with next ON: 2 errors , both 1x UFC, 1x CTR
774
- # with next OFF: 4 errors, all CTR
775
- # if Code._3150 in _msg_dict: # Note: CTL can send a 3150 (see heat_ufc_00)
776
- # _msg_dict.pop(Code._3150) # keep, prefer to have 2 extra instead of missing 1
777
- if Code._3220 in _msg_dict:
778
- _msg_dict.pop(Code._3220)
779
- # _LOGGER.debug(f"Removed 3150/3220 from %s._msgs dict", self.id)
807
+ # if self.id[:3] == "01:" and self._SLUG == "CTL":
808
+ # with next ON: 2 errors , both 1x UFC, 1x CTR
809
+ # with next OFF: 4 errors, all CTR
810
+ # if Code._3150 in _msg_dict: # Note: CTL can send a 3150 (see heat_ufc_00)
811
+ # _msg_dict.pop(Code._3150) # keep, prefer to have 2 extra instead of missing 1
812
+ # if Code._3220 in _msg_dict:
813
+ # _msg_dict.pop(Code._3220)
814
+ # _LOGGER.debug(f"Removed 3150/3220 from %s._msgs dict", self.id)
780
815
  return _msg_dict
781
816
 
782
817
  @property
@@ -862,7 +897,6 @@ class _Discovery(_MessageDB):
862
897
  # return f"{data_id:02X}" # type: ignore[return-value]
863
898
 
864
899
  res: list[str] = []
865
- # look for the "sim" OT 3220 record initially added in OtbGateway.init
866
900
  if self._gwy.msg_db:
867
901
  # SQLite query for ctx field on MessageIndex
868
902
  sql = """
@@ -995,18 +1029,24 @@ class _Discovery(_MessageDB):
995
1029
  sql = """
996
1030
  SELECT dtm from messages WHERE
997
1031
  code = ?
998
- verb = ' I'
1032
+ AND verb = ' I'
999
1033
  AND ctx = 'True'
1000
1034
  AND (src = ? OR dst = ?)
1001
1035
  """
1002
- msgs += self._gwy.msg_db.qry(
1036
+ res = self._gwy.msg_db.qry(
1003
1037
  sql,
1004
1038
  (
1005
1039
  task[_SZ_COMMAND].code,
1006
1040
  self.tcs.id[:_ID_SLICE],
1007
1041
  self.tcs.id[:_ID_SLICE],
1008
1042
  ),
1009
- )[0] # expect 1 Message in returned tuple
1043
+ )
1044
+ if len(res) > 0:
1045
+ msgs += res[0] # expect 1 Message in returned tuple
1046
+ else:
1047
+ _LOGGER.debug(
1048
+ f"No msg found for hdr {hdr}, tesk code {task[_SZ_COMMAND].code}"
1049
+ )
1010
1050
  else: # TODO(eb) remove next Q1 2026
1011
1051
  msgs += [self.tcs._msgz[task[_SZ_COMMAND].code][I_][True]]
1012
1052
  # raise NotImplementedError
@@ -1145,6 +1185,7 @@ class Parent(Entity): # A System, Zone, DhwZone or a UfhController
1145
1185
  (incl. the DHW Zone), and also any UFH controllers.
1146
1186
 
1147
1187
  For a heating Zone, children are limited to a sensor, and a number of actuators.
1188
+
1148
1189
  For the DHW Zone, the children are limited to a sensor, a DHW valve, and/or a
1149
1190
  heating valve.
1150
1191
 
ramses_rf/gateway.py CHANGED
@@ -258,14 +258,6 @@ class Gateway(Engine):
258
258
  # return True
259
259
  return include_expired or not msg._expired
260
260
 
261
- msgs = [m for device in self.devices for m in device._msg_list]
262
-
263
- for system in self.systems:
264
- msgs.extend(list(system._msgs.values()))
265
- msgs.extend([m for z in system.zones for m in z._msgs.values()])
266
- # msgs.extend([m for z in system.dhw for m in z._msgs.values()]) # TODO: DHW
267
- # Related to/Fixes ramses_cc Issue 249 non-existing via-device _HW ?
268
-
269
261
  if self.msg_db:
270
262
  pkts = {
271
263
  f"{repr(msg._pkt)[:26]}": f"{repr(msg._pkt)[27:]}"
@@ -273,12 +265,20 @@ class Gateway(Engine):
273
265
  if wanted_msg(msg, include_expired=include_expired)
274
266
  }
275
267
  else: # deprecated, to be removed in Q1 2026
276
- # _LOGGER.warning("Missing MessageIndex")
268
+ msgs = [m for device in self.devices for m in device._msg_list]
269
+ # add systems._msgs and zones._msgs
270
+ for system in self.systems:
271
+ msgs.extend(list(system._msgs.values()))
272
+ msgs.extend([m for z in system.zones for m in z._msgs.values()])
273
+ # msgs.extend([m for z in system.dhw for m in z._msgs.values()]) # TODO: DHW
274
+ # Related to/Fixes ramses_cc Issue 249 non-existing via-device _HW ?
275
+
277
276
  pkts = { # BUG: assumes pkts have unique dtms: may be untrue for contrived logs
278
277
  f"{repr(msg._pkt)[:26]}": f"{repr(msg._pkt)[27:]}"
279
278
  for msg in msgs
280
279
  if wanted_msg(msg, include_expired=include_expired)
281
280
  }
281
+ # _LOGGER.warning("Missing MessageIndex")
282
282
 
283
283
  self._resume()
284
284
 
ramses_rf/system/heat.py CHANGED
@@ -532,7 +532,7 @@ class MultiZone(SystemBase): # 0005 (+/- 000C?)
532
532
  schema = shrink(SCH_TCS_ZONES_ZON(schema))
533
533
 
534
534
  zon: Zone = self.zone_by_idx.get(zone_idx) # type: ignore[assignment]
535
- if zon is None:
535
+ if zon is None: # not found in tcs, create it
536
536
  zon = zone_factory(self, zone_idx, msg=msg, **schema) # type: ignore[unreachable]
537
537
  self.zone_by_idx[zon.idx] = zon
538
538
  self.zones.append(zon)
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.52.3"
3
+ __version__ = "0.52.4"
4
4
  VERSION = __version__
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ramses_rf
3
- Version: 0.52.3
3
+ Version: 0.52.4
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
@@ -22,6 +22,7 @@ Description-Content-Type: text/markdown
22
22
  ![Linting](https://github.com/ramses-rf/ramses_rf/actions/workflows/check-lint.yml/badge.svg)
23
23
  ![Typing](https://github.com/ramses-rf/ramses_rf/actions/workflows/check-type.yml/badge.svg)
24
24
  ![Testing](https://github.com/ramses-rf/ramses_rf/actions/workflows/check-test.yml/badge.svg)
25
+ [![Coverage](https://github.com/ramses-rf/ramses_rf/actions/workflows/check-cov.yml/badge.svg?event=push)](https://github.com/ramses-rf/ramses_rf/actions/workflows/check-cov.yml)
25
26
 
26
27
  ## Overview
27
28
 
@@ -7,28 +7,28 @@ ramses_cli/utils/convert.py,sha256=D_YiCyX5na9pgC-_NhBlW9N1dgRKUK-uLtLBfofjzZM,1
7
7
  ramses_rf/__init__.py,sha256=vp2TyFGqc1fGQHsevhmaw0QEmSSCnZx7fqizKiEwHtw,1245
8
8
  ramses_rf/binding_fsm.py,sha256=fuqvcc9YW-wr8SPH8zadpPqrHAvzl_eeWF-IBtlLppY,26632
9
9
  ramses_rf/const.py,sha256=L3z31CZ-xqno6oZp_h-67CB_5tDDqTwSWXsqRtsjMcs,5460
10
- ramses_rf/database.py,sha256=rMfEJvapc3Zh6SFqjPjntuuaYynuZiAdYTw1pcXsJPE,20344
10
+ ramses_rf/database.py,sha256=2sHiBhI5121HdX-IMquY3h5qQzDljeGWpQ9x6U4duEo,20408
11
11
  ramses_rf/dispatcher.py,sha256=YjEU-QrBLo9IfoEhJo2ikg_FxOaMYoWvzelr9Vi-JZ8,11398
12
- ramses_rf/entity_base.py,sha256=uN8QGGv0cutCYTT-oyDkvzv4xpBEObWF4jo9ArtvbLE,57640
12
+ ramses_rf/entity_base.py,sha256=LifFLcxI867j4DmsaNSHsVl2fUEPkXpJdz4b7ZFpupo,58929
13
13
  ramses_rf/exceptions.py,sha256=mt_T7irqHSDKir6KLaf6oDglUIdrw0S40JbOrWJk5jc,3657
14
- ramses_rf/gateway.py,sha256=VYZqMppU_kDYhT3EUmqpHf0LuLXPMH7ASx9jylAopWE,21218
14
+ ramses_rf/gateway.py,sha256=c88_HCAEBYPaNETMC0IUK1HU9QuEOnCGnK-5wze08_s,21289
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=jvpx0hZiMhaogyxbxnrbxS7m2GekKK4DFfVTNs7h-WQ,13476
18
- ramses_rf/version.py,sha256=E3rkZTOmbKgrnpOmzEdweYaquvnmc18LcdiXXdsnakM,125
18
+ ramses_rf/version.py,sha256=9IsbhdNecTxEJDHwnSjdsa0r2hpABWxeNcD6LfKKNs0,125
19
19
  ramses_rf/device/__init__.py,sha256=sUbH5dhbYFXSoM_TPFRutpRutBRpup7_cQ9smPtDTy8,4858
20
20
  ramses_rf/device/base.py,sha256=Yx0LZwMEb49naY8FolZ8HEBFb6XCPQBTVN_TWyO2nKg,17777
21
- ramses_rf/device/heat.py,sha256=tjORTbjxBKFzgPQbfpLuj74IlrVtOmj9GpzslK9j0A0,54569
21
+ ramses_rf/device/heat.py,sha256=I9_NlB_cOxCfCb3Zx88hY6wd7yIrknzo6msrWxXeeEs,54545
22
22
  ramses_rf/device/hvac.py,sha256=gdpVACUvtq6ERMI0mwRhqIJtKYEmybzJA2NeyN1ELrs,48431
23
23
  ramses_rf/system/__init__.py,sha256=uZLKio3gLlBzePa2aDQ1nxkcp1YXOGrn6iHTG8LiNIw,711
24
24
  ramses_rf/system/faultlog.py,sha256=GdGmVGT3137KsTlV_nhccgIFEmYu6DFsLTn4S-8JSok,12799
25
- ramses_rf/system/heat.py,sha256=3jaFEChU-HlWCRMY1y7u09s7AH4hT0pC63hnqwdmZOc,39223
25
+ ramses_rf/system/heat.py,sha256=qQmzgmyHy2x87gHAstn0ee7ZVVOq-GJIfDxCrC-6gFU,39254
26
26
  ramses_rf/system/schedule.py,sha256=Ts6tdZPTQLV5NkgwA73tPa5QUsnZNIIuYoKC-8VsXDk,18808
27
27
  ramses_rf/system/zones.py,sha256=YN_HAbeaa2YioUOjafEpp-0IHmIwmKNSK_77pPcjtns,36072
28
28
  ramses_tx/__init__.py,sha256=sqnjM7pUGJDmec6igTtKViSB8FLX49B5gwhAmcY9ERY,3596
29
29
  ramses_tx/address.py,sha256=F5ZE-EbPNNom1fW9XXUILvD7DYSMBxNJvsHVliT5gjw,8452
30
30
  ramses_tx/command.py,sha256=tqVECwd_QokEcRv2MSk7TUU4JSBzCZcJh1eQ0jIGgoY,125122
31
- ramses_tx/const.py,sha256=pnxq5upXvLUizv9Ye_I1llD9rAa3wddHgsSkc91AIUc,30300
31
+ ramses_tx/const.py,sha256=AMwHitDq115rB24f3fzclNGC4ArMW16DbqiFWQc0U5o,30306
32
32
  ramses_tx/exceptions.py,sha256=FJSU9YkvpKjs3yeTqUJX1o3TPFSe_B01gRGIh9b3PNc,2632
33
33
  ramses_tx/fingerprints.py,sha256=nfftA1E62HQnb-eLt2EqjEi_la0DAoT0wt-PtTMie0s,11974
34
34
  ramses_tx/frame.py,sha256=GzNsXr15YLeidJYGtk_xPqsZQh4ehDDlUCtT6rTDhT8,22046
@@ -38,18 +38,18 @@ ramses_tx/logger.py,sha256=1iKRHKUaqHqGd76CkE_6mCVR0sYODtxshRRwfY61fTk,10426
38
38
  ramses_tx/message.py,sha256=-moQ8v3HVlNSl-x3U0DDfDcj8WQ7vLqclMNxsohbmnw,13449
39
39
  ramses_tx/opentherm.py,sha256=58PXz9l5x8Ou6Fm3y-R_UnGHCYahoi2RKIDdYStUMzk,42378
40
40
  ramses_tx/packet.py,sha256=_qHiPFWpQpKueZOgf1jJ93Y09iZjo3LZWStLglVkXg4,7370
41
- ramses_tx/parsers.py,sha256=Z0ochrNyO2l8SAP0oJxN-tx2nJluEaNP_uJCIm_lsA8,111279
42
- ramses_tx/protocol.py,sha256=9R3aCzuiWEyXmugmB5tyR_RhBlgfnpUXj7AsMP9BzzU,28867
41
+ ramses_tx/parsers.py,sha256=xkffUleahvY6uPb34pcBDYnJ7QNQUzRuaED7wOVeEsE,113466
42
+ ramses_tx/protocol.py,sha256=zjOz0TCzMmbUiLouLFXKRL7u9a6p9YsCkLYDVC5vG-Y,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=NG81GBNZlap-Gi9ac-r6OFE-KaHvXsgPDWy-I2Irr-4,53698
45
+ ramses_tx/ramses.py,sha256=vp748Tf_a-56OMM8CWDA2ZktRfTuj0QVyPRcnsOstSM,53983
46
46
  ramses_tx/schemas.py,sha256=bqKW_V0bR6VbBD8ZQiBExNtVdXs0fryVKe3GEhupgIo,13424
47
- ramses_tx/transport.py,sha256=oVClKNnCfHioIk5tF0TGmYspJhpggQ6EspOYeyBBR4g,58841
47
+ ramses_tx/transport.py,sha256=8U6Hskle_0tdI4WViJT0e9Bl6-OeK1uKBvTQZvHtqyY,58923
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=OmZAmWYXnodOMX2FqbBZjLsKSaBVpA4Y37368OIox1o,123
51
- ramses_rf-0.52.3.dist-info/METADATA,sha256=u8gl35WcjLN5rO1P_BTfchK23MayeJLDQxFEhHOM-v4,4000
52
- ramses_rf-0.52.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
- ramses_rf-0.52.3.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
54
- ramses_rf-0.52.3.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
55
- ramses_rf-0.52.3.dist-info/RECORD,,
50
+ ramses_tx/version.py,sha256=UqvJQixbILsFY_XbawFKzLgN4gL1DKFAtz6JsZAXk4I,123
51
+ ramses_rf-0.52.4.dist-info/METADATA,sha256=XTTcXOovtegIupIA9b5wQPCfRjUeuqtHRMOKKd_5bPo,4179
52
+ ramses_rf-0.52.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
+ ramses_rf-0.52.4.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
54
+ ramses_rf-0.52.4.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
55
+ ramses_rf-0.52.4.dist-info/RECORD,,
ramses_tx/const.py CHANGED
@@ -440,7 +440,7 @@ DEV_TYPE_MAP = attr_dict_factory(
440
440
  ), # CH/DHW devices instead of HVAC/other
441
441
  "HEAT_ZONE_SENSORS": ("00", "01", "03", "04", "12", "22", "34"),
442
442
  "HEAT_ZONE_ACTUATORS": ("00", "02", "04", "13"),
443
- "THM_DEVICES": ("03", "12", "22", "34"),
443
+ "THM_DEVICES": ("03", "12", "21", "22", "34"),
444
444
  "TRV_DEVICES": ("00", "04"),
445
445
  "CONTROLLERS": ("01", "02", "12", "22", "23", "34"), # potentially controllers
446
446
  "PROMOTABLE_SLUGS": (DevType.DEV, DevType.HEA, DevType.HVC),
ramses_tx/parsers.py CHANGED
@@ -1912,6 +1912,7 @@ def parser_2411(payload: str, msg: Message) -> dict[str, Any]:
1912
1912
  "01": (2, centile), # 52 (0.0-25.0) (%)
1913
1913
  "0F": (2, hex_to_percent), # xx (0.0-1.0) (%)
1914
1914
  "10": (4, counter), # 31 (0-1800) (days)
1915
+ # "20": (4, counter), # unknown data type, uncomment when we have more info
1915
1916
  "92": (4, hex_to_temp), # 75 (0-30) (C)
1916
1917
  } # TODO: _2411_TYPES.get(payload[8:10], (8, no_op))
1917
1918
 
@@ -1938,11 +1939,35 @@ def parser_2411(payload: str, msg: Message) -> dict[str, Any]:
1938
1939
  return result
1939
1940
 
1940
1941
  try:
1941
- assert payload[8:10] in _2411_DATA_TYPES, (
1942
- f"param {param_id} has unknown data_type: {payload[8:10]}"
1943
- ) # _INFORM_DEV_MSG
1944
- length, parser = _2411_DATA_TYPES.get(payload[8:10], (8, lambda x: x))
1942
+ # Handle unknown data types gracefully instead of asserting
1943
+ if payload[8:10] not in _2411_DATA_TYPES:
1944
+ warningmsg = (
1945
+ f"{msg!r} < {_INFORM_DEV_MSG} (param {param_id} has unknown data_type: {payload[8:10]}). "
1946
+ f"This parameter uses an unrecognized data type. "
1947
+ f"Please report this packet and any context about what changed on your system."
1948
+ )
1949
+ # Return partial result with raw hex values for unknown data types
1950
+ if msg.len == 9:
1951
+ result |= {
1952
+ "value": f"0x{payload[10:18]}", # Raw hex value
1953
+ "_value_06": payload[6:10],
1954
+ "_unknown_data_type": payload[8:10],
1955
+ }
1956
+ else:
1957
+ result |= {
1958
+ "value": f"0x{payload[10:18]}", # Raw hex value
1959
+ "_value_06": payload[6:10],
1960
+ "min_value": f"0x{payload[18:26]}", # Raw hex value
1961
+ "max_value": f"0x{payload[26:34]}", # Raw hex value
1962
+ "precision": f"0x{payload[34:42]}", # Raw hex value
1963
+ "_value_42": payload[42:],
1964
+ # Flexible footer - capture everything after precision
1965
+ }
1966
+ _LOGGER.warning(f"{warningmsg}. Found values: {result}")
1967
+ return result
1945
1968
 
1969
+ # Handle known data types normally
1970
+ length, parser = _2411_DATA_TYPES[payload[8:10]]
1946
1971
  result |= {
1947
1972
  "value": parser(payload[10:18][-length:]), # type: ignore[operator]
1948
1973
  "_value_06": payload[6:10],
@@ -1962,10 +1987,11 @@ def parser_2411(payload: str, msg: Message) -> dict[str, Any]:
1962
1987
  # eg. older Orcon models may have a footer of 2 bytes
1963
1988
  }
1964
1989
  )
1965
- except AssertionError as err:
1966
- _LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
1967
- # Return partial result for unknown parameters
1968
- result["value"] = ""
1990
+ except Exception as err:
1991
+ _LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} (Error parsing 2411: {err})")
1992
+ # Return partial result for any parsing errors
1993
+ result["value"] = f"0x{payload[10:18]}" # Raw hex value
1994
+ result["_parse_error"] = f"Parser error: {err}"
1969
1995
  return result
1970
1996
 
1971
1997
 
@@ -2981,7 +3007,11 @@ def parser_unknown(payload: str, msg: Message) -> dict[str, Any]:
2981
3007
  "_value": hex_to_temp(payload[2:]),
2982
3008
  }
2983
3009
 
2984
- raise NotImplementedError
3010
+ return {
3011
+ "_payload": payload,
3012
+ "_unknown_code": msg.code,
3013
+ "_parse_error": "No parser available for this packet type",
3014
+ }
2985
3015
 
2986
3016
 
2987
3017
  _PAYLOAD_PARSERS = {
@@ -2998,8 +3028,23 @@ def parse_payload(msg: Message) -> dict | list[dict]:
2998
3028
  :return: a dict of key: value pairs or a list of such dicts, e.g. {'temperature': 21.5}
2999
3029
  """
3000
3030
  result: dict | list[dict]
3001
- result = _PAYLOAD_PARSERS.get(msg.code, parser_unknown)(msg._pkt.payload, msg)
3002
- if isinstance(result, dict) and msg.seqn.isnumeric(): # e.g. 22F1/3
3003
- result["seqx_num"] = msg.seqn
3031
+ try:
3032
+ result = _PAYLOAD_PARSERS.get(msg.code, parser_unknown)(msg._pkt.payload, msg)
3033
+ if isinstance(result, dict) and msg.seqn.isnumeric(): # e.g. 22F1/3
3034
+ result["seqx_num"] = msg.seqn
3035
+ except AssertionError as err:
3036
+ _LOGGER.warning(
3037
+ f"{msg!r} < {_INFORM_DEV_MSG} ({err}). "
3038
+ f"This packet could not be parsed completely. "
3039
+ f"Please report this message and any context about what changed on your system when this occurred."
3040
+ )
3041
+ # Return partial result with error info
3042
+ result = {
3043
+ "_payload": msg._pkt.payload,
3044
+ "_parse_error": f"AssertionError: {err}",
3045
+ "_unknown_code": msg.code,
3046
+ }
3047
+ if isinstance(result, dict) and msg.seqn.isnumeric():
3048
+ result["seqx_num"] = msg.seqn
3004
3049
 
3005
3050
  return result
ramses_tx/protocol.py CHANGED
@@ -583,7 +583,7 @@ class PortProtocol(_DeviceIdFilterMixin, _BaseProtocol):
583
583
  self._context.pause_writing()
584
584
 
585
585
  def resume_writing(self) -> None:
586
- """Inform the FSM that the Protocol has been paused."""
586
+ """Inform the FSM that the Protocol has been resumed."""
587
587
 
588
588
  super().resume_writing()
589
589
  if self._context:
@@ -597,7 +597,7 @@ class PortProtocol(_DeviceIdFilterMixin, _BaseProtocol):
597
597
  self._context.pkt_received(pkt)
598
598
 
599
599
  async def _send_impersonation_alert(self, cmd: Command) -> None:
600
- """Send an puzzle packet warning that impersonation is occurring."""
600
+ """Send a puzzle packet warning that impersonation is occurring."""
601
601
 
602
602
  if _DBG_DISABLE_IMPERSONATION_ALERTS:
603
603
  return
ramses_tx/ramses.py CHANGED
@@ -377,10 +377,10 @@ CODES_SCHEMA: dict[Code, dict[str, Any]] = { # rf_unknown
377
377
  I_: r"^(0[0-9A-F][0-9A-F]{8}0[12]){1,4}(0[12]03)?$", # (0[12]03)? only if len(array) == 1
378
378
  W_: r"^(0[0-9A-F][0-9A-F]{8}0[12])$", # never an array
379
379
  },
380
- Code._22D0: { # unknown_22d0, HVAC system switch?
380
+ Code._22D0: { # unknown_22d0, Spider thermostat, HVAC system switch?
381
381
  SZ_NAME: "message_22d0",
382
- I_: r"^(00|03)",
383
- W_: r"^03",
382
+ I_: r"^(00|03)[0-9]{6}$",
383
+ W_: r"^03[0-9]{4}1E14030020$",
384
384
  },
385
385
  Code._22D9: { # boiler_setpoint
386
386
  SZ_NAME: "boiler_setpoint",
@@ -843,6 +843,7 @@ _DEV_KLASSES_HEAT: dict[str, dict[Code, dict[VerbT, Any]]] = {
843
843
  Code._000C: {I_: {}},
844
844
  Code._000E: {I_: {}},
845
845
  Code._0016: {RQ: {}},
846
+ Code._01FF: {I_: {}, RQ: {}},
846
847
  Code._042F: {I_: {}},
847
848
  Code._1030: {I_: {}},
848
849
  Code._1060: {I_: {}},
@@ -853,9 +854,11 @@ _DEV_KLASSES_HEAT: dict[str, dict[Code, dict[VerbT, Any]]] = {
853
854
  Code._1F09: {I_: {}},
854
855
  Code._1FC9: {I_: {}},
855
856
  Code._22C9: {W_: {}}, # DT4R
857
+ Code._22D0: {W_: {}}, # Spider master THM
856
858
  Code._2309: {I_: {}, RQ: {}, W_: {}},
857
859
  Code._2349: {RQ: {}, W_: {}},
858
860
  Code._30C9: {I_: {}},
861
+ Code._3110: {I_: {}}, # Spider THM
859
862
  Code._3120: {I_: {}},
860
863
  Code._313F: {
861
864
  I_: {}
@@ -880,6 +883,7 @@ _DEV_KLASSES_HEAT: dict[str, dict[Code, dict[VerbT, Any]]] = {
880
883
  Code._3110: {I_: {}}, # Spider Autotemp
881
884
  Code._3150: {I_: {}},
882
885
  Code._4E01: {I_: {}}, # Spider Autotemp Zone controller
886
+ Code._4E04: {I_: {}}, # idem
883
887
  },
884
888
  DevType.TRV: { # e.g. HR92/HR91: Radiator Controller
885
889
  Code._0001: {W_: {r"^0[0-9A-F]"}},
@@ -1178,9 +1182,9 @@ _CODE_ONLY_FROM_CTL: tuple[Code, ...] = tuple(
1178
1182
  CODES_ONLY_FROM_CTL: tuple[Code, ...] = (
1179
1183
  Code._1030,
1180
1184
  Code._1F09,
1181
- Code._22D0,
1185
+ # Code._22D0, # also _W from the Spider master THM! issue #340
1182
1186
  Code._313F,
1183
- ) # I packets, TODO: 31Dx too?
1187
+ ) # I packets, TODO: 31Dx too? not 31D9/31DA!
1184
1188
 
1185
1189
  #
1186
1190
  ########################################################################################
ramses_tx/transport.py CHANGED
@@ -1598,4 +1598,5 @@ async def transport_factory(
1598
1598
 
1599
1599
  # TODO: remove this? better to invoke timeout after factory returns?
1600
1600
  await protocol.wait_for_connection_made(timeout=_DEFAULT_TIMEOUT_PORT)
1601
+ # pytest-cov times out in virtual_rf.py when set below 30.0 on GitHub Actions
1601
1602
  return transport
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.52.3"
3
+ __version__ = "0.52.4"
4
4
  VERSION = __version__