ramses-rf 0.52.0__py3-none-any.whl → 0.52.2__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/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """RAMSES RF - a RAMSES-II protocol decoder & analyser.
3
+ `ramses_rf` takes care of the device (upper) layer.
3
4
 
4
5
  Works with (amongst others):
5
6
  - evohome (up to 12 zones)
ramses_rf/database.py CHANGED
@@ -1,5 +1,25 @@
1
1
  #!/usr/bin/env python3
2
- """RAMSES RF - Message database and index."""
2
+ """
3
+ RAMSES RF - Message database and index.
4
+
5
+ .. table:: Database Query Methods[^1][#fn1]
6
+ :widths: auto
7
+
8
+ ===== ============ =========== ========== ==== ========================
9
+ ix method name args returns uses used by
10
+ ===== ============ =========== ========== ==== ========================
11
+ i1 get Msg, kwargs tuple(Msg) i3
12
+ i2 contains kwargs bool i4
13
+ i3 _select_from kwargs tuple(Msg) i4
14
+ i4 qry_dtms kwargs list(dtm)
15
+ i5 qry sql, kwargs tuple(Msg) _msgs()
16
+ i6 qry_field sql, kwargs tuple(fld) e4, e5
17
+ i7 get_rp_codes src, dst list(Code) Discovery-supported_cmds
18
+ ===== ============ =========== ========== ==== ========================
19
+
20
+ [#fn1] A word of explanation.
21
+ [^1]: ex = entity_base.py query methods
22
+ """
3
23
 
4
24
  from __future__ import annotations
5
25
 
@@ -62,9 +82,10 @@ def payload_keys(parsed_payload: list[dict] | dict) -> str: # type: ignore[type
62
82
 
63
83
 
64
84
  class MessageIndex:
65
- """A simple in-memory SQLite3 database for indexing RF messages.
66
- Index holds the latest message to & from all devices by header
67
- (example of a hdr: 000C|RP|01:223036|0208)."""
85
+ """A central in-memory SQLite3 database for indexing RF messages.
86
+ Index holds all the latest messages to & from all devices by `dtm`
87
+ (timestamp) and `hdr` header
88
+ (example of a hdr: ``000C|RP|01:223036|0208``)."""
68
89
 
69
90
  _housekeeping_task: asyncio.Task[None]
70
91
 
@@ -72,7 +93,8 @@ class MessageIndex:
72
93
  """Instantiate a message database/index."""
73
94
 
74
95
  self.maintain = maintain
75
- self._msgs: MsgDdT = OrderedDict() # stores all messages for retrieval. Filled & cleaned up in housekeeping_loop.
96
+ self._msgs: MsgDdT = OrderedDict() # stores all messages for retrieval.
97
+ # Filled & cleaned up in housekeeping_loop.
76
98
 
77
99
  # Connect to a SQLite DB in memory
78
100
  self._cx = sqlite3.connect(
@@ -122,16 +144,17 @@ class MessageIndex:
122
144
  def _setup_db_schema(self) -> None:
123
145
  """Set up the message database schema.
124
146
 
125
- messages TABLE Fields:
126
-
127
- - dtm message timestamp
128
- - verb " I", "RQ" etc.
129
- - src message origin address
130
- - dst message destination address
131
- - code packet code aka command class e.g. _0005, _31DA
132
- - ctx message context, created from payload as index + extra markers (Heat)
133
- - hdr packet header e.g. 000C|RP|01:223036|0208 (see: src/ramses_tx/frame.py)
134
- - plk the keys stored in the parsed payload, separated by the | char
147
+ .. note::
148
+ messages TABLE Fields:
149
+
150
+ - dtm message timestamp
151
+ - verb " I", "RQ" etc.
152
+ - src message origin address
153
+ - dst message destination address
154
+ - code packet code aka command class e.g. _0005, _31DA
155
+ - ctx message context, created from payload as index + extra markers (Heat)
156
+ - hdr packet header e.g. 000C|RP|01:223036|0208 (see: src/ramses_tx/frame.py)
157
+ - plk the keys stored in the parsed payload, separated by the | char
135
158
  """
136
159
 
137
160
  self._cu.execute(
@@ -160,7 +183,7 @@ class MessageIndex:
160
183
 
161
184
  async def _housekeeping_loop(self) -> None:
162
185
  """Periodically remove stale messages from the index,
163
- unless self.maintain is False."""
186
+ unless `self.maintain` is False."""
164
187
 
165
188
  async def housekeeping(dt_now: dt, _cutoff: td = td(days=1)) -> None:
166
189
  """
@@ -196,6 +219,7 @@ class MessageIndex:
196
219
  """
197
220
  Add a single message to the MessageIndex.
198
221
  Logs a warning if there is a duplicate dtm.
222
+
199
223
  :returns: any message that was removed because it had the same header
200
224
  """
201
225
  # TODO: eventually, may be better to use SqlAlchemy
@@ -224,22 +248,26 @@ class MessageIndex:
224
248
  pass # self._lock.release()
225
249
 
226
250
  if (
227
- dup and msg.src is not msg.src
228
- ): # when src==dst, expect to add duplicate, don't check
229
- _LOGGER.warning(
251
+ dup and msg.src is not msg.dst and not msg.dst.id.startswith("18:") # HGI
252
+ ): # when src==dst, expect to add duplicate, don't warn
253
+ _LOGGER.debug(
230
254
  "Overwrote dtm (%s) for %s: %s (contrived log?)",
231
255
  msg.dtm,
232
256
  msg._pkt._hdr,
233
257
  dup[0]._pkt,
234
258
  )
235
259
  if old is not None:
236
- _LOGGER.info("Old msg replaced: %s", old)
260
+ _LOGGER.debug("Old msg replaced: %s", old)
237
261
 
238
262
  return old
239
263
 
240
264
  def add_record(self, src: str, code: str = "", verb: str = "") -> None:
241
265
  """
242
- Add a single record to the MessageIndex with timestamp now() and no Message contents.
266
+ Add a single record to the MessageIndex with timestamp `now()` and no Message contents.
267
+
268
+ :param src: device id to use as source address
269
+ :param code: device id to use as destination address (can be identical)
270
+ :param verb: two letter verb str to use
243
271
  """
244
272
  # Used by OtbGateway init, via entity_base.py
245
273
  dtm: DtmStrT = dt.strftime(dt.now(), "%Y-%m-%dT%H:%M:%S") # type: ignore[assignment]
@@ -274,6 +302,7 @@ class MessageIndex:
274
302
  def _insert_into(self, msg: Message) -> Message | None:
275
303
  """
276
304
  Insert a message into the index.
305
+
277
306
  :returns: any message replaced (by same hdr)
278
307
  """
279
308
  assert msg._pkt._hdr is not None, "Skipping: Packet has no hdr: {msg._pkt}"
@@ -305,7 +334,7 @@ class MessageIndex:
305
334
  payload_keys(msg.payload),
306
335
  ),
307
336
  )
308
- _LOGGER.info(f"Added {msg} to gwy.msg_db")
337
+ _LOGGER.debug(f"Added {msg} to gwy.msg_db")
309
338
 
310
339
  return _old_msgs[0] if _old_msgs else None
311
340
 
@@ -348,6 +377,7 @@ class MessageIndex:
348
377
 
349
378
  def _delete_from(self, **kwargs: bool | dt | str) -> tuple[Message, ...]:
350
379
  """Remove message(s) from the index.
380
+
351
381
  :returns: any messages that were removed"""
352
382
 
353
383
  msgs = self._select_from(**kwargs)
@@ -359,32 +389,14 @@ class MessageIndex:
359
389
 
360
390
  return msgs
361
391
 
362
- # MessageIndex msg_db query methods > copy to docs/source/ramses_rf.rst
363
- # (ex = entity_base.py query methods
364
- #
365
- # +----+--------------+-------------+------------+------+--------------------------+
366
- # | ix |method name | args | returns | uses | used by |
367
- # +====+==============+=============+============+======+==========================+
368
- # | i1 | get | Msg/kwargs | tuple[Msg] | i3 | |
369
- # +----+--------------+-------------+------------+------+--------------------------+
370
- # | i2 | contains | kwargs | bool | i4 | |
371
- # +----+--------------+-------------+------------+------+--------------------------+
372
- # | i3 | _select_from | kwargs | tuple[Msg] | i4 | |
373
- # +----+--------------+-------------+------------+------+--------------------------+
374
- # | i4 | qry_dtms | kwargs | list(dtm) | | |
375
- # +----+--------------+-------------+------------+------+--------------------------+
376
- # | i5 | qry | sql, kwargs | tuple[Msg] | | _msgs() |
377
- # +----+--------------+-------------+------------+------+--------------------------+
378
- # | i6 | qry_field | sql, kwargs | tuple[fld] | | e4, e5 |
379
- # +----+--------------+-------------+------------+------+--------------------------+
380
- # | i7 | get_rp_codes | src, dst | list[Code] | | Discovery#supported_cmds |
381
- # +----+--------------+-------------+------------+------+--------------------------+
392
+ # MessageIndex msg_db query methods
382
393
 
383
394
  def get(
384
395
  self, msg: Message | None = None, **kwargs: bool | dt | str
385
396
  ) -> tuple[Message, ...]:
386
397
  """
387
398
  Public method to get a set of message(s) from the index.
399
+
388
400
  :param msg: Message to return, by dtm (expect a single result as dtm is unique key)
389
401
  :param kwargs: data table field names and criteria, e.g. (hdr=...)
390
402
  :return: tuple of matching Messages
@@ -401,6 +413,7 @@ class MessageIndex:
401
413
  def contains(self, **kwargs: bool | dt | str) -> bool:
402
414
  """
403
415
  Check if the MessageIndex contains at least 1 record that matches the provided fields.
416
+
404
417
  :param kwargs: (exact) SQLite table field_name: required_value pairs
405
418
  :return: True if at least one message fitting the given conditions is present, False when qry returned empty
406
419
  """
@@ -410,6 +423,7 @@ class MessageIndex:
410
423
  def _select_from(self, **kwargs: bool | dt | str) -> tuple[Message, ...]:
411
424
  """
412
425
  Select message(s) using the MessageIndex.
426
+
413
427
  :param kwargs: (exact) SQLite table field_name: required_value pairs
414
428
  :returns: a tuple of qualifying messages
415
429
  """
@@ -422,6 +436,7 @@ class MessageIndex:
422
436
  def qry_dtms(self, **kwargs: bool | dt | str) -> list[Any]:
423
437
  """
424
438
  Select from the ImageIndex a list of dtms that match the provided arguments.
439
+
425
440
  :param kwargs: data table field names and criteria
426
441
  :return: list of unformatted dtms that match, useful for msg lookup, or an empty list if 0 matches
427
442
  """
@@ -444,6 +459,7 @@ class MessageIndex:
444
459
  def qry(self, sql: str, parameters: tuple[str, ...]) -> tuple[Message, ...]:
445
460
  """
446
461
  Get a tuple of messages from _msgs using the index, given sql and parameters.
462
+
447
463
  :param sql: a bespoke SQL query SELECT string that should return dtm as first field
448
464
  :param parameters: tuple of kwargs with the selection filter
449
465
  :return: a tuple of qualifying messages
@@ -468,12 +484,13 @@ class MessageIndex:
468
484
  if ts in self._msgs:
469
485
  lst.append(self._msgs[ts])
470
486
  else: # happens in tests with artificial msg from heat
471
- _LOGGER.warning("MessageIndex timestamp %s not in device messages", ts)
487
+ _LOGGER.info("MessageIndex timestamp %s not in device messages", ts)
472
488
  return tuple(lst)
473
489
 
474
490
  def get_rp_codes(self, parameters: tuple[str, ...]) -> list[Code]:
475
491
  """
476
492
  Get a list of Codes from the index, given parameters.
493
+
477
494
  :param parameters: tuple of additional kwargs
478
495
  :return: list of Code: value pairs
479
496
  """
@@ -499,6 +516,7 @@ class MessageIndex:
499
516
  ) -> list[tuple[dt | str, str]]:
500
517
  """
501
518
  Get a list of fields from the index, given select sql and parameters.
519
+
502
520
  :param sql: a bespoke SQL query SELECT string
503
521
  :param parameters: tuple of additional kwargs
504
522
  :return: list of key: value pairs as defined in sql
@@ -529,7 +547,7 @@ class MessageIndex:
529
547
  # if include_expired or not self._msgs[ts].HAS_EXPIRED: # not working
530
548
  lst.append(self._msgs[ts])
531
549
  else: # happens in tests with dummy msg from heat init
532
- _LOGGER.warning("MessageIndex ts %s not in device messages", ts)
550
+ _LOGGER.info("MessageIndex ts %s not in device messages", ts)
533
551
  return tuple(lst)
534
552
 
535
553
  def clr(self) -> None:
ramses_rf/device/heat.py CHANGED
@@ -1263,6 +1263,7 @@ class BdrSwitch(Actuator, RelayDemand): # BDR (13):
1263
1263
  """The BDR class, such as a BDR91.
1264
1264
 
1265
1265
  BDR91s can be used in six distinct modes, including:
1266
+
1266
1267
  - x2 boiler controller (FC/TPI): either traditional, or newer heat pump-aware
1267
1268
  - x1 electric heat zones (0x/ELE)
1268
1269
  - x1 zone valve zones (0x/VAL)
ramses_rf/device/hvac.py CHANGED
@@ -728,12 +728,15 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
728
728
  In HomeAssistant, ramses_cc, you can set a bound device in the device configuration.
729
729
 
730
730
  System schema and known devices example:
731
- "32:153289":
732
- bound: "37:168270"
733
- class: FAN
734
- "37:168270":
735
- class: REM
736
- faked: true
731
+
732
+ .. code-block::
733
+
734
+ "32:153289":
735
+ bound: "37:168270"
736
+ class: FAN
737
+ "37:168270":
738
+ class: REM
739
+ faked: true
737
740
 
738
741
  :param device_id: The unique identifier of the device to bind
739
742
  :type device_id: str
@@ -927,7 +930,7 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
927
930
  """
928
931
  res_mode: list = self._msg_qry(sql)
929
932
  # SQLite query on MessageIndex
930
- _LOGGER.info(f"{res_mode} # FAN_MODE FETCHED from MessageIndex")
933
+ _LOGGER.debug(f"{res_mode} # FAN_MODE FETCHED from MessageIndex")
931
934
 
932
935
  sql = f"""
933
936
  SELECT code from messages WHERE verb in (' I', 'RP')
@@ -936,7 +939,7 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
936
939
  """
937
940
  res_rate: list = self._msg_qry(sql)
938
941
  # SQLite query on MessageIndex
939
- _LOGGER.info(
942
+ _LOGGER.debug(
940
943
  f"{res_rate} # FAN_RATE FETCHED from MessageIndex"
941
944
  ) # DEBUG always empty?
942
945
 
@@ -1142,7 +1145,7 @@ def class_dev_hvac(
1142
1145
  ) -> type[DeviceHvac]:
1143
1146
  """Return a device class, but only if the device must be from the HVAC group.
1144
1147
 
1145
- May return a base clase, DeviceHvac, which will need promotion.
1148
+ May return a base class, `DeviceHvac`, which will need promotion.
1146
1149
  """
1147
1150
 
1148
1151
  if not eavesdrop:
@@ -1206,54 +1209,111 @@ _REMOTES = {
1206
1209
  # see: https://github.com/arjenhiemstra/ithowifi/blob/master/software/NRG_itho_wifi/src/IthoPacket.h
1207
1210
 
1208
1211
  """
1209
- # CVE/HRU remote (536-0124) [RFT W: 3 modes, timer]
1210
- "away": (Code._22F1, 00, 01|04"), # how to invoke?
1211
- "low": (Code._22F1, 00, 02|04"), # aka eco
1212
- "medium": (Code._22F1, 00, 03|04"), # aka auto (with sensors) - is that only for 63?
1213
- "high": (Code._22F1, 00, 04|04"), # aka full
1214
-
1215
- "timer_1": (Code._22F3, 00, 00|0A"), # 10 minutes full speed
1216
- "timer_2": (Code._22F3, 00, 00|14"), # 20 minutes full speed
1217
- "timer_3": (Code._22F3, 00, 00|1E"), # 30 minutes full speed
1218
-
1219
- # RFT-AUTO (536-0150) [RFT CAR: 2 modes, auto, timer]: idx = 63, essentially same as above, but also...
1220
- "auto_night": (Code._22F8, 63, 02|03"), # additional - press auto x2
1221
-
1222
- # RFT-RV (04-00046), RFT-CO2 (04-00045) - sensors with control
1223
- "medium": (Code._22F1, 00, 03|07"), 1=away, 2=low?
1224
- "auto": (Code._22F1, 00, 05|07"), 4=high
1225
- "auto_night": (Code._22F1, 00, 0B|0B"),
1226
-
1227
- "timer_1": (Code._22F3, 00, 00|0A, 00|00, 0000"), # 10 minutes
1228
- "timer_2": (Code._22F3, 00, 00|14, 00|00, 0000"), # 20 minutes
1229
- "timer_3": (Code._22F3, 00, 00|1E, 00|00, 0000"), # 30 minutes
1230
-
1231
- # RFT-PIR (545-7550) - presence sensor
1232
-
1233
- # RFT_DF: DemandFlow remote (536-0146)
1234
- "timer_1": (Code._22F3, 00, 42|03, 03|03"), # 0b01-000-010 = 3 hrs, back to last mode
1235
- "timer_2": (Code._22F3, 00, 42|06, 03|03"), # 0b01-000-010 = 6 hrs, back to last mode
1236
- "timer_3": (Code._22F3, 00, 42|09, 03|03"), # 0b01-000-010 = 9 hrs, back to last mode
1237
- "cook_30": (Code._22F3, 00, 02|1E, 02|03"), # 30 mins (press 1x)
1238
- "cook_60": (Code._22F3, 00, 02|3C, 02|03"), # 60 mins (press 2x)
1239
-
1240
- "low": (Code._22F8, 00, 01|02"), # ?eco co2 <= 1200 ppm?
1241
- "high": (Code._22F8, 00, 02|02"), # ?comfort co2 <= 1000 ppm?
1242
-
1243
- # Join commands:
1244
- "CVERFT": (Code._1FC9, 00, Code._22F1, 0x000000, 01, Code._10E0, 0x000000"), # CVE/HRU remote (536-0124)
1245
- "AUTORFT": (Code._1FC9, 63, Code._22F8, 0x000000, 01, Code._10E0, 0x000000"), # AUTO RFT (536-0150)
1246
- "DF": (Code._1FC9, 00, Code._22F8, 0x000000, 00, Code._10E0, 0x000000"), # DemandFlow remote (536-0146)
1247
- "RV": (Code._1FC9, 00, Code._12A0, 0x000000, 01, Code._10E0, 0x000000, 00, Code._31E0, 0x000000, 00, Code._1FC9, 0x000000"), # RFT-RV (04-00046)
1248
- "CO2": (Code._1FC9, 00, Code._1298, 0x000000, 00, Code._2E10, 0x000000, 01, Code._10E0, 0x000000, 00, Code._31E0, 0x000000, 00, Code._1FC9, 0x000000"), # RFT-CO2 (04-00045)
1249
-
1250
- # Leave commands:
1251
- "Others": (Code._1FC9, 00, Code._1FC9, 0x000000"), # standard leave command
1252
- "AUTORFT": (Code._1FC9, 63, Code._1FC9, 0x000000"), # leave command of AUTO RFT (536-0150)
1253
-
1254
- # RQ 0x00
1255
- # I_ 0x01
1256
- # W_ 0x02
1257
- # RP 0x03
1212
+ Itho Remote (model) enums.
1213
+
1214
+ CVE/HRU remote (536-0124) RFT W: 3 modes, timer
1215
+ -------------------------------------------------
1216
+
1217
+ .. table:: 536-0124
1218
+ :widths: auto
1219
+
1220
+ =========== ========================= ================================================
1221
+ "away": (Code._22F1, 00, 01|04"), how to invoke?
1222
+ "low": (Code._22F1, 00, 02|04"), aka eco
1223
+ "medium": (Code._22F1, 00, 03|04"), aka auto (with sensors) - is that only for 63?
1224
+ "high": (Code._22F1, 00, 04|04"), aka full
1225
+
1226
+ "timer_1": (Code._22F3, 00, 00|0A"), 10 minutes full speed
1227
+ "timer_2": (Code._22F3, 00, 00|14"), 20 minutes full speed
1228
+ "timer_3": (Code._22F3, 00, 00|1E"), 30 minutes full speed
1229
+ =========== ========================= ================================================
1230
+
1231
+ RFT-AUTO (536-0150) RFT CAR: 2 modes, auto, timer: idx = 63, essentially same as above, but also...
1232
+ -----------------------------------------------------------------------------------------------------
1233
+
1234
+ .. table:: 536-0150
1235
+ :widths: auto
1236
+
1237
+ ============= ========================= ================================================
1238
+ "auto_night": (Code._22F8, 63, 02|03"), additional - press auto x2
1239
+ ============= ========================= ================================================
1240
+
1241
+ RFT-RV (04-00046), RFT-CO2 (04-00045) - sensors with control
1242
+ ------------------------------------------------------------
1243
+
1244
+ .. table:: 04-00046
1245
+ :widths: auto
1246
+
1247
+ ============== ======================================== =============
1248
+ "medium": (Code._22F1, 00, 03|07"), 1=away, 2=low?
1249
+ "auto": (Code._22F1, 00, 05|07"), 4=high
1250
+ "auto_night": (Code._22F1, 00, 0B|0B"),
1251
+
1252
+ "timer_1": (Code._22F3, 00, 00|0A, 00|00, 0000"), 10 minutes
1253
+ "timer_2": (Code._22F3, 00, 00|14, 00|00, 0000"), 20 minutes
1254
+ "timer_3": (Code._22F3, 00, 00|1E, 00|00, 0000"), 30 minutes
1255
+ ============== ======================================== =============
1256
+
1257
+ RFT-PIR (545-7550) - presence sensor
1258
+ ------------------------------------
1259
+
1260
+ RFT_DF: DemandFlow remote (536-0146)
1261
+ ------------------------------------
1262
+
1263
+ .. table:: 536-0146
1264
+ :widths: auto
1265
+
1266
+ =========== ================================ =========================================
1267
+ "timer_1": (Code._22F3, 00, 42|03, 03|03"), 0b01-000-010 = 3 hrs, back to last mode
1268
+ "timer_2": (Code._22F3, 00, 42|06, 03|03"), 0b01-000-010 = 6 hrs, back to last mode
1269
+ "timer_3": (Code._22F3, 00, 42|09, 03|03"), 0b01-000-010 = 9 hrs, back to last mode
1270
+ "cook_30": (Code._22F3, 00, 02|1E, 02|03"), 30 mins (press 1x)
1271
+ "cook_60": (Code._22F3, 00, 02|3C, 02|03"), 60 mins (press 2x)
1272
+
1273
+ "low": (Code._22F8, 00, 01|02"), ?eco co2 <= 1200 ppm?
1274
+ "high": (Code._22F8, 00, 02|02"), ?comfort co2 <= 1000 ppm?
1275
+ =========== ================================ =========================================
1276
+
1277
+
1278
+ Join commands:
1279
+ --------------
1280
+
1281
+ .. table:: join per accessory type
1282
+ :widths: auto
1283
+
1284
+ ========== ================= ===================== ========================= ========================== ========================= ========================== ================= ==========
1285
+ type set 1 set 2 set 3 set 4 set 5 set 6 description art #
1286
+ ========== ================= ===================== ========================= ========================== ========================= ========================== ================= ==========
1287
+ "CVERFT": (Code._1FC9, 00, Code._22F1, 0x000000, 01, Code._10E0, 0x000000") CVE/HRU remote (536-0124)
1288
+ "AUTORFT": (Code._1FC9, 63, Code._22F8, 0x000000, 01, Code._10E0, 0x000000") AUTO RFT (536-0150)
1289
+ "DF": (Code._1FC9, 00, Code._22F8, 0x000000, 00, Code._10E0, 0x000000") DemandFlow remote (536-0146)
1290
+ "RV": (Code._1FC9, 00, Code._12A0, 0x000000, 01, Code._10E0, 0x000000, 00, Code._31E0, 0x000000, 00, Code._1FC9, 0x000000") RFT-RV (04-00046)
1291
+ "CO2": (Code._1FC9, 00, Code._1298, 0x000000, 00, Code._2E10, 0x000000, 01, Code._10E0, 0x000000, 00, Code._31E0, 0x000000, 00, Code._1FC9, 0x000000") RFT-CO2 (04-00045)
1292
+ ========== ================= ===================== ========================= ========================== ========================= ========================== ================= ==========
1293
+
1294
+ Leave commands:
1295
+ ---------------
1296
+
1297
+ .. table:: leave per accessory type
1298
+ :widths: auto
1299
+
1300
+ ========== ================= ====================== ========================= ==========
1301
+ type set 1 set 2 description art #
1302
+ ========== ================= ====================== ========================= ==========
1303
+ "Others": (Code._1FC9, 00, Code._1FC9, 0x000000") standard leave command
1304
+ "AUTORFT": (Code._1FC9, 63, Code._1FC9, 0x000000") leave command of AUTO RFT (536-0150)
1305
+ ========== ================= ====================== ========================= ==========
1306
+
1307
+ .. table:: verbs
1308
+ :widths: 2, 4
1309
+
1310
+ ====== ========
1311
+ verb byte
1312
+ ====== ========
1313
+ ``RQ`` ``0x00``
1314
+ ``I_`` ``0x01``
1315
+ ``W_`` ``0x02``
1316
+ ``RP`` ``0x03``
1317
+ ====== ========
1258
1318
 
1259
1319
  """
ramses_rf/dispatcher.py CHANGED
@@ -186,6 +186,12 @@ def process_msg(gwy: Gateway, msg: Message) -> None:
186
186
  # which requires a valid payload only for 000C.
187
187
 
188
188
  def logger_xxxx(msg: Message) -> None:
189
+ """
190
+ Log msg according to src, code, log.debug setting.
191
+
192
+ :param msg: the Message being processed
193
+ :return: None
194
+ """
189
195
  if _DBG_FORCE_LOG_MESSAGES:
190
196
  _LOGGER.warning(msg)
191
197
  elif msg.src != gwy.hgi or (msg.code != Code._PUZZ and msg.verb != RQ):
@@ -268,10 +274,9 @@ def process_msg(gwy: Gateway, msg: Message) -> None:
268
274
 
269
275
  else:
270
276
  logger_xxxx(msg)
271
- # if gwy.msg_db:
272
- # gwy.msg_db.add(
273
- # msg
274
- # ) # why add it? passes all tests without
277
+ if gwy.msg_db:
278
+ gwy.msg_db.add(msg)
279
+ # why add it? enable for evohome
275
280
 
276
281
 
277
282
  # TODO: this needs cleaning up (e.g. handle intervening packet)
ramses_rf/entity_base.py CHANGED
@@ -67,7 +67,8 @@ if TYPE_CHECKING:
67
67
 
68
68
 
69
69
  _QOS_TX_LIMIT = 12 # TODO: needs work
70
- _ID_SLICE = 12 # was 9 for base address only, 12 for _02 sub/params_test?
70
+ _ID_SLICE = 9 # base address only, legacy _msgs 9
71
+ _SQL_SLICE = 12 # msg_db dst field query 12
71
72
  _SZ_LAST_PKT: Final = "last_msg"
72
73
  _SZ_NEXT_DUE: Final = "next_due"
73
74
  _SZ_TIMEOUT: Final = "timeout"
@@ -171,7 +172,31 @@ class _Entity:
171
172
 
172
173
 
173
174
  class _MessageDB(_Entity):
174
- """Maintain/utilize an entity's state database."""
175
+ """Maintain/utilize an entity's state database.
176
+
177
+ EntityBase msg_db query methods
178
+
179
+ (ix = database.py.MessageIndex method)
180
+
181
+ .. table:: Database Query Methods
182
+ :widths: auto
183
+
184
+ ==== ====================== ==================== ============ ========== ==========
185
+ e. method name args returns uses used by
186
+ ==== ====================== ==================== ============ ========== ==========
187
+ e1 _get_msg_by_hdr hdr Message i3 discover
188
+ e2 _msg_value code(s), Msg, args dict[k,v] e3,e4
189
+ e3 _msg_value_code code, verb, key dict[k,v] e4,e5,e6 e6
190
+ e4 _msg_value_msg Msg, (code) dict[k,v] e2,e3
191
+ e5 _msg_qry_by_code_key code, key, (verb=) e6,
192
+ e6 _msg_value_qry_by_code key code, key str/float e3,e5
193
+ e7 _msg_qry sql e8
194
+ e8 _msg_count sql e7
195
+ e9 supported_cmds list(Codes) i7
196
+ e10 _msgs() i5
197
+ ==== ====================== ==================== ============ ========== ==========
198
+
199
+ """
175
200
 
176
201
  _gwy: Gateway
177
202
  ctl: Controller
@@ -193,18 +218,18 @@ class _MessageDB(_Entity):
193
218
  self._msgs_: dict[
194
219
  Code, Message
195
220
  ] = {} # TODO(eb): deprecated, used in test, remove Q1 2026
196
- if not self._gwy.msg_db: # TODO(eb): deprecated since 0.52.0, remove Q1 2026
221
+ if not self._gwy.msg_db: # TODO(eb): deprecated since 0.52.1, remove Q1 2026
197
222
  self._msgz_: dict[
198
223
  Code, dict[VerbT, dict[bool | str | None, Message]]
199
224
  ] = {} # code/verb/ctx,
200
225
 
201
- # As of 0.52.0 we use SQLite MessageIndex, see ramses_rf/database.py
226
+ # As of 0.52.1 we use SQLite MessageIndex, see ramses_rf/database.py
202
227
  # _msgz_ (nested) was only used in this module. Note:
203
228
  # _msgz (now rebuilt from _msgs) also used in: client, base, device.heat
204
229
 
205
230
  def _handle_msg(self, msg: Message) -> None:
206
231
  """Store a msg in the DBs.
207
- Uses SQLite MessageIndex since 0.52.0
232
+ Uses SQLite MessageIndex since 0.52.1
208
233
  """
209
234
 
210
235
  if not (
@@ -219,7 +244,13 @@ class _MessageDB(_Entity):
219
244
  return # don't store the rest
220
245
 
221
246
  if self._gwy.msg_db: # central SQLite MessageIndex
222
- self._gwy.msg_db.add(msg)
247
+ _LOGGER.debug(
248
+ "For %s (z_id %s) add msg %s, src %s, dst %s to msg_db.",
249
+ self.id,
250
+ self._z_id,
251
+ msg.src,
252
+ msg.dst,
253
+ )
223
254
  debug_code: Code = Code._3150
224
255
  if msg.code == debug_code and msg.src.id.startswith("01:"):
225
256
  _LOGGER.debug(
@@ -232,11 +263,12 @@ class _MessageDB(_Entity):
232
263
  # Result in test log: lookup fails
233
264
  # msg.src = 01:073976 (CTL)
234
265
  # Added msg from 01:073976 (CTL) with code 0005 to _gwy.msg_db
235
- # query is for: 01:073976 < no suffix
266
+ # query is for: 01:073976 < no suffix, extended lookup to [:12] chars
267
+ self._gwy.msg_db.add(msg)
236
268
 
237
269
  # ignore any replaced message that might be returned
238
270
  else: # TODO(eb): remove Q1 2026
239
- if msg.code not in self._msgz_: # deprecated since 0.52.0
271
+ if msg.code not in self._msgz_: # deprecated since 0.52.1
240
272
  # Store msg verb + ctx by code in nested self._msgz_ Dict
241
273
  self._msgz_[msg.code] = {msg.verb: {msg._pkt._ctx: msg}}
242
274
  elif msg.verb not in self._msgz_[msg.code]:
@@ -247,7 +279,7 @@ class _MessageDB(_Entity):
247
279
  self._msgz_[msg.code][msg.verb][msg._pkt._ctx] = msg
248
280
 
249
281
  # Also store msg by code in flat self._msgs_ dict (stores the latest I/RP msgs by code)
250
- # TODO(eb): deprecated since 0.52.0, remove next block _msgs_ Q1 2026
282
+ # TODO(eb): deprecated since 0.52.1, remove next block _msgs_ Q1 2026
251
283
  if msg.verb in (I_, RP): # drop RQ's
252
284
  # if msg.code == Code._3150 and msg.src.id.startswith(
253
285
  # "02:"
@@ -270,7 +302,12 @@ class _MessageDB(_Entity):
270
302
  # safeguard against lookup failures ("sim" packets?)
271
303
  msg_list_qry.append(self._msgs[c])
272
304
  else:
273
- _LOGGER.debug("Could not fetch self._msgs[%s]", c)
305
+ _LOGGER.debug(
306
+ "_msg_list could not fetch self._msgs[%s] for %s (z_id %s)",
307
+ c,
308
+ self.id,
309
+ self._z_id,
310
+ )
274
311
  return msg_list_qry
275
312
  # else create from legacy nested dict
276
313
  return [m for c in self._msgz.values() for v in c.values() for m in v.values()]
@@ -315,32 +352,7 @@ class _MessageDB(_Entity):
315
352
  with contextlib.suppress(KeyError):
316
353
  del obj._msgz_[msg.code][msg.verb][msg._pkt._ctx]
317
354
 
318
- # EntityBase msg_db query methods > copy to docs/source/ramses_rf.rst
319
- # (ix = database.py.MessageIndex method)
320
- #
321
- # +----+----------------------+--------------------+------------+----------+----------+
322
- # | e. |method name | args | returns | uses | used by |
323
- # +====+======================+====================+============+==========+==========+
324
- # | e1 | _get_msg_by_hdr | hdr | Message | i3 | discover |
325
- # +----+----------------------+--------------------+------------+----------+----------+
326
- # | e2 | _msg_value | code(s), Msg, args | dict[k,v] | e3,e4 | |
327
- # +----+----------------------+--------------------+------------+----------+----------+
328
- # | e3 | _msg_value_code | code, verb, key | dict[k,v] | e4,e5,e6 | e6 |
329
- # +----+----------------------+--------------------+------------+----------+----------+
330
- # | e4 | _msg_value_msg | Msg, (code) | dict[k,v] | | e2,e3 |
331
- # +----+----------------------+--------------------+------------+----------+----------+
332
- # | e5 | _msg_qry_by_code_key | code, key, (verb=) | | | e6, |
333
- # +----+----------------------+--------------------+------------+----------+----------+
334
- # | e6 | _msg_value_qry_by_code_key | code, key | str/float | e3,e5 | |
335
- # +----+----------------------+--------------------+------------+----------+----------+
336
- # | e7 | _msg_qry | sql | | | e8 |
337
- # +----+----------------------+--------------------+------------+----------+----------+
338
- # | e8 | _msg_count | sql | | e7 | |
339
- # +----+----------------------+--------------------+------------+----------+----------+
340
- # | e9 | supported_cmds | | list(Codes)| i7 | |
341
- # +----+----------------------+--------------------+------------+----------+----------+
342
- # | e10| _msgs() | | | i5 | |
343
- # +----+----------------------+--------------------+------------+----------+----------+
355
+ ### entity_base query methods
344
356
 
345
357
  def _get_msg_by_hdr(self, hdr: HeaderT) -> Message | None:
346
358
  """Return a msg, if any, that matches a given header."""
@@ -399,7 +411,7 @@ class _MessageDB(_Entity):
399
411
 
400
412
  assert isinstance(code, Message), (
401
413
  f"Invalid format: _msg_value({code})"
402
- ) # catch invalidly formatted code, only handle Message
414
+ ) # catch invalidly formatted code, only handle Message from here
403
415
  return self._msg_value_msg(code, *args, **kwargs)
404
416
 
405
417
  def _msg_value_code(
@@ -410,7 +422,7 @@ class _MessageDB(_Entity):
410
422
  **kwargs: Any,
411
423
  ) -> dict | list | None:
412
424
  """
413
- Query the message database using the SQLite index for the most recent
425
+ Query the message dict or the SQLite index for the most recent
414
426
  key: value pairs(s) for a given code.
415
427
 
416
428
  :param code: filter messages by Code or a tuple of Codes, optional
@@ -442,9 +454,8 @@ class _MessageDB(_Entity):
442
454
 
443
455
  elif isinstance(code, tuple):
444
456
  msgs = [m for m in self._msgs.values() if m.code in code]
445
- msg = (
446
- max(msgs) if msgs else None
447
- ) # return highest = latest? value found in code:value pairs
457
+ msg = max(msgs) if msgs else None
458
+ # return highest = latest? value found in code:value pairs
448
459
  else:
449
460
  msg = self._msgs.get(code)
450
461
 
@@ -458,7 +469,7 @@ class _MessageDB(_Entity):
458
469
  domain_id: str | None = None,
459
470
  ) -> dict | list | None:
460
471
  """
461
- Get from a Message all or a specific key with its value,
472
+ Get from a Message all or a specific key with its value(s),
462
473
  optionally filtering for a zone or a domain
463
474
 
464
475
  :param msg: a Message to inspect
@@ -500,17 +511,17 @@ class _MessageDB(_Entity):
500
511
  or (idx == SZ_DOMAIN_ID)
501
512
  ), (
502
513
  f"full dict:{msg_dict}, payload:{msg.payload} < Coding error: key='{idx}', val='{val}'"
503
- ) # should not be there (BUG TODO(eb): but it is when using SQLite MessageIndex)
514
+ ) # should not be there (TODO(eb): BUG but occurs when using SQLite MessageIndex)
504
515
 
505
- if key == "*": # from a SQLite wildcard query, return first=only? k,v
506
- return msg_dict
507
- if key:
508
- return msg_dict.get(key)
509
- return {
510
- k: v
511
- for k, v in msg_dict.items()
512
- if k not in ("dhw_idx", SZ_DOMAIN_ID, SZ_ZONE_IDX) and k[:1] != "_"
513
- }
516
+ if (
517
+ key == "*" or not key
518
+ ): # from a SQLite wildcard query, return first=only? k,v
519
+ return {
520
+ k: v
521
+ for k, v in msg_dict.items()
522
+ if k not in ("dhw_idx", SZ_DOMAIN_ID, SZ_ZONE_IDX) and k[:1] != "_"
523
+ }
524
+ return msg_dict.get(key)
514
525
 
515
526
  # SQLite methods, since 0.52.0
516
527
 
@@ -518,7 +529,6 @@ class _MessageDB(_Entity):
518
529
  """
519
530
  Retrieve from the MessageIndex a list of Code keys involving this device.
520
531
 
521
- :param kwargs: not used as of 0.52.0
522
532
  :return: list of Codes or empty list when query returned empty
523
533
  """
524
534
  if self._gwy.msg_db:
@@ -530,7 +540,7 @@ class _MessageDB(_Entity):
530
540
  res: list[Code] = []
531
541
 
532
542
  for rec in self._gwy.msg_db.qry_field(
533
- sql, (self.id[:_ID_SLICE], self.id[:_ID_SLICE])
543
+ sql, (self.id[:_SQL_SLICE], self.id[:_SQL_SLICE])
534
544
  ):
535
545
  _LOGGER.debug("Fetched from index: %s", rec[0])
536
546
  # Example: "Fetched from index: code 1FD4"
@@ -550,7 +560,7 @@ class _MessageDB(_Entity):
550
560
  Retrieve from the MessageIndex the most current Code for a code(s) &
551
561
  keyword combination involving this device.
552
562
 
553
- :param code: (optional) a message Code to use, e.g. 31DA or a tuple of Codes
563
+ :param code: (optional) a message Code to use, e.g. Code._31DA or a tuple of Codes
554
564
  :param key: (optional) message keyword to fetch, e.g. SZ_HUMIDITY
555
565
  :param kwargs: optional verb='vb' single verb
556
566
  :return: Code of most recent query result message or None when query returned empty
@@ -581,9 +591,11 @@ class _MessageDB(_Entity):
581
591
  res = None
582
592
 
583
593
  for rec in self._gwy.msg_db.qry_field(
584
- sql, (vb, self.id[:_ID_SLICE], self.id[:_ID_SLICE], code_qry, key)
594
+ sql, (vb, self.id[:_SQL_SLICE], self.id[:_SQL_SLICE], code_qry, key)
585
595
  ):
586
- _LOGGER.debug("Fetched from index: %s", rec)
596
+ _LOGGER.debug(
597
+ "_msg_qry_by_code_key fetched rec: %s, code: %s", rec, code_qry
598
+ )
587
599
  assert isinstance(rec[0], dt) # mypy hint
588
600
  if rec[0] > latest: # dtm, only use most recent
589
601
  res = Code(rec[1])
@@ -605,7 +617,7 @@ class _MessageDB(_Entity):
605
617
 
606
618
  :param code: (optional) a single message Code to use, e.g. 31DA
607
619
  :param key: (optional) message keyword to fetch the value for, e.g. SZ_HUMIDITY or * (wildcard)
608
- :param kwargs: not used as of 0.52.0
620
+ :param kwargs: not used as of 0.52.1
609
621
  :return: a single string or float value or None when qry returned empty
610
622
  """
611
623
  val_msg: dict | list | None = None
@@ -642,7 +654,7 @@ class _MessageDB(_Entity):
642
654
  # """SELECT code from messages WHERE verb in (' I', 'RP') AND (src = ? OR dst = ?)
643
655
  # AND (code = '31DA' OR ...) AND (plk LIKE '%{SZ_FAN_INFO}%' OR ...)""" = 2 params
644
656
  for rec in self._gwy.msg_db.qry_field(
645
- sql, (self.id[:_ID_SLICE], self.id[:_ID_SLICE])
657
+ sql, (self.id[:_SQL_SLICE], self.id[:_SQL_SLICE])
646
658
  ):
647
659
  _pl = self._msgs[Code(rec[0])].payload
648
660
  # add payload dict to res(ults)
@@ -698,7 +710,7 @@ class _MessageDB(_Entity):
698
710
  _msg_dict = { # ? use ctx (context) instead of just the address?
699
711
  m.code: m
700
712
  for m in self._gwy.msg_db.qry(
701
- sql, (self.id[:_ID_SLICE], self.id[:_ID_SLICE])
713
+ sql, (self.id[:_SQL_SLICE], self.id[:_SQL_SLICE])
702
714
  ) # e.g. 01:123456_HW
703
715
  }
704
716
  # if CTL, remove 3150, 3220 heat_demand, both are only stored on children
@@ -774,12 +786,12 @@ class _Discovery(_MessageDB):
774
786
  code: CODES_SCHEMA[code][SZ_NAME]
775
787
  for code in sorted(
776
788
  self._gwy.msg_db.get_rp_codes(
777
- (self.id[:_ID_SLICE], self.id[:_ID_SLICE])
789
+ (self.id[:_SQL_SLICE], self.id[:_SQL_SLICE])
778
790
  )
779
791
  )
780
792
  if self._is_not_deprecated_cmd(code)
781
793
  }
782
- return { # TODO(eb): deprecated since 0.52.0, remove Q1 2026
794
+ return { # TODO(eb): deprecated since 0.52.1, remove Q1 2026
783
795
  code: (CODES_SCHEMA[code][SZ_NAME] if code in CODES_SCHEMA else None)
784
796
  for code in sorted(self._msgz)
785
797
  if self._msgz[code].get(RP) and self._is_not_deprecated_cmd(code)
@@ -806,7 +818,7 @@ class _Discovery(_MessageDB):
806
818
  AND (src = ? OR dst = ?)
807
819
  """
808
820
  for rec in self._gwy.msg_db.qry_field(
809
- sql, (self.id[:_ID_SLICE], self.id[:_ID_SLICE])
821
+ sql, (self.id[:_SQL_SLICE], self.id[:_SQL_SLICE])
810
822
  ):
811
823
  _LOGGER.debug("Fetched OT ctx from index: %s", rec[0])
812
824
  res.append(rec[0])
@@ -937,8 +949,8 @@ class _Discovery(_MessageDB):
937
949
  sql,
938
950
  (
939
951
  task[_SZ_COMMAND].code,
940
- self.tcs.id[:_ID_SLICE],
941
- self.tcs.id[:_ID_SLICE],
952
+ self.tcs.id[:_ID_SLICE], # OK? not _SQL_SLICE?
953
+ self.tcs.id[:_ID_SLICE], # OK? not _SQL_SLICE?
942
954
  ),
943
955
  )[0] # expect 1 Message in returned tuple
944
956
  else: # TODO(eb) remove next Q1 2026
ramses_rf/gateway.py CHANGED
@@ -175,6 +175,7 @@ class Gateway(Engine):
175
175
 
176
176
  # initialize SQLite index, set in _tx/Engine
177
177
  if self._sqlite_index: # TODO(eb): default to ON in Q4 2025
178
+ _LOGGER.info("Ramses RF starts SQLite MessageIndex")
178
179
  self.create_sqlite_message_index() # if activated in ramses_cc > Engine
179
180
 
180
181
  # temporarily turn on discovery, remember original state
@@ -212,7 +213,7 @@ class Gateway(Engine):
212
213
  def _pause(self, *args: Any) -> None:
213
214
  """Pause the (unpaused) gateway (disables sending/discovery).
214
215
 
215
- There is the option to save other objects, as *args.
216
+ There is the option to save other objects, as `args`.
216
217
  """
217
218
  _LOGGER.debug("Gateway: Pausing engine...")
218
219
 
@@ -227,7 +228,7 @@ class Gateway(Engine):
227
228
  def _resume(self) -> tuple[Any]:
228
229
  """Resume the (paused) gateway (enables sending/discovery, if applicable).
229
230
 
230
- Will restore other objects, as *args.
231
+ Will restore other objects, as `args`.
231
232
  """
232
233
  args: tuple[Any]
233
234
 
ramses_rf/schemas.py CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """RAMSES RF - a RAMSES-II protocol decoder & analyser.
3
3
 
4
- Schema processor for upper layer.
4
+ :term:`Schema` processor for upper layer.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
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.0"
3
+ __version__ = "0.52.2"
4
4
  VERSION = __version__
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ramses_rf
3
- Version: 0.52.0
3
+ Version: 0.52.2
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
@@ -4,52 +4,52 @@ ramses_cli/debug.py,sha256=vgR0lOHoYjWarN948dI617WZZGNuqHbeq6Tc16Da7b4,608
4
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
- ramses_rf/__init__.py,sha256=VG3E9GHbtC6lx6E1DMQJeFitHnydMKJyPxQBethdrzg,1193
7
+ ramses_rf/__init__.py,sha256=vp2TyFGqc1fGQHsevhmaw0QEmSSCnZx7fqizKiEwHtw,1245
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=21M1XV7qTQaydqInctf7vX-PAVdrM4xmJoaXX8oB87s,20748
11
- ramses_rf/dispatcher.py,sha256=UqilAoL6T85ZFPkcpvcfuidNbusONngQoCVWHV6vOzk,11277
12
- ramses_rf/entity_base.py,sha256=eLGhJTv8FDHjXmsywUhn1imEhdo0bFgAHP_KLNl_HiE,56004
10
+ ramses_rf/database.py,sha256=pfCOt6wvABAaW275mNLoRnObFSQ9nT5d7PtDLMTvgr4,20340
11
+ ramses_rf/dispatcher.py,sha256=YjEU-QrBLo9IfoEhJo2ikg_FxOaMYoWvzelr9Vi-JZ8,11398
12
+ ramses_rf/entity_base.py,sha256=OPdQEeJ5zk49FKCFmRui4urFQVR2tLBS1iWCTWeZkNo,55712
13
13
  ramses_rf/exceptions.py,sha256=mt_T7irqHSDKir6KLaf6oDglUIdrw0S40JbOrWJk5jc,3657
14
- ramses_rf/gateway.py,sha256=1MRmqpDlhE1SFm25dSvnSTX_dUoi9Pz--qycJIJyFAU,21151
14
+ ramses_rf/gateway.py,sha256=VYZqMppU_kDYhT3EUmqpHf0LuLXPMH7ASx9jylAopWE,21218
15
15
  ramses_rf/helpers.py,sha256=TNk_QkpIOB3alOp1sqnA9LOzi4fuDCeapNlW3zTzNas,4250
16
16
  ramses_rf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- ramses_rf/schemas.py,sha256=UhvRhV4nZ3kvrLLM3wwIguQUIjIgd_AKvp2wkTSpNEA,13468
18
- ramses_rf/version.py,sha256=ggzTkhC_7XHqI8ONDXEhJk9tXE1LLvMfYJyCrJzF8KI,125
17
+ ramses_rf/schemas.py,sha256=jvpx0hZiMhaogyxbxnrbxS7m2GekKK4DFfVTNs7h-WQ,13476
18
+ ramses_rf/version.py,sha256=3xbuWkwVUOezgu-nlN98fGrnAgqtfivOcMVKjCTSBds,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=UmZuA-5czrfoClVEeUAPWgze5obWNQpYI7ZPQpVJB6s,54704
22
- ramses_rf/device/hvac.py,sha256=of8wWHhJOX0KcvVqqlyJLArKrv1ST7rlbL7kLV9v_0Q,45603
21
+ ramses_rf/device/heat.py,sha256=hvTX32Evp23vqjXu-YHqTuneem4_QBRSxCkYR7dg4VI,54705
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
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
- ramses_tx/__init__.py,sha256=qNMTe8hBkIuecvtCiekUB0pKdD8atb0SjWxVNVe3yhE,3538
28
+ ramses_tx/__init__.py,sha256=sqnjM7pUGJDmec6igTtKViSB8FLX49B5gwhAmcY9ERY,3596
29
29
  ramses_tx/address.py,sha256=F5ZE-EbPNNom1fW9XXUILvD7DYSMBxNJvsHVliT5gjw,8452
30
- ramses_tx/command.py,sha256=r9dNaofjjOQXZSUrZjsNpvEukNn4rSGy0OLr2Dyd2TI,125129
30
+ ramses_tx/command.py,sha256=tqVECwd_QokEcRv2MSk7TUU4JSBzCZcJh1eQ0jIGgoY,125122
31
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
34
  ramses_tx/frame.py,sha256=GzNsXr15YLeidJYGtk_xPqsZQh4ehDDlUCtT6rTDhT8,22046
35
- ramses_tx/gateway.py,sha256=ztPg3fMgn-fM15dLxrw5PG34SCxD7eR3pdz1xwvz7Ag,11345
35
+ ramses_tx/gateway.py,sha256=8o9NyEAyIui3B6HA0R8o64u4U8NZFlNl0TQ6rhwMoCk,11369
36
36
  ramses_tx/helpers.py,sha256=J4OCRckp3JshGQTvvqEskFjB1hPS7uA_opVsuIqmZds,32915
37
37
  ramses_tx/logger.py,sha256=qYbUoNPnPaFWKVsYvLG6uTVuPTdZ8HsMzBbGx0DpBqc,10177
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=e6IwVEMLv2EL8gbZaM2s-qA3E2AN8dCwdIgfEbFo730,111029
41
+ ramses_tx/parsers.py,sha256=Z0ochrNyO2l8SAP0oJxN-tx2nJluEaNP_uJCIm_lsA8,111279
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
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
46
+ ramses_tx/schemas.py,sha256=U8SkwbW41zac0MQe0NgP6qTUqymmbeCG-UHPEw_GAv0,13267
47
+ ramses_tx/transport.py,sha256=kyvnVf2RpuA_8YRO6C4S60YHctDag0ywfoMzb0wWDGY,58552
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=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,,
50
+ ramses_tx/version.py,sha256=NEkkAJQW7Hx8GmvkR9BZpLSOi30OMi9Awth13sWXuTQ,123
51
+ ramses_rf-0.52.2.dist-info/METADATA,sha256=Fm3vwa0FvFkMEHz44B8FDq0PjboTn_g9sGHyK7HiydA,4000
52
+ ramses_rf-0.52.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
+ ramses_rf-0.52.2.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
54
+ ramses_rf-0.52.2.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
55
+ ramses_rf-0.52.2.dist-info/RECORD,,
ramses_tx/__init__.py CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env python3
2
- """RAMSES RF - a RAMSES-II protocol decoder & analyser."""
2
+ """RAMSES RF - a RAMSES-II protocol decoder & analyser.
3
+ `ramses_tx` takes care of the RF protocol (lower) layer.
4
+ """
3
5
 
4
6
  from __future__ import annotations
5
7
 
ramses_tx/command.py CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env python3
2
- """RAMSES RF - a RAMSES-II protocol decoder & analyser.
2
+ """
3
+ RAMSES RF - a RAMSES-II protocol decoder & analyser.
3
4
 
4
5
  This module provides the `Command` class for constructing and managing RAMSES-II protocol
5
6
  commands (packets) that are to be sent to HVAC devices. It includes methods for creating
@@ -1394,7 +1395,7 @@ class Command(Frame):
1394
1395
 
1395
1396
  :param ctl_id: The device ID of the DHW controller
1396
1397
  :type ctl_id: DeviceIdT | str
1397
- :param **kwargs: Additional parameters (currently only 'dhw_idx' is supported)
1398
+ :param kwargs: Additional parameters (currently only 'dhw_idx' is supported)
1398
1399
  :key dhw_idx: The DHW circuit index (0 or 1, defaults to 0 for single-DHW systems)
1399
1400
  :type dhw_idx: int, optional
1400
1401
  :return: A Command object for the RQ|1F41 message
@@ -1438,7 +1439,7 @@ class Command(Frame):
1438
1439
  :type until: datetime | str | None
1439
1440
  :param duration: Duration in seconds for temporary mode (alternative to 'until')
1440
1441
  :type duration: int | None
1441
- :param **kwargs: Additional parameters (currently only 'dhw_idx' is supported)
1442
+ :param kwargs: Additional parameters (currently only 'dhw_idx' is supported)
1442
1443
  :key dhw_idx: The DHW circuit index (0 or 1, defaults to 0 for single-DHW systems)
1443
1444
  :type dhw_idx: int, optional
1444
1445
  :return: A Command object for the W|1F41 message
@@ -1501,7 +1502,7 @@ class Command(Frame):
1501
1502
  :type codes: Code | Iterable[Code] | None
1502
1503
  :param dst_id: Optional destination device ID (defaults to broadcast)
1503
1504
  :type dst_id: DeviceIdT | str | None
1504
- :param **kwargs: Additional parameters
1505
+ :param kwargs: Additional parameters
1505
1506
  :key oem_code: OEM code for bind offers (only used with I-type messages)
1506
1507
  :type oem_code: str, optional
1507
1508
  :return: A Command object for the bind operation
@@ -2531,7 +2532,7 @@ class Command(Frame):
2531
2532
  :type supply_flow: float | None
2532
2533
  :param exhaust_flow: Current exhaust air flow rate (if available)
2533
2534
  :type exhaust_flow: float | None
2534
- :param **kwargs: Additional parameters (reserved for future use)
2535
+ :param kwargs: Additional parameters (reserved for future use)
2535
2536
  :return: A configured Command object for the HVAC fan status update
2536
2537
  :rtype: Command
2537
2538
 
ramses_tx/gateway.py CHANGED
@@ -113,7 +113,7 @@ class Engine:
113
113
  self._include,
114
114
  self._exclude,
115
115
  )
116
- self._sqlite_index = kwargs.pop(SZ_SQLITE_INDEX)
116
+ self._sqlite_index = kwargs.pop(SZ_SQLITE_INDEX, False) # default True?
117
117
  self._kwargs: dict[str, Any] = kwargs # HACK
118
118
 
119
119
  self._engine_lock = Lock() # FIXME: threading lock, or asyncio lock?
ramses_tx/parsers.py CHANGED
@@ -3,9 +3,11 @@
3
3
 
4
4
  NOTES: aspirations on a consistent Schema, going forward:
5
5
 
6
- :mode/state: | :bool: | :mutex (infinitive. vs -ing): | :flags:
7
- mode (config.) | enabled | disabled, heat, cool, heat_cool... | ch_enabled, dhw_enabled
8
- state (action) | active | idle, heating, cooling... | is_heating, is_cooling
6
+ ============== ======== =================================== ========================
7
+ :mode/state: :bool: :mutex (infinitive. vs -ing): :flags:
8
+ mode (config.) enabled disabled, heat, cool, heat_cool... ch_enabled, dhw_enabled
9
+ state (action) active idle, heating, cooling... is_heating, is_cooling
10
+ ============== ======== =================================== ========================
9
11
 
10
12
  - prefer: enabled: True over xx_enabled: True (if only ever 1 flag)
11
13
  - prefer: active: True over is_heating: True (if only ever 1 flag)
@@ -362,8 +364,9 @@ def parser_0009(payload: str, msg: Message) -> dict | list[dict]: # TODO: only
362
364
 
363
365
  The failsafe mode defines the relay behaviour if the RF communication is lost (e.g.
364
366
  when a room thermostat stops communicating due to discharged batteries):
365
- False (disabled) - if RF comms are lost, relay will be held in OFF position
366
- True (enabled) - if RF comms are lost, relay will cycle at 20% ON, 80% OFF
367
+
368
+ - False (disabled) - if RF comms are lost, relay will be held in OFF position
369
+ - True (enabled) - if RF comms are lost, relay will cycle at 20% ON, 80% OFF
367
370
 
368
371
  This setting may need to be enabled to ensure frost protect mode.
369
372
  """
@@ -2289,45 +2292,46 @@ def parser_31da(payload: str, msg: Message) -> PayDictT._31DA:
2289
2292
  def parser_31e0(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
2290
2293
  """Notes are.
2291
2294
 
2292
- van means of”.
2295
+ "van" means "of".
2293
2296
  - 0 = min. van min. potm would be:
2294
2297
  - 0 = minimum of minimum potentiometer
2295
2298
 
2296
2299
  See: https://www.industrialcontrolsonline.com/honeywell-t991a
2297
2300
  - modulates air temperatures in ducts
2298
-
2299
- case 0x31E0: ' 12768:
2300
- {
2301
- string str4;
2302
- unchecked
2303
- {
2304
- result.Fan = Conversions.ToString((double)(int)data[checked(start + 1)] / 2.0);
2305
- str4 = "";
2306
- }
2307
- str4 = (data[start + 2] & 0xF) switch
2308
- {
2309
- 0 => str4 + "0 = min. potm. ",
2310
- 1 => str4 + "0 = min. van min. potm ",
2311
- 2 => str4 + "0 = min. fan ",
2312
- _ => "",
2313
- };
2314
- switch (data[start + 2] & 0xF0)
2315
- {
2316
- case 16:
2317
- str4 += "100 = max. potm";
2318
- break;
2319
- case 32:
2320
- str4 += "100 = max. van max. potm ";
2321
- break;
2322
- case 48:
2323
- str4 += "100 = max. fan ";
2324
- break;
2325
- }
2326
- result.Data = str4;
2327
- break;
2328
- }
2329
2301
  """
2330
2302
 
2303
+ # coding note:
2304
+ # case 0x31E0: ' 12768:
2305
+ # {
2306
+ # string str4;
2307
+ # unchecked
2308
+ # {
2309
+ # result.Fan = Conversions.ToString((double)(int)data[checked(start + 1)] / 2.0);
2310
+ # str4 = "";
2311
+ # }
2312
+ # str4 = (data[start + 2] & 0xF) switch
2313
+ # {
2314
+ # 0 => str4 + "0 = min. potm. ",
2315
+ # 1 => str4 + "0 = min. van min. potm ",
2316
+ # 2 => str4 + "0 = min. fan ",
2317
+ # _ => "",
2318
+ # };
2319
+ # switch (data[start + 2] & 0xF0)
2320
+ # {
2321
+ # case 16:
2322
+ # str4 += "100 = max. potm";
2323
+ # break;
2324
+ # case 32:
2325
+ # str4 += "100 = max. van max. potm ";
2326
+ # break;
2327
+ # case 48:
2328
+ # str4 += "100 = max. fan ";
2329
+ # break;
2330
+ # }
2331
+ # result.Data = str4;
2332
+ # break;
2333
+ # }
2334
+
2331
2335
  # .I --- 37:005302 32:132403 --:------ 31E0 008 00-0000-00 01-0064-00 # RF15 CO2 to Orcon HRC400 series SmartComfort Valve
2332
2336
 
2333
2337
  # .I --- 29:146052 32:023459 --:------ 31E0 003 00-0000
ramses_tx/schemas.py CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """RAMSES RF - a RAMSES-II protocol decoder & analyser.
3
3
 
4
- Schema processor for protocol (lower) layer.
4
+ :term:`Schema` processor for protocol (lower) layer.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -73,13 +73,17 @@ class PktLogConfigT(TypedDict):
73
73
  def sch_packet_log_dict_factory(
74
74
  default_backups: int = 0,
75
75
  ) -> dict[vol.Required, vol.Any]:
76
- """Return a packet log dict with a configurable default rotation policy.
76
+ """
77
+ :return: a packet log dict with a configurable default rotation policy.
77
78
 
78
- usage:
79
+ Usage:
80
+
81
+ .. code-block::
82
+
83
+ SCH_PACKET_LOG_7 = vol.Schema(
84
+ packet_log_dict_factory(default_backups=7), extra=vol.PREVENT_EXTRA
85
+ )
79
86
 
80
- SCH_PACKET_LOG_7 = vol.Schema(
81
- packet_log_dict_factory(default_backups=7), extra=vol.PREVENT_EXTRA
82
- )
83
87
  """
84
88
 
85
89
  SCH_PACKET_LOG_CONFIG = vol.Schema(
@@ -162,11 +166,13 @@ class PortConfigT(TypedDict):
162
166
  def sch_serial_port_dict_factory() -> dict[vol.Required, vol.Any]:
163
167
  """Return a serial port dict.
164
168
 
165
- usage:
169
+ Usage:
166
170
 
167
- SCH_SERIAL_PORT = vol.Schema(
168
- sch_serial_port_dict_factory(), extra=vol.PREVENT_EXTRA
169
- )
171
+ .. code-block::
172
+
173
+ SCH_SERIAL_PORT = vol.Schema(
174
+ sch_serial_port_dict_factory(), extra=vol.PREVENT_EXTRA
175
+ )
170
176
  """
171
177
 
172
178
  SCH_SERIAL_PORT_NAME = str
@@ -252,11 +258,13 @@ def sch_global_traits_dict_factory(
252
258
  ) -> tuple[dict[vol.Optional, vol.Any], vol.Any]:
253
259
  """Return a global traits dict with a configurable extra traits.
254
260
 
255
- usage:
261
+ Usage:
256
262
 
257
- SCH_GLOBAL_TRAITS = vol.Schema(
258
- sch_global_traits_dict(heat=traits), extra=vol.PREVENT_EXTRA
259
- )
263
+ .. code-block::
264
+
265
+ SCH_GLOBAL_TRAITS = vol.Schema(
266
+ sch_global_traits_dict(heat=traits), extra=vol.PREVENT_EXTRA
267
+ )
260
268
  """
261
269
 
262
270
  heat_traits = heat_traits or {}
@@ -348,7 +356,8 @@ def select_device_filter_mode(
348
356
  known_list: DeviceListT,
349
357
  block_list: DeviceListT,
350
358
  ) -> bool:
351
- """Determine which device filter to use, if any.
359
+ """
360
+ Determine which device filter to use, if any.
352
361
 
353
362
  Either:
354
363
  - block if device_id in block_list (could be empty), otherwise
ramses_tx/transport.py CHANGED
@@ -3,30 +3,38 @@
3
3
 
4
4
  Operates at the pkt layer of: app - msg - pkt - h/w
5
5
 
6
- For ser2net, use the following YAML with: ser2net -c misc/ser2net.yaml
7
- connection: &con00
8
- accepter: telnet(rfc2217),tcp,5001
9
- timeout: 0
10
- connector: serialdev,/dev/ttyUSB0,115200n81,local
11
- options:
12
- max-connections: 3
13
-
14
- For socat, see:
15
- socat -dd pty,raw,echo=0 pty,raw,echo=0
16
- python client.py monitor /dev/pts/0
17
- cat packet.log | cut -d ' ' -f 2- | unix2dos > /dev/pts/1
6
+ For ser2net, use the following YAML with: ``ser2net -c misc/ser2net.yaml``
7
+
8
+ .. code-block::
9
+
10
+ connection: &con00
11
+ accepter: telnet(rfc2217),tcp,5001
12
+ timeout: 0
13
+ connector: serialdev,/dev/ttyUSB0,115200n81,local
14
+ options:
15
+ max-connections: 3
16
+
17
+ For ``socat``, see:
18
+
19
+ .. code-block::
20
+
21
+ socat -dd pty,raw,echo=0 pty,raw,echo=0
22
+ python client.py monitor /dev/pts/0
23
+ cat packet.log | cut -d ' ' -f 2- | unix2dos > /dev/pts/1
18
24
 
19
25
  For re-flashing evofw3 via Arduino IDE on *my* atmega328p (YMMV):
20
- - Board: atmega328p (SW UART)
21
- - Bootloader: Old Bootloader
22
- - Processor: atmega328p (5V, 16 MHz)
23
- - Host: 57600 (or 115200, YMMV)
24
- - Pinout: Nano
26
+
27
+ - Board: atmega328p (SW UART)
28
+ - Bootloader: Old Bootloader
29
+ - Processor: atmega328p (5V, 16 MHz)
30
+ - Host: 57600 (or 115200, YMMV)
31
+ - Pinout: Nano
25
32
 
26
33
  For re-flashing evofw3 via Arduino IDE on *my* atmega32u4 (YMMV):
27
- - Board: atmega32u4 (HW UART)
28
- - Processor: atmega32u4 (5V, 16 MHz)
29
- - Pinout: Pro Micro
34
+
35
+ - Board: atmega32u4 (HW UART)
36
+ - Processor: atmega32u4 (5V, 16 MHz)
37
+ - Pinout: Pro Micro
30
38
  """
31
39
 
32
40
  from __future__ import annotations
@@ -250,11 +258,12 @@ async def is_hgi80(serial_port: SerPortNameT) -> bool | None:
250
258
 
251
259
 
252
260
  def _normalise(pkt_line: str) -> str:
253
- """Perform any (transparent) frame-level hacks, as required at (near-)RF layer.
261
+ """
262
+ Perform any (transparent) frame-level hacks, as required at (near-)RF layer.
254
263
 
255
264
  Goals:
256
265
  - ensure an evofw3 provides the same output as a HGI80 (none, presently)
257
- - handle 'strange' packets (e.g. I|08:|0008)
266
+ - handle 'strange' packets (e.g. ``I|08:|0008``)
258
267
  """
259
268
 
260
269
  # TODO: deprecate as only for ramses_esp <0.4.0
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.0"
3
+ __version__ = "0.52.2"
4
4
  VERSION = __version__