ramses-rf 0.52.3__py3-none-any.whl → 0.52.5__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/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/system/zones.py CHANGED
@@ -36,7 +36,7 @@ from ramses_rf.device import (
36
36
  TrvActuator,
37
37
  UfhController,
38
38
  )
39
- from ramses_rf.entity_base import Child, Entity, Parent, class_by_attr
39
+ from ramses_rf.entity_base import _ID_SLICE, Child, Entity, Parent, class_by_attr
40
40
  from ramses_rf.helpers import shrink
41
41
  from ramses_rf.schemas import (
42
42
  SCH_TCS_DHW,
@@ -728,13 +728,33 @@ class Zone(ZoneSchedule):
728
728
  """Set the target temperature, until the next scheduled setpoint."""
729
729
 
730
730
  if value is None:
731
- return self.reset_mode()
731
+ self.reset_mode()
732
732
 
733
733
  cmd = Command.set_zone_setpoint(self.ctl.id, self.idx, value)
734
734
  self._gwy.send_cmd(cmd, priority=Priority.HIGH)
735
735
 
736
736
  @property
737
737
  def temperature(self) -> float | None: # 30C9
738
+ if self._gwy.msg_db:
739
+ # evohome zones only get initial temp from src + idx, so use zone sensor if newer
740
+ sql = f"""
741
+ SELECT dtm from messages WHERE verb in (' I', 'RP')
742
+ AND code = '30C9'
743
+ AND (plk LIKE '%{SZ_TEMPERATURE}%')
744
+ AND ((src = ? AND ctx = ?) OR src = ?)
745
+ """
746
+ sensor_id = "aa:aaaaaa" # should not match any device_id
747
+ if self._sensor:
748
+ sensor_id = self._sensor.id
749
+ # custom SQLite query on MessageIndex
750
+ msgs = self._gwy.msg_db.qry(
751
+ sql, (self.id[:_ID_SLICE], self.idx, sensor_id[:_ID_SLICE])
752
+ )
753
+ if msgs and len(msgs) > 0:
754
+ msgs_sorted = sorted(msgs, reverse=True)
755
+ return msgs_sorted[0].payload.get(SZ_TEMPERATURE) # type: ignore[no-any-return]
756
+ return None
757
+ # else: TODO Q1 2026 remove remainder
738
758
  return self._msg_value(Code._30C9, key=SZ_TEMPERATURE) # type: ignore[no-any-return]
739
759
 
740
760
  @property
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.5"
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.5
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
 
@@ -0,0 +1,55 @@
1
+ ramses_cli/__init__.py,sha256=uvGzWqOf4avvgzxJNSLFWEelIWqSZ-AeLAZzg5x58bc,397
2
+ ramses_cli/client.py,sha256=h6liNKFXGsR2q9Unn81ushK53zqKgmkT0oAWNB5Jn0I,20369
3
+ ramses_cli/debug.py,sha256=PLcz-3PjUiMVqtD_p6VqTA92eHUM58lOBFXh_qgQ_wA,576
4
+ ramses_cli/discovery.py,sha256=MWoahBnAAVzfK2S7EDLsY2WYqN_ZK9L-lktrj8_4cb0,12978
5
+ ramses_cli/utils/cat_slow.py,sha256=AhUpM5gnegCitNKU-JGHn-DrRzSi-49ZR1Qw6lxe_t8,607
6
+ ramses_cli/utils/convert.py,sha256=N3LxGe3_0pclijtmYW-ChqCuPTzbkoJA4XNAnoSnBk0,1806
7
+ ramses_rf/__init__.py,sha256=vp2TyFGqc1fGQHsevhmaw0QEmSSCnZx7fqizKiEwHtw,1245
8
+ ramses_rf/binding_fsm.py,sha256=fuqvcc9YW-wr8SPH8zadpPqrHAvzl_eeWF-IBtlLppY,26632
9
+ ramses_rf/const.py,sha256=L3z31CZ-xqno6oZp_h-67CB_5tDDqTwSWXsqRtsjMcs,5460
10
+ ramses_rf/database.py,sha256=9nY1Wt2hjkBoDvDqopzPP3mPSH5-Q5XHPQuJnM2GHOw,21362
11
+ ramses_rf/dispatcher.py,sha256=YjEU-QrBLo9IfoEhJo2ikg_FxOaMYoWvzelr9Vi-JZ8,11398
12
+ ramses_rf/entity_base.py,sha256=Psa75zsZf6GOvXIv4c37vJDbg5iVQ1gQCADkBNOz1m8,58912
13
+ ramses_rf/exceptions.py,sha256=mt_T7irqHSDKir6KLaf6oDglUIdrw0S40JbOrWJk5jc,3657
14
+ ramses_rf/gateway.py,sha256=B7BlrNHx26lIHzjgy6-4NMtohb_GATaBuucgsUUxRj4,22005
15
+ ramses_rf/helpers.py,sha256=TNk_QkpIOB3alOp1sqnA9LOzi4fuDCeapNlW3zTzNas,4250
16
+ ramses_rf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ ramses_rf/schemas.py,sha256=0_qQZSGj2ixq4UCtMxoAeWklOJiM4bbBUzMsPw8gUPw,13475
18
+ ramses_rf/version.py,sha256=wV7QvKiXDL06V_DHo0XYcc-6CIYMlhztVGzCh1m45D8,125
19
+ ramses_rf/device/__init__.py,sha256=sUbH5dhbYFXSoM_TPFRutpRutBRpup7_cQ9smPtDTy8,4858
20
+ ramses_rf/device/base.py,sha256=Tu5I8Lj7KplfRsIBQAYjilS6YPgTyjpU8qgKugMR2Jk,18281
21
+ ramses_rf/device/heat.py,sha256=T-ENFzH1AEOd0m4-TgV-Qk0TnLNir_pYmyKlJtxy6NI,54538
22
+ ramses_rf/device/hvac.py,sha256=vdgiPiLtCAGr7CVsGhQl6XuAFkyYdQSE_2AEdCmRl2I,48502
23
+ ramses_rf/system/__init__.py,sha256=uZLKio3gLlBzePa2aDQ1nxkcp1YXOGrn6iHTG8LiNIw,711
24
+ ramses_rf/system/faultlog.py,sha256=GdGmVGT3137KsTlV_nhccgIFEmYu6DFsLTn4S-8JSok,12799
25
+ ramses_rf/system/heat.py,sha256=qQmzgmyHy2x87gHAstn0ee7ZVVOq-GJIfDxCrC-6gFU,39254
26
+ ramses_rf/system/schedule.py,sha256=Ts6tdZPTQLV5NkgwA73tPa5QUsnZNIIuYoKC-8VsXDk,18808
27
+ ramses_rf/system/zones.py,sha256=qMv7CuvZUKBNLpPRXgFA70NFRStgVJcw062h5mc3Y8Q,37034
28
+ ramses_tx/__init__.py,sha256=sqnjM7pUGJDmec6igTtKViSB8FLX49B5gwhAmcY9ERY,3596
29
+ ramses_tx/address.py,sha256=IuwUwZxykn3fP1UCRcv4D-zbTICBe2FJjDAFX5X6VoI,9108
30
+ ramses_tx/command.py,sha256=NeqWbdaWrXjP5KEZgXFicy_1UYOoUznRHN2ZfhVyDH0,125652
31
+ ramses_tx/const.py,sha256=AMwHitDq115rB24f3fzclNGC4ArMW16DbqiFWQc0U5o,30306
32
+ ramses_tx/exceptions.py,sha256=FJSU9YkvpKjs3yeTqUJX1o3TPFSe_B01gRGIh9b3PNc,2632
33
+ ramses_tx/fingerprints.py,sha256=nfftA1E62HQnb-eLt2EqjEi_la0DAoT0wt-PtTMie0s,11974
34
+ ramses_tx/frame.py,sha256=GzNsXr15YLeidJYGtk_xPqsZQh4ehDDlUCtT6rTDhT8,22046
35
+ ramses_tx/gateway.py,sha256=Fl6EqAUU2DnLOiA2_87sS7VPBEyxA1a_ICCmg55WMMA,11494
36
+ ramses_tx/helpers.py,sha256=VIPSw0lrDrE3S92YchzeRo7G2AlQ_vE8OuYUF-rlpQw,33616
37
+ ramses_tx/logger.py,sha256=1iKRHKUaqHqGd76CkE_6mCVR0sYODtxshRRwfY61fTk,10426
38
+ ramses_tx/message.py,sha256=HUSNiBgGlnWEBeBqb4K28GcU5oB7RbS9a0JC2p9SGe4,13676
39
+ ramses_tx/opentherm.py,sha256=58PXz9l5x8Ou6Fm3y-R_UnGHCYahoi2RKIDdYStUMzk,42378
40
+ ramses_tx/packet.py,sha256=_nzuInS_WhdOI26SYvgsdDqIaDvVNguc2YDwdPOVCbU,7661
41
+ ramses_tx/parsers.py,sha256=qQz8VOKaH26MUvo9AfUwhrnbH7VwXLxFc2gKJYvjXvY,148571
42
+ ramses_tx/protocol.py,sha256=ApA__ubfxM63c1OAxRcH6fhjMQTdC07SnSH9cGfWO2U,32427
43
+ ramses_tx/protocol_fsm.py,sha256=ZKtehCr_4TaDdfdlfidFLJaOVTYtaEq5h4tLqNIhb9s,26827
44
+ ramses_tx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
+ ramses_tx/ramses.py,sha256=vp748Tf_a-56OMM8CWDA2ZktRfTuj0QVyPRcnsOstSM,53983
46
+ ramses_tx/schemas.py,sha256=bqKW_V0bR6VbBD8ZQiBExNtVdXs0fryVKe3GEhupgIo,13424
47
+ ramses_tx/transport.py,sha256=-Ikwj_Hy6JEnQPS9gBalV3H5AKQd3ATxKskR7Dy7_sE,59774
48
+ ramses_tx/typed_dicts.py,sha256=w-0V5t2Q3GiNUOrRAWiW9GtSwbta_7luME6GfIb1zhI,10869
49
+ ramses_tx/typing.py,sha256=eF2SlPWhNhEFQj6WX2AhTXiyRQVXYnFutiepllYl2rI,5042
50
+ ramses_tx/version.py,sha256=j7HXmU97PXKJClRv1saySQ_JZYHAxBznxUTdWH64uHE,123
51
+ ramses_rf-0.52.5.dist-info/METADATA,sha256=gGSPoi37fljrfFxDIVHXFUU5RuzGu6wwwD0JbvdwiVQ,4179
52
+ ramses_rf-0.52.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
53
+ ramses_rf-0.52.5.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
54
+ ramses_rf-0.52.5.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
55
+ ramses_rf-0.52.5.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
ramses_tx/address.py CHANGED
@@ -39,7 +39,12 @@ class Address:
39
39
  _SLUG = None
40
40
 
41
41
  def __init__(self, device_id: DeviceIdT) -> None:
42
- """Create an address from a valid device id."""
42
+ """Create an address from a valid device ID.
43
+
44
+ :param device_id: The RAMSES II device ID (e.g., '01:123456')
45
+ :type device_id: DeviceIdT
46
+ :raises ValueError: If the device_id is not a valid format.
47
+ """
43
48
 
44
49
  # if device_id is None:
45
50
  # device_id = NON_DEVICE_ID
@@ -91,7 +96,15 @@ class Address:
91
96
 
92
97
  @classmethod
93
98
  def convert_from_hex(cls, device_hex: str, friendly_id: bool = False) -> str:
94
- """Convert (say) '06368E' to '01:145038' (or 'CTL:145038')."""
99
+ """Convert a 6-character hex string to a device ID.
100
+
101
+ :param device_hex: The hex string to convert (e.g., '06368E')
102
+ :type device_hex: str
103
+ :param friendly_id: If True, returns a named ID (e.g., 'CTL:145038'), defaults to False
104
+ :type friendly_id: bool
105
+ :return: The formatted device ID string
106
+ :rtype: str
107
+ """
95
108
 
96
109
  if device_hex == "FFFFFE": # aka '63:262142'
97
110
  return ">null dev<" if friendly_id else ALL_DEVICE_ID
@@ -191,11 +204,13 @@ def is_valid_dev_id(value: str, dev_class: None | str = None) -> bool:
191
204
 
192
205
  @lru_cache(maxsize=256) # there is definite benefit in caching this
193
206
  def pkt_addrs(addr_fragment: str) -> tuple[Address, Address, Address, Address, Address]:
194
- """Return the address fields from (e.g): '01:078710 --:------ 01:144246'.
195
-
196
- returns: src_addr, dst_addr, addr_0, addr_1, addr_2
207
+ """Parse address fields from a 30-character address fragment.
197
208
 
198
- Will raise an InvalidAddrSetError if the address fields are not valid.
209
+ :param addr_fragment: The 30-char fragment (e.g., '01:078710 --:------ 01:144246')
210
+ :type addr_fragment: str
211
+ :return: A tuple of (src_addr, dst_addr, addr_0, addr_1, addr_2)
212
+ :rtype: tuple[Address, Address, Address, Address, Address]
213
+ :raises PacketAddrSetInvalid: If the address fields are not valid.
199
214
  """
200
215
  # for debug: print(pkt_addrs.cache_info())
201
216
 
ramses_tx/command.py CHANGED
@@ -2254,7 +2254,13 @@ class Command(Frame):
2254
2254
 
2255
2255
  @classmethod # constructor for RQ|2E04
2256
2256
  def get_system_mode(cls, ctl_id: DeviceIdT | str) -> Command:
2257
- """Constructor to get the mode of a system (c.f. parser_2e04)."""
2257
+ """Get the mode of a system (c.f. parser_2e04).
2258
+
2259
+ :param ctl_id: The device ID of the controller
2260
+ :type ctl_id: DeviceIdT | str
2261
+ :return: A Command object for the RQ|2E04 message
2262
+ :rtype: Command
2263
+ """
2258
2264
 
2259
2265
  return cls.from_attrs(RQ, ctl_id, Code._2E04, FF)
2260
2266
 
@@ -2453,7 +2459,17 @@ class Command(Frame):
2453
2459
  datetime: dt | str,
2454
2460
  is_dst: bool = False,
2455
2461
  ) -> Command:
2456
- """Constructor to set the datetime of a system (c.f. parser_313f)."""
2462
+ """Set the datetime of a system (c.f. parser_313f).
2463
+
2464
+ :param ctl_id: The device ID of the controller
2465
+ :type ctl_id: DeviceIdT | str
2466
+ :param datetime: The target date and time
2467
+ :type datetime: dt | str
2468
+ :param is_dst: Whether Daylight Saving Time is active, defaults to False
2469
+ :type is_dst: bool
2470
+ :return: A Command object for the W|313F message
2471
+ :rtype: Command
2472
+ """
2457
2473
  # .W --- 30:185469 01:037519 --:------ 313F 009 0060003A0C1B0107E5
2458
2474
 
2459
2475
  dt_str = hex_from_dtm(datetime, is_dst=is_dst, incl_seconds=True)
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/helpers.py CHANGED
@@ -131,28 +131,36 @@ file_time = _FILE_TIME()
131
131
  def timestamp() -> float:
132
132
  """Return the number of seconds since the Unix epoch.
133
133
 
134
- Return an accurate value, even for Windows-based systems.
134
+ This function attempts to return a high-precision value, using specific
135
+ system calls on Windows if available.
136
+ :return: The current timestamp in seconds.
137
+ :rtype: float
135
138
  """
136
139
 
137
140
  # see: https://www.python.org/dev/peps/pep-0564/
138
- if sys.platform != "win32": # since 1970-01-01T00:00:00Z, time.gmtime(0)
141
+ if sys.platform == "win32":
142
+ # Windows uses a different epoch (1601-01-01)
143
+ ctypes.windll.kernel32.GetSystemTimePreciseAsFileTime(ctypes.byref(file_time))
144
+ _time = (file_time.dwLowDateTime + (file_time.dwHighDateTime << 32)) / 1e7
145
+ return float(_time - 134774 * 24 * 60 * 60)
146
+ else:
147
+ # Linux/macOS uses the Unix epoch (1970-01-01)
139
148
  return time.time_ns() / 1e9
140
149
 
141
- # otherwise, is since 1601-01-01T00:00:00Z
142
- ctypes.windll.kernel32.GetSystemTimePreciseAsFileTime(ctypes.byref(file_time)) # type: ignore[unreachable]
143
- _time = (file_time.dwLowDateTime + (file_time.dwHighDateTime << 32)) / 1e7
144
- return _time - 134774 * 24 * 60 * 60
145
-
146
150
 
147
151
  def dt_now() -> dt:
148
152
  """Return the current datetime as a local/naive datetime object.
149
153
 
150
154
  This is slower, but potentially more accurate, than dt.now(), and is used mainly for
151
155
  packet timestamps.
156
+
157
+ :return: The current local datetime.
158
+ :rtype: dt
152
159
  """
153
160
  if sys.platform == "win32":
154
161
  return dt.fromtimestamp(timestamp())
155
- return dt.now()
162
+ else:
163
+ return dt.now()
156
164
 
157
165
 
158
166
  def dt_str() -> str:
@@ -369,7 +377,14 @@ def hex_from_str(value: str) -> str:
369
377
 
370
378
 
371
379
  def hex_to_temp(value: HexStr4) -> bool | float | None: # TODO: remove bool
372
- """Convert a 2's complement 4-byte hex string to a float."""
380
+ """Convert a 4-byte 2's complement hex string to a float temperature ('C).
381
+
382
+ :param value: The 4-character hex string (e.g., '07D0')
383
+ :type value: HexStr4
384
+ :return: The temperature in Celsius, or None if N/A
385
+ :rtype: float | None
386
+ :raises ValueError: If input is not a 4-char hex string or temperature is invalid.
387
+ """
373
388
  if not isinstance(value, str) or len(value) != 4:
374
389
  raise ValueError(f"Invalid value: {value}, is not a 4-char hex string")
375
390
  if value == "31FF": # means: N/A (== 127.99, 2s complement), signed?
@@ -488,13 +503,18 @@ AIR_QUALITY_BASIS: dict[str, str] = {
488
503
 
489
504
  # 31DA[2:6] and 12C8[2:6]
490
505
  def parse_air_quality(value: HexStr4) -> PayDictT.AIR_QUALITY:
491
- """Return the air quality (%): poor (0.0) to excellent (1.0).
506
+ """Return the air quality percentage (0.0 to 1.0) and its basis.
492
507
 
493
508
  The basis of the air quality level should be one of: VOC, CO2 or relative humidity.
494
509
  If air_quality is EF, air_quality_basis should be 00.
495
510
 
496
511
  The sensor value is None if there is no sensor present (is not an error).
497
512
  The dict does not include the key if there is a sensor fault.
513
+
514
+ :param value: The 4-character hex string encoding quality and basis
515
+ :type value: HexStr4
516
+ :return: A dictionary containing the air quality and its basis (e.g., CO2, VOC)
517
+ :rtype: PayDictT.AIR_QUALITY
498
518
  """ # VOC: Volatile organic compounds
499
519
 
500
520
  # TODO: remove this as API used only internally...
ramses_tx/message.py CHANGED
@@ -54,7 +54,9 @@ class MessageBase:
54
54
  def __init__(self, pkt: Packet) -> None:
55
55
  """Create a message from a valid packet.
56
56
 
57
- :raises InvalidPacketError if message payload is invalid.
57
+ :param pkt: The packet to process into a message
58
+ :type pkt: Packet
59
+ :raises PacketInvalid: If the packet payload cannot be parsed.
58
60
  """
59
61
 
60
62
  self._pkt = pkt
@@ -359,10 +361,14 @@ def re_compile_re_match(regex: str, string: str) -> bool: # Optional[Match[Any]
359
361
 
360
362
 
361
363
  def _check_msg_payload(msg: MessageBase, payload: str) -> None:
362
- """Validate the packet's payload against its verb/code pair.
363
-
364
- :raises InvalidPayloadError if the payload is seen as invalid. Such payloads may
365
- actually be valid, in which case the rules (likely the regex) will need updating.
364
+ """Validate a packet's payload against its verb/code pair.
365
+
366
+ :param msg: The message object being validated
367
+ :type msg: MessageBase
368
+ :param payload: The raw hex payload string
369
+ :type payload: str
370
+ :raises PacketInvalid: If the code is unknown or verb/code pair is invalid.
371
+ :raises PacketPayloadInvalid: If the payload does not match the expected regex.
366
372
  """
367
373
 
368
374
  _ = repr(msg._pkt) # HACK: ? raise InvalidPayloadError
ramses_tx/packet.py CHANGED
@@ -48,9 +48,14 @@ class Packet(Frame):
48
48
  _rssi: str
49
49
 
50
50
  def __init__(self, dtm: dt, frame: str, **kwargs: Any) -> None:
51
- """Create a packet from a string (actually from f"{RSSI} {frame}").
52
-
53
- Will raise InvalidPacketError if it is invalid.
51
+ """Create a packet from a raw frame string.
52
+
53
+ :param dtm: The timestamp when the packet was received
54
+ :type dtm: dt
55
+ :param frame: The raw frame string, typically including RSSI
56
+ :type frame: str
57
+ :param kwargs: Metadata including 'comment', 'err_msg', or 'raw_frame'
58
+ :raises PacketInvalid: If the frame content is malformed.
54
59
  """
55
60
 
56
61
  super().__init__(frame[4:]) # remove RSSI
@@ -156,9 +161,12 @@ class Packet(Frame):
156
161
 
157
162
  # TODO: remove None as a possible return value
158
163
  def pkt_lifespan(pkt: Packet) -> td: # import OtbGateway??
159
- """Return the pkt lifespan, or dt.max() if the packet does not expire.
164
+ """Return the lifespan of a packet before it expires.
160
165
 
161
- Some codes require a valid payload to best determine lifespan (e.g. 1F09).
166
+ :param pkt: The packet instance to evaluate
167
+ :type pkt: Packet
168
+ :return: The duration the packet's data remains valid
169
+ :rtype: td
162
170
  """
163
171
 
164
172
  if pkt.verb in (RQ, W_):