ramses-rf 0.52.4__py3-none-any.whl → 0.53.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ ramses_cli/__init__.py,sha256=uvGzWqOf4avvgzxJNSLFWEelIWqSZ-AeLAZzg5x58bc,397
2
+ ramses_cli/client.py,sha256=OpsVfrdwChrBKia_yfAlI60ERMg7KAk3BWRpQyY3AWA,24762
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/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ ramses_cli/utils/cat_slow.py,sha256=AhUpM5gnegCitNKU-JGHn-DrRzSi-49ZR1Qw6lxe_t8,607
7
+ ramses_cli/utils/convert.py,sha256=N3LxGe3_0pclijtmYW-ChqCuPTzbkoJA4XNAnoSnBk0,1806
8
+ ramses_rf/__init__.py,sha256=AXsCK1Eh9FWeAI9D_zY_2KB0dqrTb9a5TNY1NvyQaDM,1271
9
+ ramses_rf/binding_fsm.py,sha256=fuqvcc9YW-wr8SPH8zadpPqrHAvzl_eeWF-IBtlLppY,26632
10
+ ramses_rf/const.py,sha256=L3z31CZ-xqno6oZp_h-67CB_5tDDqTwSWXsqRtsjMcs,5460
11
+ ramses_rf/database.py,sha256=9nY1Wt2hjkBoDvDqopzPP3mPSH5-Q5XHPQuJnM2GHOw,21362
12
+ ramses_rf/dispatcher.py,sha256=YjEU-QrBLo9IfoEhJo2ikg_FxOaMYoWvzelr9Vi-JZ8,11398
13
+ ramses_rf/entity_base.py,sha256=6jsHQD_5buaV56rF6nTgzPKOLsXFGVly9EHAeigqeRg,59039
14
+ ramses_rf/exceptions.py,sha256=mt_T7irqHSDKir6KLaf6oDglUIdrw0S40JbOrWJk5jc,3657
15
+ ramses_rf/gateway.py,sha256=KWE7ZkHGPcZFp9eaKN9zXchkykMJLEx_k5Le99or5lQ,30064
16
+ ramses_rf/helpers.py,sha256=TNk_QkpIOB3alOp1sqnA9LOzi4fuDCeapNlW3zTzNas,4250
17
+ ramses_rf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ ramses_rf/schemas.py,sha256=X1GAK3kttuLMiSCUDY2s-85fgBxPeU8xiDa6gJ1I5mY,13543
19
+ ramses_rf/version.py,sha256=FlL_C6hC_miMOJxgK12zDE2CvaAfRA3KoLOL1_U-7QE,125
20
+ ramses_rf/device/__init__.py,sha256=sUbH5dhbYFXSoM_TPFRutpRutBRpup7_cQ9smPtDTy8,4858
21
+ ramses_rf/device/base.py,sha256=Tu5I8Lj7KplfRsIBQAYjilS6YPgTyjpU8qgKugMR2Jk,18281
22
+ ramses_rf/device/heat.py,sha256=T-ENFzH1AEOd0m4-TgV-Qk0TnLNir_pYmyKlJtxy6NI,54538
23
+ ramses_rf/device/hvac.py,sha256=vdgiPiLtCAGr7CVsGhQl6XuAFkyYdQSE_2AEdCmRl2I,48502
24
+ ramses_rf/system/__init__.py,sha256=uZLKio3gLlBzePa2aDQ1nxkcp1YXOGrn6iHTG8LiNIw,711
25
+ ramses_rf/system/faultlog.py,sha256=GdGmVGT3137KsTlV_nhccgIFEmYu6DFsLTn4S-8JSok,12799
26
+ ramses_rf/system/heat.py,sha256=qQmzgmyHy2x87gHAstn0ee7ZVVOq-GJIfDxCrC-6gFU,39254
27
+ ramses_rf/system/schedule.py,sha256=Ts6tdZPTQLV5NkgwA73tPa5QUsnZNIIuYoKC-8VsXDk,18808
28
+ ramses_rf/system/zones.py,sha256=qMv7CuvZUKBNLpPRXgFA70NFRStgVJcw062h5mc3Y8Q,37034
29
+ ramses_tx/__init__.py,sha256=sqnjM7pUGJDmec6igTtKViSB8FLX49B5gwhAmcY9ERY,3596
30
+ ramses_tx/address.py,sha256=IuwUwZxykn3fP1UCRcv4D-zbTICBe2FJjDAFX5X6VoI,9108
31
+ ramses_tx/command.py,sha256=drxmpdM4YgyPg4h0QIr1ouxK9QjfeLVgnFpDRox0CCY,125652
32
+ ramses_tx/const.py,sha256=gmRQ59V5AJx2TlACL3tDSBy5VpvEOVbQfG0z7WjFCKE,32941
33
+ ramses_tx/exceptions.py,sha256=FJSU9YkvpKjs3yeTqUJX1o3TPFSe_B01gRGIh9b3PNc,2632
34
+ ramses_tx/fingerprints.py,sha256=nfftA1E62HQnb-eLt2EqjEi_la0DAoT0wt-PtTMie0s,11974
35
+ ramses_tx/frame.py,sha256=GzNsXr15YLeidJYGtk_xPqsZQh4ehDDlUCtT6rTDhT8,22046
36
+ ramses_tx/gateway.py,sha256=Fl6EqAUU2DnLOiA2_87sS7VPBEyxA1a_ICCmg55WMMA,11494
37
+ ramses_tx/helpers.py,sha256=VIPSw0lrDrE3S92YchzeRo7G2AlQ_vE8OuYUF-rlpQw,33616
38
+ ramses_tx/logger.py,sha256=1iKRHKUaqHqGd76CkE_6mCVR0sYODtxshRRwfY61fTk,10426
39
+ ramses_tx/message.py,sha256=HUSNiBgGlnWEBeBqb4K28GcU5oB7RbS9a0JC2p9SGe4,13676
40
+ ramses_tx/opentherm.py,sha256=58PXz9l5x8Ou6Fm3y-R_UnGHCYahoi2RKIDdYStUMzk,42378
41
+ ramses_tx/packet.py,sha256=_nzuInS_WhdOI26SYvgsdDqIaDvVNguc2YDwdPOVCbU,7661
42
+ ramses_tx/parsers.py,sha256=qQz8VOKaH26MUvo9AfUwhrnbH7VwXLxFc2gKJYvjXvY,148571
43
+ ramses_tx/protocol.py,sha256=nBPKCD1tcGp_FiX0qhsY0XoGO_h87w5cYywBjSpum4w,33048
44
+ ramses_tx/protocol_fsm.py,sha256=o9vLvlXor3LkPgsY1zii5P1R01GzYLf_PECDdoxtC24,27520
45
+ ramses_tx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
+ ramses_tx/ramses.py,sha256=vp748Tf_a-56OMM8CWDA2ZktRfTuj0QVyPRcnsOstSM,53983
47
+ ramses_tx/schemas.py,sha256=Hrmf_q9bAZtkKJzGu6GtUO0QV_-K9i4L99EzGWR13eE,13408
48
+ ramses_tx/transport.py,sha256=Tk2CPAu9W0T4HCe6Q2VUYex2bFNZlyYsluP7iLf0rdQ,76277
49
+ ramses_tx/typed_dicts.py,sha256=w-0V5t2Q3GiNUOrRAWiW9GtSwbta_7luME6GfIb1zhI,10869
50
+ ramses_tx/typing.py,sha256=eF2SlPWhNhEFQj6WX2AhTXiyRQVXYnFutiepllYl2rI,5042
51
+ ramses_tx/version.py,sha256=DToXw6Td-r5_4JxS4fbBBGkajaXoEj8xCJOh7vaNcQI,123
52
+ ramses_rf-0.53.0.dist-info/METADATA,sha256=YkSNqr4RWgOsSi-z8SO5H4LEbzOI87IVYurjHWpUgRc,4179
53
+ ramses_rf-0.53.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
54
+ ramses_rf-0.53.0.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
55
+ ramses_rf-0.53.0.dist-info/licenses/LICENSE,sha256=ptVutrtSMr7X-ek6LduiD8Cce4JsNn_8sR8MYlm-fvo,1086
56
+ ramses_rf-0.53.0.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
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021-2025 David Bonnes
3
+ Copyright (c) 2021-2026 D. Bonnes and E. Broerse
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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
@@ -1866,7 +1866,7 @@ class Command(Frame):
1866
1866
  The actual number of available zones depends on the controller configuration.
1867
1867
  Requesting a non-existent zone will typically result in no response.
1868
1868
  """
1869
- return cls.from_attrs(W_, ctl_id, Code._2309, _check_idx(zone_idx))
1869
+ return cls.from_attrs(RQ, ctl_id, Code._2309, _check_idx(zone_idx))
1870
1870
 
1871
1871
  @classmethod # constructor for W|2309
1872
1872
  def set_zone_setpoint(
@@ -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
@@ -1,5 +1,9 @@
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
+
4
+ This module contains constants, enums, and helper classes used throughout the
5
+ library to decode and encode RAMSES-II protocol packets.
6
+ """
3
7
 
4
8
  from __future__ import annotations
5
9
 
@@ -15,16 +19,23 @@ DEV_MODE = __dev_mode__
15
19
  DEFAULT_DISABLE_QOS: Final[bool | None] = None
16
20
  DEFAULT_WAIT_FOR_REPLY: Final[bool | None] = None
17
21
 
18
- DEFAULT_ECHO_TIMEOUT: Final[float] = 0.50 # waiting for echo pkt after cmd sent
19
- DEFAULT_RPLY_TIMEOUT: Final[float] = 0.50 # waiting for reply pkt after echo pkt rcvd
22
+ #: Waiting for echo pkt after cmd sent (seconds)
23
+ DEFAULT_ECHO_TIMEOUT: Final[float] = 0.50
24
+
25
+ #: Waiting for reply pkt after echo pkt rcvd (seconds)
26
+ DEFAULT_RPLY_TIMEOUT: Final[float] = 0.50
20
27
  DEFAULT_BUFFER_SIZE: Final[int] = 32
21
28
 
22
- DEFAULT_SEND_TIMEOUT: Final[float] = 20.0 # total waiting for successful send: FIXME
23
- MAX_SEND_TIMEOUT: Final[float] = 20.0 # for a command to be sent, incl. queuing time
29
+ #: Total waiting for successful send (seconds)
30
+ DEFAULT_SEND_TIMEOUT: Final[float] = 20.0
31
+ #: For a command to be sent, incl. queuing time (seconds)
32
+ MAX_SEND_TIMEOUT: Final[float] = 20.0
24
33
 
25
- MAX_RETRY_LIMIT: Final[int] = 3 # for a command to be re-sent (not incl. 1st send)
34
+ #: For a command to be re-sent (not incl. 1st send)
35
+ MAX_RETRY_LIMIT: Final[int] = 3
26
36
 
27
- MIN_INTER_WRITE_GAP: Final[float] = 0.05 # seconds
37
+ #: Minimum gap between writes (seconds)
38
+ MIN_INTER_WRITE_GAP: Final[float] = 0.05
28
39
  DEFAULT_GAP_DURATION: Final[float] = MIN_INTER_WRITE_GAP
29
40
  DEFAULT_MAX_RETRIES: Final[int] = 3
30
41
  DEFAULT_NUM_REPEATS: Final[int] = 0
@@ -139,6 +150,8 @@ SZ_OTC_ACTIVE: Final = "otc_active"
139
150
 
140
151
  @verify(EnumCheck.UNIQUE)
141
152
  class Priority(IntEnum):
153
+ """Priority levels for protocol messages."""
154
+
142
155
  LOWEST = 4
143
156
  LOW = 2
144
157
  DEFAULT = 0
@@ -147,31 +160,60 @@ class Priority(IntEnum):
147
160
 
148
161
 
149
162
  def slug(string: str) -> str:
150
- """Convert a string to snake_case."""
163
+ """Convert a string to snake_case.
164
+
165
+ :param string: The input string to convert.
166
+ :return: The string converted to snake_case (lowercase, with non-alphanumerics replaced by underscores).
167
+ """
151
168
  return re.sub(r"[\W_]+", "_", string.lower())
152
169
 
153
170
 
154
171
  # TODO: FIXME: This is a mess - needs converting to StrEnum
155
172
  class AttrDict(dict): # type: ignore[type-arg]
173
+ """A read-only dictionary that supports dot-access and two-way lookup.
174
+
175
+ This class is typically used to map hex codes (keys) to human-readable slugs (values),
176
+ while also allowing reverse lookup via dot notation (e.g., ``map.SLUG``).
177
+
178
+ .. warning::
179
+ This class is immutable. Attempting to modify it will raise a :exc:`TypeError`.
180
+ """
181
+
156
182
  _SZ_AKA_SLUG: Final = "_root_slug"
157
183
  _SZ_DEFAULT: Final = "_default"
158
184
  _SZ_SLUGS: Final = "SLUGS"
159
185
 
160
- @classmethod
161
- def __readonly(cls, *args: Any, **kwargs: Any) -> NoReturn:
162
- raise TypeError(f"'{cls.__class__.__name__}' object is read only")
186
+ def _readonly(self, *args: Any, **kwargs: Any) -> NoReturn:
187
+ """Raise TypeError for read-only operations."""
188
+ raise TypeError(f"'{self.__class__.__name__}' object is read only")
189
+
190
+ def __setitem__(self, key: Any, value: Any) -> NoReturn:
191
+ self._readonly()
163
192
 
164
- __delitem__ = __readonly
165
- __setitem__ = __readonly
166
- clear = __readonly
167
- pop = __readonly
168
- popitem = __readonly
169
- setdefault = __readonly
170
- update = __readonly
193
+ def __delitem__(self, key: Any) -> NoReturn:
194
+ self._readonly()
171
195
 
172
- del __readonly
196
+ def clear(self) -> NoReturn:
197
+ self._readonly()
198
+
199
+ def pop(self, *args: Any, **kwargs: Any) -> NoReturn:
200
+ self._readonly()
201
+
202
+ def popitem(self) -> NoReturn:
203
+ self._readonly()
204
+
205
+ def setdefault(self, *args: Any, **kwargs: Any) -> NoReturn:
206
+ self._readonly()
207
+
208
+ def update(self, *args: Any, **kwargs: Any) -> NoReturn:
209
+ self._readonly()
173
210
 
174
211
  def __init__(self, main_table: dict[str, dict], attr_table: dict[str, Any]) -> None: # type: ignore[type-arg]
212
+ """Initialize the AttrDict.
213
+
214
+ :param main_table: A dictionary mapping keys (usually hex codes) to property dictionaries.
215
+ :param attr_table: A dictionary of additional attributes to expose on the object.
216
+ """
175
217
  self._main_table = main_table
176
218
  self._attr_table = attr_table
177
219
  self._attr_table[self._SZ_SLUGS] = tuple(sorted(main_table.keys()))
@@ -237,7 +279,12 @@ class AttrDict(dict): # type: ignore[type-arg]
237
279
  return self.__getattribute__(name)
238
280
 
239
281
  def _hex(self, key: str) -> str:
240
- """Return the key/ID (2-byte hex string) of the two-way dict (e.g. '04')."""
282
+ """Return the key/ID (2-byte hex string) of the two-way dict (e.g. '04').
283
+
284
+ :param key: The lookup key (can be slug or code).
285
+ :raises KeyError: If the key is not found.
286
+ :return: The 2-byte hex string identifier.
287
+ """
241
288
  if key in self._main_table:
242
289
  return list(self._main_table[key].keys())[0] # type: ignore[no-any-return]
243
290
  if key in self._reverse:
@@ -245,7 +292,12 @@ class AttrDict(dict): # type: ignore[type-arg]
245
292
  raise KeyError(key)
246
293
 
247
294
  def _str(self, key: str) -> str:
248
- """Return the value (string) of the two-way dict (e.g. 'radiator_valve')."""
295
+ """Return the value (string) of the two-way dict (e.g. 'radiator_valve').
296
+
297
+ :param key: The lookup key.
298
+ :raises KeyError: If the key is not found.
299
+ :return: The human-readable slug string.
300
+ """
249
301
  if key in self._main_table:
250
302
  return list(self._main_table[key].values())[0] # type: ignore[no-any-return]
251
303
  if key in self:
@@ -256,14 +308,23 @@ class AttrDict(dict): # type: ignore[type-arg]
256
308
  # return {k: k for k in super().values()}.values()
257
309
 
258
310
  def slug(self, key: str) -> str:
259
- """WIP: Return master slug for a hex key/ID (e.g. 00 -> 'TRV', not 'TR0')."""
311
+ """Return master slug for a hex key/ID.
312
+
313
+ Example: 00 -> 'TRV' (master), not 'TR0'.
314
+
315
+ :param key: The hex key to look up.
316
+ :return: The master slug.
317
+ """
260
318
  slug_ = self._slug_lookup[key]
261
319
  # if slug_ in self._attr_table["_TRANSFORMS"]:
262
320
  # return self._attr_table["_TRANSFORMS"][slug_]
263
321
  return slug_ # type: ignore[no-any-return]
264
322
 
265
323
  def slugs(self) -> tuple[str]:
266
- """Return the slugs from the main table."""
324
+ """Return the slugs from the main table.
325
+
326
+ :return: A tuple of all available slugs.
327
+ """
267
328
  return self._attr_table[self._SZ_SLUGS] # type: ignore[no-any-return]
268
329
 
269
330
 
@@ -271,6 +332,12 @@ def attr_dict_factory(
271
332
  main_table: dict[str, dict], # type: ignore[type-arg]
272
333
  attr_table: dict | None = None, # type: ignore[type-arg]
273
334
  ) -> AttrDict: # is: SlottedAttrDict
335
+ """Create a new AttrDict instance with a slotted subclass.
336
+
337
+ :param main_table: The primary mapping of codes to slugs.
338
+ :param attr_table: Optional additional attributes to attach to the instance.
339
+ :return: An instance of a dynamic AttrDict subclass.
340
+ """
274
341
  if attr_table is None:
275
342
  attr_table = {}
276
343
 
@@ -295,6 +362,8 @@ def attr_dict_factory(
295
362
  # slugs for device/zone entity klasses, used by 0005/000C
296
363
  @verify(EnumCheck.UNIQUE)
297
364
  class DevRole(StrEnum):
365
+ """Slugs for device/zone entity classes, used by commands 0005/000C."""
366
+
298
367
  #
299
368
  # Generic device/zone classes
300
369
  ACT = "ACT" # Generic heating zone actuator group
@@ -345,6 +414,8 @@ DEV_ROLE_MAP = attr_dict_factory(
345
414
  # slugs for device entity types, used in device_ids
346
415
  @verify(EnumCheck.UNIQUE)
347
416
  class DevType(StrEnum):
417
+ """Slugs for device entity types, used in device_ids."""
418
+
348
419
  #
349
420
  # Promotable/Generic devices
350
421
  DEV = "DEV" # xx: Promotable device
@@ -457,6 +528,8 @@ DEV_TYPE_MAP = attr_dict_factory(
457
528
 
458
529
  # slugs for zone entity klasses, used by 0005/000C
459
530
  class ZoneRole(StrEnum):
531
+ """Slugs for zone entity classes, used by commands 0005/000C."""
532
+
460
533
  #
461
534
  # Generic device/zone classes
462
535
  ACT = "ACT" # Generic heating zone actuator group
@@ -645,6 +718,8 @@ MESSAGE_REGEX = re.compile(f"^{r} {v} {r} {d} {d} {d} {c} {l} {p}$")
645
718
 
646
719
  # Used by 0418/system_fault parser
647
720
  class FaultDeviceClass(StrEnum):
721
+ """Device classes for system faults."""
722
+
648
723
  CONTROLLER = "controller"
649
724
  SENSOR = "sensor"
650
725
  SETPOINT = "setpoint"
@@ -666,6 +741,8 @@ FAULT_DEVICE_CLASS: Final[dict[str, FaultDeviceClass]] = {
666
741
 
667
742
 
668
743
  class FaultState(StrEnum):
744
+ """States for system faults."""
745
+
669
746
  FAULT = "fault"
670
747
  RESTORE = "restore"
671
748
  UNKNOWN_C0 = "unknown_c0"
@@ -680,6 +757,8 @@ FAULT_STATE: Final[dict[str, FaultState]] = { # a bitmap?
680
757
 
681
758
 
682
759
  class FaultType(StrEnum):
760
+ """Types of system faults."""
761
+
683
762
  SYSTEM_FAULT = "system_fault"
684
763
  MAINS_LOW = "mains_low"
685
764
  BATTERY_LOW = "battery_low"
@@ -703,6 +782,8 @@ FAULT_TYPE: Final[dict[str, FaultType]] = {
703
782
 
704
783
 
705
784
  class SystemType(StrEnum):
785
+ """System types (e.g. Evohome, Hometronics)."""
786
+
706
787
  CHRONOTHERM = "chronotherm"
707
788
  EVOHOME = "evohome"
708
789
  HOMETRONICS = "hometronics"
@@ -742,6 +823,8 @@ FAN_RATE: Final = "fan_rate" # percentage, 0.0 - 1.0 # deprecated, use SZ_FAN_
742
823
  # Below, verbs & codes - can use Verb/Code/Index for mypy type checking
743
824
  @verify(EnumCheck.UNIQUE)
744
825
  class VerbT(StrEnum):
826
+ """Protocol verbs (message types)."""
827
+
745
828
  I_ = " I"
746
829
  RQ = "RQ"
747
830
  RP = "RP"
@@ -756,6 +839,8 @@ W_: Final = VerbT.W_
756
839
 
757
840
  @verify(EnumCheck.UNIQUE)
758
841
  class MsgId(StrEnum):
842
+ """Message identifiers."""
843
+
759
844
  _00 = "00"
760
845
  _03 = "03"
761
846
  _06 = "06"
@@ -791,6 +876,8 @@ class MsgId(StrEnum):
791
876
  # StrEnum is intended to include all known codes, see: test suite, code schema in ramses.py
792
877
  @verify(EnumCheck.UNIQUE)
793
878
  class Code(StrEnum):
879
+ """Protocol command codes."""
880
+
794
881
  _0001 = "0001"
795
882
  _0002 = "0002"
796
883
  _0004 = "0004"
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_):