ramses-rf 0.51.6__py3-none-any.whl → 0.51.7__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/helpers.py CHANGED
@@ -67,7 +67,7 @@ def shrink(
67
67
  """Return a minimized dict, after removing all the meaningless items.
68
68
 
69
69
  Specifically, removes items with:
70
- - uwanted keys (starting with '_')
70
+ - unwanted keys (starting with '_')
71
71
  - falsey values
72
72
  """
73
73
 
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.51.6"
3
+ __version__ = "0.51.7"
4
4
  VERSION = __version__
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ramses_rf
3
- Version: 0.51.6
3
+ Version: 0.51.7
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
@@ -12,10 +12,10 @@ ramses_rf/dispatcher.py,sha256=JGkqSi1o-YhQ2rj8tNkXwYLLeJIC7F061xpHoH8sSsM,11201
12
12
  ramses_rf/entity_base.py,sha256=V9m_Q5SOLP5ko3sok0NDvyz3YdYch1QsxM6tHCIE7cA,39212
13
13
  ramses_rf/exceptions.py,sha256=rzVZDcYxFH7BjUAQ3U1fHWtgBpwk3BgjX1TO1L1iM8c,2538
14
14
  ramses_rf/gateway.py,sha256=WdIIGgs87CYfXwSCSVb2YzqOgLC7W4bkpulWQb7PFNw,20564
15
- ramses_rf/helpers.py,sha256=LcrVLqnF2qJWqXrC7UXKOQE8khCT3OhoTpZ_ZVBjw3A,4249
15
+ ramses_rf/helpers.py,sha256=TNk_QkpIOB3alOp1sqnA9LOzi4fuDCeapNlW3zTzNas,4250
16
16
  ramses_rf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  ramses_rf/schemas.py,sha256=mYOUZOH5OIDNBxRM2vd8POzDWEEmLhxh5UtqjTpFNek,13287
18
- ramses_rf/version.py,sha256=QtONmxsLFzHSx9Z1ddbRf5_x-XAddalTD9dH8-naWpo,125
18
+ ramses_rf/version.py,sha256=4EfbWfYC4nSL7JCP_xnhFTpV6Yt5Vc4kNosAUW-CKNs,125
19
19
  ramses_rf/device/__init__.py,sha256=sUbH5dhbYFXSoM_TPFRutpRutBRpup7_cQ9smPtDTy8,4858
20
20
  ramses_rf/device/base.py,sha256=WGkBTUNjRUEe-phxdtdiXVCZnTi6-i1i_YT6g689UTM,17450
21
21
  ramses_rf/device/heat.py,sha256=2sCsggySVcuTzyXDmgWy76QbhlU5MQWSejy3zgI5BDE,54242
@@ -33,23 +33,23 @@ ramses_tx/exceptions.py,sha256=FJSU9YkvpKjs3yeTqUJX1o3TPFSe_B01gRGIh9b3PNc,2632
33
33
  ramses_tx/fingerprints.py,sha256=nfftA1E62HQnb-eLt2EqjEi_la0DAoT0wt-PtTMie0s,11974
34
34
  ramses_tx/frame.py,sha256=9lUVh8gAMXNRAolfFw2WuWANjn24AWkmscuM9Tm5imE,22036
35
35
  ramses_tx/gateway.py,sha256=TXLYwT6tFpmSokD29Qyj1ze7UGCxKidooeyP557Jfoo,11266
36
- ramses_tx/helpers.py,sha256=0zIvMY2zZ5V52fDAyVGXxNBND1HI-9fBN3dlgDNKNyo,33040
37
- ramses_tx/logger.py,sha256=9xTxKVdZEoNZvvbjKSeKtMmlSzLhocTRW-61O6WpVsk,11397
36
+ ramses_tx/helpers.py,sha256=0VAJ505kpq4K9b9ZeskWI1o2sWwyCbdnKOKZviKFdgY,32913
37
+ ramses_tx/logger.py,sha256=qYbUoNPnPaFWKVsYvLG6uTVuPTdZ8HsMzBbGx0DpBqc,10177
38
38
  ramses_tx/message.py,sha256=hl_gLfwrF79ftUNnsgNt3XGsIhM2Pts0MtZZuGjfaxk,13169
39
39
  ramses_tx/opentherm.py,sha256=58PXz9l5x8Ou6Fm3y-R_UnGHCYahoi2RKIDdYStUMzk,42378
40
40
  ramses_tx/packet.py,sha256=NGunaGCkEjhTp9t4mARK5e7kbqT-Z_JKCH7ibMYMJXU,7357
41
- ramses_tx/parsers.py,sha256=ngKmZNFPp4k-HmpOtX8_zWnrjM4O68OzjqAOEMznHFE,109863
41
+ ramses_tx/parsers.py,sha256=PVTbPqcYPUko3BKDaOQoFDwIo4LWAUx5kfRb2KURAMI,109917
42
42
  ramses_tx/protocol.py,sha256=ifj3qwcQivjQDaQUwM94xp-U8Pmef6zwSH7mav8DLWA,28867
43
43
  ramses_tx/protocol_fsm.py,sha256=YhHkTqbl8w-myimsOjV50uIFgg9HiApwPU7xA_jg5nU,26827
44
44
  ramses_tx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  ramses_tx/ramses.py,sha256=9R-JrInORWUNMPklrAPQWwtr_2aaruQmFqQPw5mFkrE,52223
46
46
  ramses_tx/schemas.py,sha256=h2AcArVROy1_C4n6F0Crj4e-2BxXxH74xogFlc6nKHI,12769
47
- ramses_tx/transport.py,sha256=aLpULRSivoJqzH8GDPRDcbehETOhFflEqmHbaniGLvg,56210
47
+ ramses_tx/transport.py,sha256=MwPnkQ0L-2qJt4mIJy3-C9XmHwBDjT7Kg-1LthPByVw,58331
48
48
  ramses_tx/typed_dicts.py,sha256=w-0V5t2Q3GiNUOrRAWiW9GtSwbta_7luME6GfIb1zhI,10869
49
49
  ramses_tx/typing.py,sha256=eF2SlPWhNhEFQj6WX2AhTXiyRQVXYnFutiepllYl2rI,5042
50
- ramses_tx/version.py,sha256=Jvb0klvS67N7SxexepgSmtxSwb8sKcIR8MBBeUXTfSM,123
51
- ramses_rf-0.51.6.dist-info/METADATA,sha256=OzEjMdl75qcH3T8URoBIKgSNQKAualdYRRD8giwa034,3909
52
- ramses_rf-0.51.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
- ramses_rf-0.51.6.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
54
- ramses_rf-0.51.6.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
55
- ramses_rf-0.51.6.dist-info/RECORD,,
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,,
ramses_tx/helpers.py CHANGED
@@ -470,7 +470,7 @@ def parse_valve_demand(
470
470
  if int(value, 16) & 0xF0 == 0xF0:
471
471
  return _faulted_device(SZ_HEAT_DEMAND, value)
472
472
 
473
- result = int(value, 16) / 200 # c.f. hex_to_percentage
473
+ result = int(value, 16) / 200 # c.f. hex_to_percent
474
474
  if result == 1.01: # HACK - does it mean maximum?
475
475
  result = 1.0
476
476
  elif result > 1.0:
@@ -606,12 +606,9 @@ def _parse_hvac_humidity(
606
606
  if int(value, 16) & 0xF0 == 0xF0:
607
607
  return _faulted_sensor(param_name, value)
608
608
 
609
- percentage = int(value, 16) / 100 # TODO: confirm not 200
610
- assert percentage <= 1.0, value # TODO: raise exception if > 1.0?
609
+ percentage = hex_to_percent(value, False) # TODO: confirm not /200
611
610
 
612
- result: dict[str, float | str | None] = {
613
- param_name: percentage
614
- } # was: percent_from_hex(value, high_res=False)
611
+ result: dict[str, float | str | None] = {param_name: percentage}
615
612
  if temp:
616
613
  result |= {SZ_TEMPERATURE: hex_to_temp(temp)}
617
614
  if dewpoint:
@@ -881,7 +878,7 @@ def _parse_fan_heater(param_name: str, value: HexStr2) -> Mapping[str, float | N
881
878
  if int(value, 16) & 0xF0 == 0xF0:
882
879
  return _faulted_sensor(param_name, value) # type: ignore[return-value]
883
880
 
884
- percentage = int(value, 16) / 200 # Siber DF EVO 2 is /200, not /100 (?Others)
881
+ percentage = int(value, 16) / 200 # Siber DF EVO 2 is /200, not /100 (Others?)
885
882
  assert percentage <= 1.0, value # TODO: raise exception if > 1.0?
886
883
 
887
884
  return {param_name: percentage} # was: percent_from_hex(value, high_res=False)
ramses_tx/logger.py CHANGED
@@ -173,33 +173,6 @@ class TimedRotatingFileHandler(_TimedRotatingFileHandler):
173
173
  # self.doRollover()
174
174
  # return super().emit(record)
175
175
 
176
- # To fix issue ramses_cc 293, test if this override is still required
177
- # async def getFilesToDelete(self) -> list[str]: # zxdavb: my version
178
- # """Determine the files to delete when rolling over.
179
- #
180
- # Overridden as old log files were not being deleted.
181
- # """
182
- # # See bpo-44753 (this code is as was before that commit), bpo45628, bpo-46063
183
- # dirName, baseName = os.path.split(self.baseFilename)
184
- # loop = asyncio.get_running_loop()
185
- # # Must run async in executor to prevent HA blocking call on rollover (ramses_cc issue 293)
186
- # file_names = await loop.run_in_executor(None, os.listdir, dirName) < doesn't work
187
- #
188
- # result = []
189
- # prefix = baseName + "."
190
- # plen = len(prefix)
191
- # for fileName in file_names:
192
- # if fileName[:plen] == prefix:
193
- # suffix = fileName[plen:]
194
- # if self.extMatch.match(suffix):
195
- # result.append(os.path.join(dirName, fileName))
196
- # if len(result) < self.backupCount:
197
- # result = []
198
- # else:
199
- # result.sort()
200
- # result = result[: len(result) - self.backupCount]
201
- # return result
202
-
203
176
 
204
177
  def getLogger( # permits a bespoke Logger class
205
178
  name: str | None = None, pkt_log: bool = False
ramses_tx/parsers.py CHANGED
@@ -1648,21 +1648,17 @@ def parser_22f3(payload: str, msg: Message) -> dict[str, Any]:
1648
1648
  _LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
1649
1649
 
1650
1650
  new_speed = { # from now, until timer expiry
1651
- 0x00: "fan_boost", # # set fan off, or 'boost' mode?
1652
- 0x01: "per_request", # # set fan as per payload[6:10]?
1653
- 0x02: "per_vent_speed", # set fan as per current fan mode/speed?
1651
+ 0x00: "fan_boost", # set fan off, or 'boost' mode?
1652
+ 0x01: "per_request?", # set fan as per payload[6:10]?
1653
+ 0x02: "per_request", # set fan as per payload[6:10]
1654
1654
  }.get(int(payload[2:4], 0x10) & 0x07) # 0b0000-0111
1655
1655
 
1656
- fallback_speed: str | None
1657
- if msg.len == 7 and payload[9:10] == "06": # Vasco and ClimaRad REM
1658
- fallback_speed = "per_vent_speed" # after timer expiry
1659
- # set fan as per current fan mode/speed
1660
- else:
1661
- fallback_speed = { # after timer expiry
1662
- 0x08: "fan_off", # # set fan off?
1663
- 0x10: "per_request", # # set fan as per payload[6:10], or payload[10:]?
1664
- 0x18: "per_vent_speed", # set fan as per current fan mode/speed?
1665
- }.get(int(payload[2:4], 0x10) & 0x38) # 0b0011-1000
1656
+ fallback_speed = { # after timer expiry
1657
+ 0x00: "per_vent_speed", # set fan as per current fan mode
1658
+ 0x08: "fan_off", # set fan off?
1659
+ 0x10: "per_request", # set fan as per payload[10:14]
1660
+ 0x18: "per_vent_speed?", # set fan as per current fan mode/speed?
1661
+ }.get(int(payload[2:4], 0x10) & 0x38) # 0b0011-1000
1666
1662
 
1667
1663
  units = {
1668
1664
  0x00: "minutes",
@@ -1677,15 +1673,21 @@ def parser_22f3(payload: str, msg: Message) -> dict[str, Any]:
1677
1673
  result = {
1678
1674
  "minutes" if units != "index" else "index": duration,
1679
1675
  "flags": hex_to_flag8(payload[2:4]),
1680
- "_new_speed_mode": new_speed,
1681
- "_fallback_speed_mode": fallback_speed,
1676
+ "new_speed_mode": new_speed,
1677
+ "fallback_speed_mode": fallback_speed,
1682
1678
  }
1683
1679
 
1684
- if msg.len >= 5 and payload[6:10] != "0000": # new speed?
1685
- result["rate"] = parser_22f1(f"00{payload[6:10]}", msg).get("rate")
1680
+ if msg._addrs[0] == NON_DEV_ADDR and msg.len <= 3:
1681
+ result["_scheme"] = "itho"
1682
+
1683
+ if msg.len >= 5 and payload[6:10] != "0000": # new speed
1684
+ mode_info = parser_22f1(f"00{payload[6:10]}", msg)
1685
+ result["_scheme"] = mode_info.get("_scheme")
1686
+ result["fan_mode"] = mode_info.get("fan_mode")
1686
1687
 
1687
- if msg.len >= 7: # fallback speed?
1688
- result.update({"_unknown_5": payload[10:]})
1688
+ if msg.len >= 7 and payload[10:14] != "0000": # fallback speed
1689
+ mode_info = parser_22f1(f"00{payload[10:14]}", msg)
1690
+ result["fallback_fan_mode"] = mode_info.get("fan_mode")
1689
1691
 
1690
1692
  return result
1691
1693
 
ramses_tx/transport.py CHANGED
@@ -1033,6 +1033,8 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
1033
1033
  self._topic_base = validate_topic_path(self._broker_url.path)
1034
1034
  self._topic_pub = ""
1035
1035
  self._topic_sub = ""
1036
+ # Track if we've subscribed to a wildcard data topic (e.g. ".../+/rx")
1037
+ self._data_wildcard_topic = ""
1036
1038
 
1037
1039
  self._mqtt_qos = int(parse_qs(self._broker_url.query).get("qos", ["0"])[0])
1038
1040
 
@@ -1143,17 +1145,30 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
1143
1145
  # Subscribe to base topic to see 'online' messages
1144
1146
  self.client.subscribe(self._topic_base) # hope to see 'online' message
1145
1147
 
1146
- # Also subscribe to data topics with wildcard for reliability after reconnect
1147
- # This ensures we get data even if we miss the 'online' message
1148
- if self._topic_base.endswith("/+"):
1148
+ # Also subscribe to data topics with wildcard for reliability, but only
1149
+ # until a specific device topic is known. Once _topic_sub is set, avoid
1150
+ # overlapping subscriptions that would duplicate messages.
1151
+ if self._topic_base.endswith("/+") and not (
1152
+ hasattr(self, "_topic_sub") and self._topic_sub
1153
+ ):
1149
1154
  data_wildcard = self._topic_base.replace("/+", "/+/rx")
1150
1155
  self.client.subscribe(data_wildcard, qos=self._mqtt_qos)
1156
+ self._data_wildcard_topic = data_wildcard
1151
1157
  _LOGGER.debug(f"Subscribed to data wildcard: {data_wildcard}")
1152
1158
 
1153
1159
  # If we already have specific topics, re-subscribe to them
1154
1160
  if hasattr(self, "_topic_sub") and self._topic_sub:
1155
1161
  self.client.subscribe(self._topic_sub, qos=self._mqtt_qos)
1156
1162
  _LOGGER.debug(f"Re-subscribed to specific topic: {self._topic_sub}")
1163
+ # If we had a wildcard subscription, drop it to prevent duplicates
1164
+ if getattr(self, "_data_wildcard_topic", ""):
1165
+ try:
1166
+ self.client.unsubscribe(self._data_wildcard_topic)
1167
+ _LOGGER.debug(
1168
+ f"Unsubscribed data wildcard after specific subscribe: {self._data_wildcard_topic}"
1169
+ )
1170
+ finally:
1171
+ self._data_wildcard_topic = ""
1157
1172
 
1158
1173
  def _on_connect_fail(
1159
1174
  self,
@@ -1225,6 +1240,17 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
1225
1240
 
1226
1241
  self.client.subscribe(self._topic_sub, qos=self._mqtt_qos)
1227
1242
 
1243
+ # If we previously subscribed to a wildcard data topic, unsubscribe now
1244
+ # to avoid duplicate delivery (wildcard and specific both matching)
1245
+ if getattr(self, "_data_wildcard_topic", ""):
1246
+ try:
1247
+ self.client.unsubscribe(self._data_wildcard_topic)
1248
+ _LOGGER.debug(
1249
+ f"Unsubscribed data wildcard after device online: {self._data_wildcard_topic}"
1250
+ )
1251
+ finally:
1252
+ self._data_wildcard_topic = ""
1253
+
1228
1254
  # Only call connection_made on first connection, not reconnections
1229
1255
  if not self._connection_established:
1230
1256
  self._connection_established = True
@@ -1295,6 +1321,21 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
1295
1321
  self._connection_established = True
1296
1322
  self._make_connection(gwy_id=gateway_id) # type: ignore[arg-type]
1297
1323
 
1324
+ # Ensure we subscribe specifically to the device topic and drop the
1325
+ # wildcard subscription to prevent duplicates
1326
+ try:
1327
+ self.client.subscribe(self._topic_sub, qos=self._mqtt_qos)
1328
+ except Exception as err: # pragma: no cover - defensive
1329
+ _LOGGER.debug(f"Error subscribing specific topic: {err}")
1330
+ if getattr(self, "_data_wildcard_topic", ""):
1331
+ try:
1332
+ self.client.unsubscribe(self._data_wildcard_topic)
1333
+ _LOGGER.debug(
1334
+ f"Unsubscribed data wildcard after inferring device: {self._data_wildcard_topic}"
1335
+ )
1336
+ finally:
1337
+ self._data_wildcard_topic = ""
1338
+
1298
1339
  try:
1299
1340
  payload = json.loads(msg.payload)
1300
1341
  except json.JSONDecodeError:
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.6"
3
+ __version__ = "0.51.7"
4
4
  VERSION = __version__