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_cli/client.py +1 -0
- ramses_cli/debug.py +1 -1
- ramses_cli/utils/convert.py +2 -2
- ramses_rf/database.py +47 -20
- ramses_rf/device/base.py +14 -3
- ramses_rf/device/heat.py +2 -2
- ramses_rf/device/hvac.py +24 -21
- ramses_rf/entity_base.py +70 -29
- ramses_rf/gateway.py +30 -19
- ramses_rf/schemas.py +1 -1
- ramses_rf/system/heat.py +1 -1
- ramses_rf/system/zones.py +22 -2
- ramses_rf/version.py +1 -1
- {ramses_rf-0.52.3.dist-info → ramses_rf-0.52.5.dist-info}/METADATA +2 -1
- ramses_rf-0.52.5.dist-info/RECORD +55 -0
- {ramses_rf-0.52.3.dist-info → ramses_rf-0.52.5.dist-info}/WHEEL +1 -1
- ramses_tx/address.py +21 -6
- ramses_tx/command.py +18 -2
- ramses_tx/const.py +1 -1
- ramses_tx/helpers.py +30 -10
- ramses_tx/message.py +11 -5
- ramses_tx/packet.py +13 -5
- ramses_tx/parsers.py +1096 -28
- ramses_tx/protocol.py +95 -20
- ramses_tx/ramses.py +9 -5
- ramses_tx/transport.py +31 -6
- ramses_tx/version.py +1 -1
- ramses_rf-0.52.3.dist-info/RECORD +0 -55
- {ramses_rf-0.52.3.dist-info → ramses_rf-0.52.5.dist-info}/entry_points.txt +0 -0
- {ramses_rf-0.52.3.dist-info → ramses_rf-0.52.5.dist-info}/licenses/LICENSE +0 -0
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
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ramses_rf
|
|
3
|
-
Version: 0.52.
|
|
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
|

|
|
23
23
|

|
|
24
24
|

|
|
25
|
+
[](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,,
|
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
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
:
|
|
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
|
|
363
|
-
|
|
364
|
-
:
|
|
365
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
164
|
+
"""Return the lifespan of a packet before it expires.
|
|
160
165
|
|
|
161
|
-
|
|
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_):
|