ramses-rf 0.51.7__py3-none-any.whl → 0.51.9__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_tx/const.py CHANGED
@@ -788,7 +788,7 @@ class MsgId(StrEnum):
788
788
  _7F = "7F"
789
789
 
790
790
 
791
- # StrEnum is intended include all known codes, see: test suite, code schema in ramses.py
791
+ # StrEnum is intended to include all known codes, see: test suite, code schema in ramses.py
792
792
  @verify(EnumCheck.UNIQUE)
793
793
  class Code(StrEnum):
794
794
  _0001 = "0001"
ramses_tx/frame.py CHANGED
@@ -65,7 +65,7 @@ class Frame:
65
65
  def __init__(self, frame: str) -> None:
66
66
  """Create a frame from a string.
67
67
 
68
- Will raise InvalidPacketError if it is invalid.
68
+ :raises InvalidPacketError: if provided string is invalid.
69
69
  """
70
70
 
71
71
  self._frame: str = frame
@@ -123,7 +123,7 @@ class Frame:
123
123
  if not strict_checking:
124
124
  return
125
125
 
126
- try: # Strict checking: helps users avoid to constructing bad commands
126
+ try: # Strict checking: helps users avoid constructing bad commands
127
127
  if addrs[0] == NON_DEV_ADDR:
128
128
  assert self.verb == I_, "wrong verb or dst addr should be present"
129
129
  elif addrs[2] == NON_DEV_ADDR:
@@ -138,7 +138,7 @@ class Frame:
138
138
  raise exc.PacketInvalid(f"Bad frame: Invalid address set: {err}") from err
139
139
 
140
140
  def __repr__(self) -> str:
141
- """Return a unambiguous string representation of this object."""
141
+ """Return an unambiguous string representation of this object."""
142
142
 
143
143
  if self._repr is None:
144
144
  self._repr = " ".join( # type: ignore[unreachable]
@@ -387,7 +387,7 @@ class Frame:
387
387
 
388
388
  @property
389
389
  def _hdr(self) -> HeaderT: # incl. self._ctx
390
- """Return the QoS header (fingerprint) of this packet (i.e. device_id/code/hdr).
390
+ """Return the QoS header (fingerprint) of this packet (i.e. device_id|code|verb).
391
391
 
392
392
  Used for QoS (timeouts, retries), callbacks, etc.
393
393
  """
ramses_tx/gateway.py CHANGED
@@ -91,7 +91,7 @@ class Engine:
91
91
  if input_file:
92
92
  self._disable_sending = True
93
93
  elif not port_name:
94
- raise TypeError("Either a port_name or a input_file must be specified")
94
+ raise TypeError("Either a port_name or an input_file must be specified")
95
95
 
96
96
  self.ser_name = port_name
97
97
  self._input_file = input_file
ramses_tx/helpers.py CHANGED
@@ -787,9 +787,9 @@ def fan_info_to_byte(info: str) -> int:
787
787
 
788
788
  def fan_info_flags(flags_list: list[int]) -> int:
789
789
  flag_res: int = 0
790
- for index, shft in enumerate(range(7, 4, -1)): # index = 7, 6 and 5
790
+ for index, shift in enumerate(range(7, 4, -1)): # index = 7, 6 and 5
791
791
  if flags_list[index] == 1:
792
- flag_res |= 1 << shft # set bits
792
+ flag_res |= 1 << shift # set bits
793
793
  return flag_res
794
794
 
795
795
 
ramses_tx/message.py CHANGED
@@ -54,7 +54,7 @@ class MessageBase:
54
54
  def __init__(self, pkt: Packet) -> None:
55
55
  """Create a message from a valid packet.
56
56
 
57
- Will raise InvalidPacketError if it is invalid.
57
+ :raises InvalidPacketError if message payload is invalid.
58
58
  """
59
59
 
60
60
  self._pkt = pkt
@@ -66,11 +66,15 @@ class MessageBase:
66
66
  self.dtm: dt = pkt.dtm
67
67
 
68
68
  self.verb: VerbT = pkt.verb
69
- self.seqn: str = pkt.seqn
69
+ self.seqn: str = (
70
+ pkt.seqn
71
+ ) # the msg is part of a set for 1 Code, received in order
70
72
  self.code: Code = pkt.code
71
73
  self.len: int = pkt._len
72
74
 
73
- self._payload = self._validate(self._pkt.payload) # ? raise InvalidPacketError
75
+ self._payload = self._validate(
76
+ self._pkt.payload
77
+ ) # ? may raise InvalidPacketError
74
78
 
75
79
  self._str: str = None # type: ignore[assignment]
76
80
 
@@ -92,7 +96,7 @@ class MessageBase:
92
96
 
93
97
  if self.src.id == self._addrs[0].id: # type: ignore[unreachable]
94
98
  name_0 = self._name(self.src)
95
- name_1 = "" if self.dst is self.src else self._name(self.dst)
99
+ name_1 = "" if self.dst == self.src else self._name(self.dst)
96
100
  else:
97
101
  name_0 = ""
98
102
  name_1 = self._name(self.src)
@@ -139,16 +143,18 @@ class MessageBase:
139
143
 
140
144
  @property
141
145
  def _has_array(self) -> bool:
142
- """Return True if the message's raw payload is an array."""
146
+ """
147
+ :return: True if the message's raw payload is an array.
148
+ """
143
149
 
144
150
  return bool(self._pkt._has_array)
145
151
 
146
152
  @property
147
153
  def _idx(self) -> dict[str, str]:
148
- """Return the domain_id/zone_idx/other_idx of a message payload, if any.
154
+ """Get the domain_id/zone_idx/other_idx of a message payload, if any.
155
+ Used to identify the zone/domain that a message applies to.
149
156
 
150
- Used to identify the zone/domain that a message applies to. Returns an empty
151
- dict if there is none such, or None if undetermined.
157
+ :return: an empty dict if there is none such, or None if undetermined.
152
158
  """
153
159
 
154
160
  # .I --- 01:145038 --:------ 01:145038 3B00 002 FCC8
@@ -229,7 +235,7 @@ class MessageBase:
229
235
  assert isinstance(self._pkt._idx, str) # mypy hint
230
236
  return {IDX_NAMES[Code._22C9]: self._pkt._idx}
231
237
 
232
- assert isinstance(self._pkt._idx, str) # mypy check
238
+ assert isinstance(self._pkt._idx, str) # mypy hint
233
239
  idx_name = SZ_DOMAIN_ID if self._pkt._idx[:1] == "F" else SZ_ZONE_IDX
234
240
  index_name = IDX_NAMES.get(self.code, idx_name)
235
241
 
@@ -237,9 +243,10 @@ class MessageBase:
237
243
 
238
244
  # TODO: needs work...
239
245
  def _validate(self, raw_payload: str) -> dict | list[dict]: # type: ignore[type-arg]
240
- """Validate the message, and parse the payload if so.
246
+ """Validate a message packet payload, and parse it if valid.
241
247
 
242
- Raise an exception (InvalidPacketError) if it is not valid.
248
+ :return: a dict containing key: value pairs, or a list of those created from the payload
249
+ :raises an InvalidPacketError exception if it is not valid.
243
250
  """
244
251
 
245
252
  try: # parse the payload
@@ -249,10 +256,9 @@ class MessageBase:
249
256
  if not self._has_payload and (
250
257
  self.verb == RQ and self.code not in RQ_IDX_COMPLEX
251
258
  ):
252
- # _LOGGER.error("%s", msg)
253
259
  return {}
254
260
 
255
- result = parse_payload(self)
261
+ result = parse_payload(self) # invoke the code parsers
256
262
 
257
263
  if isinstance(result, list):
258
264
  return result
@@ -353,7 +359,7 @@ def re_compile_re_match(regex: str, string: str) -> bool: # Optional[Match[Any]
353
359
  def _check_msg_payload(msg: MessageBase, payload: str) -> None:
354
360
  """Validate the packet's payload against its verb/code pair.
355
361
 
356
- Raise an InvalidPayloadError if the payload is seen as invalid. Such payloads may
362
+ :raises InvalidPayloadError if the payload is seen as invalid. Such payloads may
357
363
  actually be valid, in which case the rules (likely the regex) will need updating.
358
364
  """
359
365
 
ramses_tx/packet.py CHANGED
@@ -104,7 +104,7 @@ class Packet(Frame):
104
104
  return f"{dtm} ... {self}{hdr}"
105
105
 
106
106
  def __str__(self) -> str:
107
- """Return a brief readable string representation of this object."""
107
+ """Return a brief readable string representation of this object aka 'header'."""
108
108
  # e.g.: 000A|RQ|01:145038|08
109
109
  return super().__repr__() # TODO: self._hdr
110
110
 
ramses_tx/parsers.py CHANGED
@@ -1391,15 +1391,18 @@ def parser_2210(payload: str, msg: Message) -> dict[str, Any]:
1391
1391
  "02", # requested by CO2 level/sensor
1392
1392
  "03", # requested by humidity level/sensor
1393
1393
  ), f"expected req_reason (00|02|03), not {payload[20:22]}"
1394
- assert payload[78:80] in ("00", "02"), (
1395
- f"expected byte 39 (00|02), not {payload[78:80]}"
1396
- )
1397
- assert payload[80:82] in ("01", "08"), (
1398
- f"expected byte 40 (01|08), not {payload[80:82]}"
1399
- )
1400
- assert payload[82:] in ("00", "40"), (
1401
- f"expected byte 41- (00|40), not {payload[82:]}"
1402
- )
1394
+ assert payload[78:80] in (
1395
+ "00",
1396
+ "02",
1397
+ ), f"expected byte 39 (00|02), not {payload[78:80]}"
1398
+ assert payload[80:82] in (
1399
+ "01",
1400
+ "08",
1401
+ ), f"expected byte 40 (01|08), not {payload[80:82]}"
1402
+ assert payload[82:] in (
1403
+ "00",
1404
+ "40",
1405
+ ), f"expected byte 41- (00|40), not {payload[82:]}"
1403
1406
 
1404
1407
  except AssertionError as err:
1405
1408
  _LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
@@ -1909,42 +1912,57 @@ def parser_2411(payload: str, msg: Message) -> dict[str, Any]:
1909
1912
  "92": (4, hex_to_temp), # 75 (0-30) (C)
1910
1913
  } # TODO: _2411_TYPES.get(payload[8:10], (8, no_op))
1911
1914
 
1912
- assert payload[4:6] in _2411_TABLE, (
1913
- f"param {payload[4:6]} is unknown"
1914
- ) # _INFORM_DEV_MSG
1915
- description = _2411_TABLE.get(payload[4:6], "Unknown")
1915
+ # Handle unknown parameters gracefully instead of asserting
1916
+ param_id = payload[4:6]
1917
+ try:
1918
+ description = _2411_TABLE.get(param_id, "Unknown")
1919
+ if param_id not in _2411_TABLE:
1920
+ _LOGGER.warning(
1921
+ f"2411 message received with unknown parameter ID: {param_id}. "
1922
+ f"This parameter is not in the known parameter schema. "
1923
+ f"Message: {msg!r}"
1924
+ )
1925
+ except Exception as err:
1926
+ _LOGGER.warning(f"Error looking up 2411 parameter {param_id}: {err}")
1927
+ description = "Unknown"
1916
1928
 
1917
1929
  result = {
1918
- "parameter": payload[4:6],
1930
+ "parameter": param_id,
1919
1931
  "description": description,
1920
1932
  }
1921
1933
 
1922
1934
  if msg.verb == RQ:
1923
1935
  return result
1924
1936
 
1925
- assert payload[8:10] in _2411_DATA_TYPES, (
1926
- f"param {payload[4:6]} has unknown data_type: {payload[8:10]}"
1927
- ) # _INFORM_DEV_MSG
1928
- length, parser = _2411_DATA_TYPES.get(payload[8:10], (8, lambda x: x))
1937
+ try:
1938
+ assert payload[8:10] in _2411_DATA_TYPES, (
1939
+ f"param {param_id} has unknown data_type: {payload[8:10]}"
1940
+ ) # _INFORM_DEV_MSG
1941
+ length, parser = _2411_DATA_TYPES.get(payload[8:10], (8, lambda x: x))
1942
+
1943
+ result |= {
1944
+ "value": parser(payload[10:18][-length:]), # type: ignore[operator]
1945
+ "_value_06": payload[6:10],
1946
+ }
1929
1947
 
1930
- result |= {
1931
- "value": parser(payload[10:18][-length:]), # type: ignore[operator]
1932
- "_value_06": payload[6:10],
1933
- }
1948
+ if msg.len == 9:
1949
+ return result
1934
1950
 
1935
- if msg.len == 9:
1951
+ return (
1952
+ result
1953
+ | {
1954
+ "min_value": parser(payload[18:26][-length:]), # type: ignore[operator]
1955
+ "max_value": parser(payload[26:34][-length:]), # type: ignore[operator]
1956
+ "precision": parser(payload[34:42][-length:]), # type: ignore[operator]
1957
+ "_value_42": payload[42:],
1958
+ }
1959
+ )
1960
+ except AssertionError as err:
1961
+ _LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
1962
+ # Return partial result for unknown parameters
1963
+ result["value"] = ""
1936
1964
  return result
1937
1965
 
1938
- return (
1939
- result
1940
- | {
1941
- "min_value": parser(payload[18:26][-length:]), # type: ignore[operator]
1942
- "max_value": parser(payload[26:34][-length:]), # type: ignore[operator]
1943
- "precision": parser(payload[34:42][-length:]), # type: ignore[operator]
1944
- "_value_42": payload[42:],
1945
- }
1946
- )
1947
-
1948
1966
 
1949
1967
  # unknown_2420, from OTB
1950
1968
  def parser_2420(payload: str, msg: Message) -> dict[str, Any]:
@@ -2344,7 +2362,7 @@ def parser_3210(payload: str, msg: Message) -> PayDictT._3210:
2344
2362
  return {SZ_TEMPERATURE: hex_to_temp(payload[2:])}
2345
2363
 
2346
2364
 
2347
- # opentherm_msg, from OTB (and some RND)
2365
+ # opentherm_msg, from OTB (and OT_RND)
2348
2366
  def parser_3220(payload: str, msg: Message) -> dict[str, Any]:
2349
2367
  try:
2350
2368
  ot_type, ot_id, ot_value, ot_schema = decode_frame(payload[2:10])
@@ -2968,8 +2986,12 @@ _PAYLOAD_PARSERS = {
2968
2986
 
2969
2987
 
2970
2988
  def parse_payload(msg: Message) -> dict | list[dict]:
2989
+ """
2990
+ Apply the appropriate parser defined in this module to the message.
2991
+ :param msg: a Message object containing packet data and extra attributes
2992
+ :return: a dict of key: value pairs or a list of such dicts, e.g. {'temperature': 21.5}
2993
+ """
2971
2994
  result: dict | list[dict]
2972
-
2973
2995
  result = _PAYLOAD_PARSERS.get(msg.code, parser_unknown)(msg._pkt.payload, msg)
2974
2996
  if isinstance(result, dict) and msg.seqn.isnumeric(): # e.g. 22F1/3
2975
2997
  result["seqx_num"] = msg.seqn
ramses_tx/protocol.py CHANGED
@@ -332,7 +332,7 @@ class _DeviceIdFilterMixin(_BaseProtocol):
332
332
  /,
333
333
  *,
334
334
  disable_warnings: bool = False,
335
- strick_checking: bool = False,
335
+ strict_checking: bool = False,
336
336
  ) -> DeviceIdT | None:
337
337
  """Return the device_id of the gateway specified in the include_list, if any.
338
338
 
@@ -384,7 +384,7 @@ class _DeviceIdFilterMixin(_BaseProtocol):
384
384
  f"The {SZ_KNOWN_LIST} includes exactly one gateway (HGI): {known_hgi}"
385
385
  )
386
386
 
387
- if strick_checking:
387
+ if strict_checking:
388
388
  return known_hgi if [known_hgi] == explicit_hgis else None
389
389
  return known_hgi
390
390
 
ramses_tx/protocol_fsm.py CHANGED
@@ -291,7 +291,7 @@ class ProtocolContext:
291
291
  # may want to set some instance variables, according to type of transport
292
292
  self._state.connection_made()
293
293
 
294
- # TODO: Should we clear the buffer if connection is lost (and apoligise to senders?
294
+ # TODO: Should we clear the buffer if connection is lost (and apologise to senders?
295
295
  def connection_lost(self, err: ExceptionT | None) -> None:
296
296
  self._state.connection_lost()
297
297
 
ramses_tx/ramses.py CHANGED
@@ -223,7 +223,7 @@ CODES_SCHEMA: dict[Code, dict[str, Any]] = { # rf_unknown
223
223
  SZ_NAME: "device_info",
224
224
  I_: r"^(00|FF)([0-9A-F]{30,})?$", # r"^[0-9A-F]{32,}$" might be OK
225
225
  RQ: r"^00$", # NOTE: 63 seen (no RP), some devices will accept [0-9A-F]{2}
226
- # RP: r"^[0-9A-F]{2}([0-9A-F]){30,}$", # NOTE: indx same as RQ
226
+ # RP: r"^[0-9A-F]{2}([0-9A-F]){30,}$", # NOTE: index same as RQ
227
227
  SZ_LIFESPAN: False,
228
228
  },
229
229
  Code._10E1: { # device_id
@@ -480,6 +480,7 @@ CODES_SCHEMA: dict[Code, dict[str, Any]] = { # rf_unknown
480
480
  SZ_NAME: "fan_params",
481
481
  I_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}([0-9A-F]{8}){4}[0-9A-F]{4}$",
482
482
  RQ: r"^(00|01|15|16|17|21)00[0-9A-F]{2}((00){19})?$",
483
+ RP: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}(([0-9A-F]{8}){3}[0-9A-F]{4})?$",
483
484
  W_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}(([0-9A-F]{8}){3}[0-9A-F]{4})?$",
484
485
  },
485
486
  Code._2420: { # unknown_2420, from OTB
@@ -1220,6 +1221,7 @@ SZ_MIN_VALUE: Final = "min_value"
1220
1221
  SZ_MAX_VALUE: Final = "max_value"
1221
1222
  SZ_PRECISION: Final = "precision"
1222
1223
  SZ_DATA_TYPE: Final = "data_type"
1224
+ SZ_DATA_UNIT: Final = "data_unit"
1223
1225
 
1224
1226
  _22F1_MODE_ITHO: dict[str, str] = {
1225
1227
  "00": "off", # not seen
@@ -1265,12 +1267,13 @@ _22F1_SCHEMES: dict[str, dict[str, str]] = {
1265
1267
 
1266
1268
  # unclear if true for only Orcon/*all* models
1267
1269
  _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
1268
- "31": { # slot 09
1270
+ "31": { # slot 09 (FANs produced after 2021)
1269
1271
  SZ_DESCRIPTION: "Time to change filter (days)",
1270
1272
  SZ_MIN_VALUE: 0,
1271
1273
  SZ_MAX_VALUE: 1800,
1272
1274
  SZ_PRECISION: 30,
1273
1275
  SZ_DATA_TYPE: "10",
1276
+ SZ_DATA_UNIT: "days",
1274
1277
  },
1275
1278
  "3D": { # slot 00
1276
1279
  SZ_DESCRIPTION: "Away mode Supply fan rate (%)",
@@ -1278,6 +1281,7 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
1278
1281
  SZ_MAX_VALUE: 0.4,
1279
1282
  SZ_PRECISION: 0.005,
1280
1283
  SZ_DATA_TYPE: "0F",
1284
+ SZ_DATA_UNIT: "%",
1281
1285
  },
1282
1286
  "3E": { # slot 01
1283
1287
  SZ_DESCRIPTION: "Away mode Exhaust fan rate (%)",
@@ -1285,6 +1289,7 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
1285
1289
  SZ_MAX_VALUE: 0.4,
1286
1290
  SZ_PRECISION: 0.005,
1287
1291
  SZ_DATA_TYPE: "0F",
1292
+ SZ_DATA_UNIT: "%",
1288
1293
  },
1289
1294
  "3F": { # slot 02
1290
1295
  SZ_DESCRIPTION: "Low mode Supply fan rate (%)",
@@ -1292,6 +1297,7 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
1292
1297
  SZ_MAX_VALUE: 0.8,
1293
1298
  SZ_PRECISION: 0.005,
1294
1299
  SZ_DATA_TYPE: "0F",
1300
+ SZ_DATA_UNIT: "%",
1295
1301
  },
1296
1302
  "40": { # slot 03
1297
1303
  SZ_DESCRIPTION: "Low mode Exhaust fan rate (%)",
@@ -1299,13 +1305,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
1299
1305
  SZ_MAX_VALUE: 0.8,
1300
1306
  SZ_PRECISION: 0.005,
1301
1307
  SZ_DATA_TYPE: "0F",
1308
+ SZ_DATA_UNIT: "%",
1302
1309
  },
1303
1310
  "41": { # slot 04
1304
1311
  SZ_DESCRIPTION: "Medium mode Supply fan rate (%)",
1305
- SZ_MIN_VALUE: 0.0,
1312
+ SZ_MIN_VALUE: 0.1, # Orcon FAN responds with 0.0, but I guess this should be the same as for "42"
1306
1313
  SZ_MAX_VALUE: 1.0,
1307
1314
  SZ_PRECISION: 0.005,
1308
1315
  SZ_DATA_TYPE: "0F",
1316
+ SZ_DATA_UNIT: "%",
1309
1317
  },
1310
1318
  "42": { # slot 05
1311
1319
  SZ_DESCRIPTION: "Medium mode Exhaust fan rate (%)",
@@ -1313,13 +1321,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
1313
1321
  SZ_MAX_VALUE: 1.0,
1314
1322
  SZ_PRECISION: 0.005,
1315
1323
  SZ_DATA_TYPE: "0F",
1324
+ SZ_DATA_UNIT: "%",
1316
1325
  },
1317
1326
  "43": { # slot 06
1318
1327
  SZ_DESCRIPTION: "High mode Supply fan rate (%)",
1319
- SZ_MIN_VALUE: 0.0,
1328
+ SZ_MIN_VALUE: 0.1,
1320
1329
  SZ_MAX_VALUE: 1.0,
1321
1330
  SZ_PRECISION: 0.005,
1322
1331
  SZ_DATA_TYPE: "0F",
1332
+ SZ_DATA_UNIT: "%",
1323
1333
  },
1324
1334
  "44": { # slot 07
1325
1335
  SZ_DESCRIPTION: "High mode Exhaust fan rate (%)",
@@ -1327,6 +1337,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
1327
1337
  SZ_MAX_VALUE: 1.0,
1328
1338
  SZ_PRECISION: 0.005,
1329
1339
  SZ_DATA_TYPE: "0F",
1340
+ SZ_DATA_UNIT: "%",
1341
+ },
1342
+ "4B": { # slot 09 (FANs produced before 2021) Also check code 22F7
1343
+ SZ_DESCRIPTION: "(Test) Bypass Valve (0=auto, 1=open, 2=closed)",
1344
+ SZ_MIN_VALUE: 0,
1345
+ SZ_MAX_VALUE: 2,
1346
+ SZ_PRECISION: 1,
1347
+ SZ_DATA_TYPE: "00",
1348
+ SZ_DATA_UNIT: "",
1330
1349
  },
1331
1350
  "4E": { # slot 0A
1332
1351
  SZ_DESCRIPTION: "Moisture scenario position (0=medium, 1=high)",
@@ -1334,13 +1353,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
1334
1353
  SZ_MAX_VALUE: 1,
1335
1354
  SZ_PRECISION: 1,
1336
1355
  SZ_DATA_TYPE: "00",
1356
+ SZ_DATA_UNIT: "",
1337
1357
  },
1338
1358
  "52": { # slot 0B
1339
1359
  SZ_DESCRIPTION: "Sensor sensitivity (%)",
1340
1360
  SZ_MIN_VALUE: 0,
1341
1361
  SZ_MAX_VALUE: 25.0,
1342
1362
  SZ_PRECISION: 0.1,
1343
- SZ_DATA_TYPE: "0F",
1363
+ SZ_DATA_TYPE: "01",
1364
+ SZ_DATA_UNIT: "%",
1344
1365
  },
1345
1366
  "54": { # slot 0C
1346
1367
  SZ_DESCRIPTION: "Moisture sensor overrun time (mins)",
@@ -1348,13 +1369,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
1348
1369
  SZ_MAX_VALUE: 60,
1349
1370
  SZ_PRECISION: 1,
1350
1371
  SZ_DATA_TYPE: "00",
1372
+ SZ_DATA_UNIT: "min",
1351
1373
  },
1352
1374
  "75": { # slot 0D
1353
1375
  SZ_DESCRIPTION: "Comfort temperature (°C)",
1354
1376
  SZ_MIN_VALUE: 0.0,
1355
1377
  SZ_MAX_VALUE: 30.0,
1356
1378
  SZ_PRECISION: 0.01,
1357
- SZ_DATA_TYPE: 92,
1379
+ SZ_DATA_TYPE: "92",
1380
+ SZ_DATA_UNIT: "°C",
1358
1381
  },
1359
1382
  "95": { # slot 08
1360
1383
  SZ_DESCRIPTION: "Boost mode Supply/exhaust fan rate (%)",
@@ -1362,6 +1385,7 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
1362
1385
  SZ_MAX_VALUE: 1.0,
1363
1386
  SZ_PRECISION: 0.005,
1364
1387
  SZ_DATA_TYPE: "0F",
1388
+ SZ_DATA_UNIT: "%",
1365
1389
  },
1366
1390
  }
1367
1391
 
@@ -1456,3 +1480,19 @@ _31DA_FAN_INFO: dict[int, str] = {
1456
1480
  # RAMSES_ZONES_ALL = RAMSES_ZONES.pop("ALL")
1457
1481
  # RAMSES_ZONES_DHW = RAMSES_ZONES[ZON_ROLE.DHW]
1458
1482
  # [RAMSES_ZONES[k].update(RAMSES_ZONES_ALL) for k in RAMSES_ZONES if k != ZON_ROLE.DHW]
1483
+
1484
+ __all__ = [
1485
+ "CODES_BY_DEV_SLUG",
1486
+ "CODES_SCHEMA",
1487
+ "CODE_NAME_LOOKUP",
1488
+ "CODES_BY_DEV_SLUG",
1489
+ "CODES_SCHEMA",
1490
+ "HVAC_KLASS_BY_VC_PAIR",
1491
+ "_2411_PARAMS_SCHEMA",
1492
+ "SZ_DESCRIPTION",
1493
+ "SZ_MIN_VALUE",
1494
+ "SZ_MAX_VALUE",
1495
+ "SZ_PRECISION",
1496
+ "SZ_DATA_TYPE",
1497
+ "SZ_DATA_UNIT",
1498
+ ]
ramses_tx/schemas.py CHANGED
@@ -215,6 +215,7 @@ def ConvertNullToDict() -> Callable[[_T | None], _T | dict[Never, Never]]:
215
215
 
216
216
 
217
217
  SZ_ALIAS: Final = "alias"
218
+ SZ_BOUND_TO: Final = "bound"
218
219
  SZ_CLASS: Final = "class"
219
220
  SZ_FAKED: Final = "faked"
220
221
  SZ_SCHEME: Final = "scheme"
@@ -296,6 +297,8 @@ def sch_global_traits_dict_factory(
296
297
  vol.Optional(SZ_CLASS, default="HVC"): vol.Any(
297
298
  None, *hvac_slugs, *(str(DEV_TYPE_MAP[s]) for s in hvac_slugs)
298
299
  ), # TODO: consider removing None
300
+ # Add 'bound' trait for FAN devices
301
+ vol.Optional(SZ_BOUND_TO): vol.Any(None, vol.Match(DEVICE_ID_REGEX.ANY)),
299
302
  }
300
303
  )
301
304
  SCH_TRAITS_HVAC = SCH_TRAITS_HVAC.extend(
ramses_tx/transport.py CHANGED
@@ -44,7 +44,7 @@ import sys
44
44
  from collections import deque
45
45
  from collections.abc import Awaitable, Callable, Iterable
46
46
  from datetime import datetime as dt, timedelta as td
47
- from functools import wraps
47
+ from functools import partial, wraps
48
48
  from io import TextIOWrapper
49
49
  from string import printable
50
50
  from time import perf_counter
@@ -142,8 +142,7 @@ else: # is linux
142
142
 
143
143
  def list_links(devices: set[str]) -> list[str]:
144
144
  """Search for symlinks to ports already listed in devices."""
145
-
146
- links = []
145
+ links: list[str] = []
147
146
  for device in glob.glob("/dev/*") + glob.glob("/dev/serial/by-id/*"):
148
147
  if os.path.islink(device) and os.path.realpath(device) in devices:
149
148
  links.append(device)
@@ -174,7 +173,7 @@ else: # is linux
174
173
  return result
175
174
 
176
175
 
177
- def is_hgi80(serial_port: SerPortNameT) -> bool | None:
176
+ async def is_hgi80(serial_port: SerPortNameT) -> bool | None:
178
177
  """Return True/False if the device attached to the port has the attrs of an HGI80.
179
178
 
180
179
  Return None if it's not possible to tell (falsy should assume is evofw3).
@@ -209,7 +208,10 @@ def is_hgi80(serial_port: SerPortNameT) -> bool | None:
209
208
 
210
209
  # otherwise, we can look at device attrs via comports()...
211
210
  try:
212
- komports = comports(include_links=True)
211
+ loop = asyncio.get_running_loop()
212
+ komports = await loop.run_in_executor(
213
+ None, partial(comports, include_links=True)
214
+ )
213
215
  except ImportError as err:
214
216
  raise exc.TransportSerialError(f"Unable to find {serial_port}: {err}") from err
215
217
 
@@ -841,8 +843,6 @@ class PortTransport(_RegHackMixin, _FullTransport, _PortTransportAbstractor): #
841
843
  self._leak_sem(), name="PortTransport._leak_sem()"
842
844
  )
843
845
 
844
- self._is_hgi80 = is_hgi80(self.serial.name)
845
-
846
846
  self._loop.create_task(
847
847
  self._create_connection(), name="PortTransport._create_connection()"
848
848
  )
@@ -855,6 +855,8 @@ class PortTransport(_RegHackMixin, _FullTransport, _PortTransportAbstractor): #
855
855
 
856
856
  # signature also serves to discover the HGI's device_id (& for pkt log, if any)
857
857
 
858
+ self._is_hgi80 = await is_hgi80(self.serial.name)
859
+
858
860
  async def connect_sans_signature() -> None:
859
861
  """Call connection_made() without sending/waiting for a signature."""
860
862
 
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.51.7"
3
+ __version__ = "0.51.9"
4
4
  VERSION = __version__
@@ -1,55 +0,0 @@
1
- ramses_cli/__init__.py,sha256=uvGzWqOf4avvgzxJNSLFWEelIWqSZ-AeLAZzg5x58bc,397
2
- ramses_cli/client.py,sha256=vbKS3KVPiGsDWLp5cR3SVBtXrs-TinzlxSbTgcb4G2k,19724
3
- ramses_cli/debug.py,sha256=vgR0lOHoYjWarN948dI617WZZGNuqHbeq6Tc16Da7b4,608
4
- ramses_cli/discovery.py,sha256=81XbmpNiCpUHVZBwo2g1eRwyJG-wZhpSsc44G3hHlFA,12972
5
- ramses_cli/utils/cat_slow.py,sha256=AhUpM5gnegCitNKU-JGHn-DrRzSi-49ZR1Qw6lxe_t8,607
6
- ramses_cli/utils/convert.py,sha256=D_YiCyX5na9pgC-_NhBlW9N1dgRKUK-uLtLBfofjzZM,1804
7
- ramses_rf/__init__.py,sha256=zONFBiRdf07cPTSxzr2V3t-b3CGokZjT9SGit4JUKRA,1055
8
- ramses_rf/binding_fsm.py,sha256=uZAOl3i19KCXqqlaLJWkEqMMP7NJBhVPW3xTikQD1fY,25996
9
- ramses_rf/const.py,sha256=L3z31CZ-xqno6oZp_h-67CB_5tDDqTwSWXsqRtsjMcs,5460
10
- ramses_rf/database.py,sha256=ZZZgucyuU1IHsSewGukZfDg2gu8KeNaEFriWKM0TUHs,10287
11
- ramses_rf/dispatcher.py,sha256=JGkqSi1o-YhQ2rj8tNkXwYLLeJIC7F061xpHoH8sSsM,11201
12
- ramses_rf/entity_base.py,sha256=V9m_Q5SOLP5ko3sok0NDvyz3YdYch1QsxM6tHCIE7cA,39212
13
- ramses_rf/exceptions.py,sha256=rzVZDcYxFH7BjUAQ3U1fHWtgBpwk3BgjX1TO1L1iM8c,2538
14
- ramses_rf/gateway.py,sha256=WdIIGgs87CYfXwSCSVb2YzqOgLC7W4bkpulWQb7PFNw,20564
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=mYOUZOH5OIDNBxRM2vd8POzDWEEmLhxh5UtqjTpFNek,13287
18
- ramses_rf/version.py,sha256=4EfbWfYC4nSL7JCP_xnhFTpV6Yt5Vc4kNosAUW-CKNs,125
19
- ramses_rf/device/__init__.py,sha256=sUbH5dhbYFXSoM_TPFRutpRutBRpup7_cQ9smPtDTy8,4858
20
- ramses_rf/device/base.py,sha256=WGkBTUNjRUEe-phxdtdiXVCZnTi6-i1i_YT6g689UTM,17450
21
- ramses_rf/device/heat.py,sha256=2sCsggySVcuTzyXDmgWy76QbhlU5MQWSejy3zgI5BDE,54242
22
- ramses_rf/device/hvac.py,sha256=H_PUfG_jrrvJgtnu6Bco6PLxHn7CHALwebZzZI1ygFo,23917
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=3jaFEChU-HlWCRMY1y7u09s7AH4hT0pC63hnqwdmZOc,39223
26
- ramses_rf/system/schedule.py,sha256=Ts6tdZPTQLV5NkgwA73tPa5QUsnZNIIuYoKC-8VsXDk,18808
27
- ramses_rf/system/zones.py,sha256=9AH7ooN5QfiqvWuor2P1Dn8aILjQb2RWL9rWqDH1IjA,36075
28
- ramses_tx/__init__.py,sha256=4FsVOzICJ4H80LJ0MknZCN0_V-g0k1nMkHUQ0IdrJW8,3161
29
- ramses_tx/address.py,sha256=5swDr_SvOs1CxBmT-iJpldf8R00mOb7gKPMiEnxLz84,8452
30
- ramses_tx/command.py,sha256=y69y9NYgQHuPbm7h6xC0osf3e1YIKY9jwmsfPiJ8N6U,58348
31
- ramses_tx/const.py,sha256=QmwSS4BIN3ZFrLUiiFScP1RCUHuJ782V3ycRPQTtB_c,30297
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=9lUVh8gAMXNRAolfFw2WuWANjn24AWkmscuM9Tm5imE,22036
35
- ramses_tx/gateway.py,sha256=TXLYwT6tFpmSokD29Qyj1ze7UGCxKidooeyP557Jfoo,11266
36
- ramses_tx/helpers.py,sha256=0VAJ505kpq4K9b9ZeskWI1o2sWwyCbdnKOKZviKFdgY,32913
37
- ramses_tx/logger.py,sha256=qYbUoNPnPaFWKVsYvLG6uTVuPTdZ8HsMzBbGx0DpBqc,10177
38
- ramses_tx/message.py,sha256=hl_gLfwrF79ftUNnsgNt3XGsIhM2Pts0MtZZuGjfaxk,13169
39
- ramses_tx/opentherm.py,sha256=58PXz9l5x8Ou6Fm3y-R_UnGHCYahoi2RKIDdYStUMzk,42378
40
- ramses_tx/packet.py,sha256=NGunaGCkEjhTp9t4mARK5e7kbqT-Z_JKCH7ibMYMJXU,7357
41
- ramses_tx/parsers.py,sha256=PVTbPqcYPUko3BKDaOQoFDwIo4LWAUx5kfRb2KURAMI,109917
42
- ramses_tx/protocol.py,sha256=ifj3qwcQivjQDaQUwM94xp-U8Pmef6zwSH7mav8DLWA,28867
43
- ramses_tx/protocol_fsm.py,sha256=YhHkTqbl8w-myimsOjV50uIFgg9HiApwPU7xA_jg5nU,26827
44
- ramses_tx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- ramses_tx/ramses.py,sha256=9R-JrInORWUNMPklrAPQWwtr_2aaruQmFqQPw5mFkrE,52223
46
- ramses_tx/schemas.py,sha256=h2AcArVROy1_C4n6F0Crj4e-2BxXxH74xogFlc6nKHI,12769
47
- ramses_tx/transport.py,sha256=MwPnkQ0L-2qJt4mIJy3-C9XmHwBDjT7Kg-1LthPByVw,58331
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=hWp2I2S7p1_tlItYBqDNAFpsM7kL_J8xYU-6mC2e_Ws,123
51
- ramses_rf-0.51.7.dist-info/METADATA,sha256=w3QiOIRJncgPc3R3h1MaxjMDOjzm0NYBHFEoeAr6o6A,3909
52
- ramses_rf-0.51.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
- ramses_rf-0.51.7.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
54
- ramses_rf-0.51.7.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
55
- ramses_rf-0.51.7.dist-info/RECORD,,