ramses-rf 0.51.3__py3-none-any.whl → 0.51.4__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/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.3"
3
+ __version__ = "0.51.4"
4
4
  VERSION = __version__
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ramses_rf
3
- Version: 0.51.3
3
+ Version: 0.51.4
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
@@ -15,7 +15,7 @@ ramses_rf/gateway.py,sha256=vqoTEb6QXnwaIMa66oed_3LEVvlyQ3flsAAMliEEvVA,20921
15
15
  ramses_rf/helpers.py,sha256=LcrVLqnF2qJWqXrC7UXKOQE8khCT3OhoTpZ_ZVBjw3A,4249
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=H7dGP9Kn7kENpYvpC0mlcipijKfHZqlzlTauaoFe_8k,125
18
+ ramses_rf/version.py,sha256=zEZhrTC1gMQzcGZ3ZSkZrhF_OBj8Dnlv5VhehgnT3_s,125
19
19
  ramses_rf/device/__init__.py,sha256=sUbH5dhbYFXSoM_TPFRutpRutBRpup7_cQ9smPtDTy8,4858
20
20
  ramses_rf/device/base.py,sha256=V2YzRhdxrTqfHYrCBq6pJsYdTgAx8gGzfdo8pkbeEo8,17450
21
21
  ramses_rf/device/heat.py,sha256=2sCsggySVcuTzyXDmgWy76QbhlU5MQWSejy3zgI5BDE,54242
@@ -38,18 +38,18 @@ ramses_tx/logger.py,sha256=7vUpcfOFMW95juMWDx5dhUOqV8DTsindZ-Qz2aCmEoA,11073
38
38
  ramses_tx/message.py,sha256=J1wvVkLPJQr2ffKCUQYSWwLPzRTZBC0zUU5W9DkN3hU,13190
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=R-oFcRUe7HPsm9n4196hFUD1tULbFdKBAWo8HvzGyjw,109218
41
+ ramses_tx/parsers.py,sha256=eU5dqbbw1vzWDFxDhyNPy2j6t_LQN56mRJa0A-PeKiE,109411
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
- ramses_tx/ramses.py,sha256=yPSdhxGRYQ9AuTWoKs9Cb3YQn5YnEKZVs7BYkQFmqCw,52037
45
+ ramses_tx/ramses.py,sha256=GnZwvx-HSVFdjXfUen6aWClDtrAmYaKwbrWl-LsyKO4,52045
46
46
  ramses_tx/schemas.py,sha256=h2AcArVROy1_C4n6F0Crj4e-2BxXxH74xogFlc6nKHI,12769
47
- ramses_tx/transport.py,sha256=28CtiqNltcJhLr4VIHCW9uCohWXrvxmx6ySUSIuRQ9c,52892
47
+ ramses_tx/transport.py,sha256=-IO8UY85OOytciX3h7tFN58BBDtI3TEoOgmUmv-LiNc,56288
48
48
  ramses_tx/typed_dicts.py,sha256=4ZT50M-Cuwy2ljAIorwoxEJ9c737xUHrUxX9wTh79xE,10834
49
49
  ramses_tx/typing.py,sha256=eF2SlPWhNhEFQj6WX2AhTXiyRQVXYnFutiepllYl2rI,5042
50
- ramses_tx/version.py,sha256=EK6Cr3XBdGkYKI-L4TW-S8rBw478PNtR_ckrbfQTCYM,123
51
- ramses_rf-0.51.3.dist-info/METADATA,sha256=eLSMlCmKZZ66ImYx16vDtejxWCYIkWFfYRc5ZYqZom8,3906
52
- ramses_rf-0.51.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
- ramses_rf-0.51.3.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
54
- ramses_rf-0.51.3.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
55
- ramses_rf-0.51.3.dist-info/RECORD,,
50
+ ramses_tx/version.py,sha256=QqJQBBdWjuoIvxrhzIH4ysjOp5bzB0KI1TdFhr9c6og,123
51
+ ramses_rf-0.51.4.dist-info/METADATA,sha256=odaoeguG-xZAlWq8nYF4PHVK3SV5mzJRi1QdD4IBKcE,3906
52
+ ramses_rf-0.51.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
+ ramses_rf-0.51.4.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
54
+ ramses_rf-0.51.4.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
55
+ ramses_rf-0.51.4.dist-info/RECORD,,
ramses_tx/parsers.py CHANGED
@@ -1382,18 +1382,27 @@ def parser_1fd4(payload: str, msg: Message) -> PayDictT._1FD4:
1382
1382
  def parser_2210(payload: str, msg: Message) -> dict[str, Any]:
1383
1383
  try:
1384
1384
  assert msg.verb in (RP, I_) or payload == "00"
1385
- assert payload[10:12] == payload[38:40] and payload[10:12] in (
1385
+ assert payload[10:12] == payload[38:40] and payload[
1386
+ 10:12
1387
+ ] in ( # auto requested fan speed step?
1386
1388
  "58",
1389
+ "64",
1387
1390
  "96",
1388
1391
  "FF",
1389
- ), f"expected (58|96|FF), not {payload[10:12]}"
1392
+ ), f"expected req.speed? (58|64|96|FF), not {payload[10:12]}"
1390
1393
  assert payload[20:22] == payload[48:50] and payload[20:22] in (
1391
1394
  "00",
1392
1395
  "03",
1393
- ), f"expected (00|03), not {payload[10:12]}"
1394
- assert payload[78:80] in ("00", "02"), f"expected (00|02), not {payload[78:80]}"
1395
- assert payload[80:82] in ("01", "08"), f"expected (01|08), not {payload[80:82]}"
1396
- assert payload[82:] in ("00", "40"), f"expected (00|40), not {payload[82:]}"
1396
+ ), f"expected byte 10 (00|03), not {payload[20:22]}"
1397
+ assert payload[78:80] in ("00", "02"), (
1398
+ f"expected byte 39 (00|02), not {payload[78:80]}"
1399
+ )
1400
+ assert payload[80:82] in ("01", "08"), (
1401
+ f"expected byte 40 (01|08), not {payload[80:82]}"
1402
+ )
1403
+ assert payload[82:] in ("00", "40"), (
1404
+ f"expected byte 41- (00|40), not {payload[82:]}"
1405
+ )
1397
1406
 
1398
1407
  except AssertionError as err:
1399
1408
  _LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
ramses_tx/ramses.py CHANGED
@@ -1059,7 +1059,7 @@ _DEV_KLASSES_HVAC: dict[str, dict[Code, dict[VerbT, Any]]] = {
1059
1059
  Code._12C8: {I_: {}},
1060
1060
  Code._1470: {RP: {}},
1061
1061
  Code._1F09: {I_: {}, RP: {}},
1062
- Code._1FC9: {W_: {}},
1062
+ Code._1FC9: {I_: {}, W_: {}},
1063
1063
  Code._2210: {I_: {}, RP: {}},
1064
1064
  Code._22E0: {RP: {}},
1065
1065
  Code._22E5: {RP: {}},
ramses_tx/transport.py CHANGED
@@ -778,7 +778,8 @@ class FileTransport(_ReadTransport, _FileTransportAbstractor):
778
778
  while not self._reading:
779
779
  await asyncio.sleep(0.001)
780
780
  self._frame_read(dtm_str, pkt_line)
781
- # await asyncio.sleep(0) # NOTE: big performance penalty if delay >0
781
+ await asyncio.sleep(0)
782
+ # NOTE: instable without, big performance penalty if delay >0
782
783
 
783
784
  elif isinstance(self._pkt_source, str): # file_name, used in client parse
784
785
  # open file file_name before reading
@@ -794,7 +795,8 @@ class FileTransport(_ReadTransport, _FileTransportAbstractor):
794
795
  ] != "#":
795
796
  self._frame_read(dtm_pkt_line[:26], dtm_pkt_line[27:])
796
797
  # this is where the parsing magic happens!
797
- # await asyncio.sleep(0) # NOTE: big performance penalty if delay >0
798
+ await asyncio.sleep(0)
799
+ # NOTE: instable without, big performance penalty if delay >0
798
800
  except FileNotFoundError as err:
799
801
  _LOGGER.warning(f"Correct the packet file name; {err}")
800
802
  elif isinstance(self._pkt_source, TextIOWrapper): # used by client monitor
@@ -804,7 +806,8 @@ class FileTransport(_ReadTransport, _FileTransportAbstractor):
804
806
  # can be blank lines in annotated log files
805
807
  if (dtm_pkt_line := dtm_pkt_line.strip()) and dtm_pkt_line[:1] != "#":
806
808
  self._frame_read(dtm_pkt_line[:26], dtm_pkt_line[27:])
807
- await asyncio.sleep(0) # NOTE: big performance penalty if delay >0
809
+ await asyncio.sleep(0)
810
+ # NOTE: instable without, big performance penalty if delay >0
808
811
  else:
809
812
  raise exc.TransportSourceInvalid(
810
813
  f"Packet source is not dict, TextIOWrapper or str: {self._pkt_source:!r}"
@@ -1035,6 +1038,7 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
1035
1038
 
1036
1039
  self._connected = False
1037
1040
  self._connecting = False
1041
+ self._connection_established = False # Track if initial connection was made
1038
1042
  self._extra[SZ_IS_EVOFW3] = True
1039
1043
 
1040
1044
  # Reconnection settings
@@ -1136,8 +1140,21 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
1136
1140
  self._reconnect_task.cancel()
1137
1141
  self._reconnect_task = None
1138
1142
 
1143
+ # Subscribe to base topic to see 'online' messages
1139
1144
  self.client.subscribe(self._topic_base) # hope to see 'online' message
1140
1145
 
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("/+"):
1149
+ data_wildcard = self._topic_base.replace("/+", "/+/rx")
1150
+ self.client.subscribe(data_wildcard, qos=self._mqtt_qos)
1151
+ _LOGGER.debug(f"Subscribed to data wildcard: {data_wildcard}")
1152
+
1153
+ # If we already have specific topics, re-subscribe to them
1154
+ if hasattr(self, "_topic_sub") and self._topic_sub:
1155
+ self.client.subscribe(self._topic_sub, qos=self._mqtt_qos)
1156
+ _LOGGER.debug(f"Re-subscribed to specific topic: {self._topic_sub}")
1157
+
1141
1158
  def _on_connect_fail(
1142
1159
  self,
1143
1160
  client: mqtt.Client,
@@ -1160,14 +1177,27 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
1160
1177
  ) -> None:
1161
1178
  _LOGGER.warning(f"MQTT disconnected: {reason_code.getName()}")
1162
1179
 
1180
+ was_connected = self._connected
1163
1181
  self._connected = False
1164
1182
 
1183
+ # If we were previously connected and had established communication,
1184
+ # notify that the device is now offline
1185
+ if was_connected and hasattr(self, "_topic_sub") and self._topic_sub:
1186
+ device_topic = self._topic_sub[:-3] # Remove "/rx" suffix
1187
+ _LOGGER.warning(f"{self}: the MQTT device is offline: {device_topic}")
1188
+
1189
+ # Pause writing since device is offline
1190
+ if hasattr(self, "_protocol"):
1191
+ self._protocol.pause_writing()
1192
+
1165
1193
  # Only attempt reconnection if we didn't deliberately disconnect
1166
1194
  if not self._closing and not reason_code.is_failure:
1167
1195
  # This was an unexpected disconnect, schedule reconnection
1196
+ _LOGGER.debug("MQTT unexpected disconnect - scheduling reconnection")
1168
1197
  self._schedule_reconnect()
1169
1198
  elif reason_code.is_failure and not self._closing:
1170
1199
  # Connection failed, also schedule reconnection
1200
+ _LOGGER.debug("MQTT connection failed - scheduling reconnection")
1171
1201
  self._schedule_reconnect()
1172
1202
 
1173
1203
  def _create_connection(self, msg: mqtt.MQTTMessage) -> None:
@@ -1191,7 +1221,12 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
1191
1221
 
1192
1222
  self.client.subscribe(self._topic_sub, qos=self._mqtt_qos)
1193
1223
 
1194
- self._make_connection(gwy_id=msg.topic[-9:]) # type: ignore[arg-type]
1224
+ # Only call connection_made on first connection, not reconnections
1225
+ if not self._connection_established:
1226
+ self._connection_established = True
1227
+ self._make_connection(gwy_id=msg.topic[-9:]) # type: ignore[arg-type]
1228
+ else:
1229
+ _LOGGER.info("MQTT reconnected - protocol connection already established")
1195
1230
 
1196
1231
  # NOTE: self._frame_read() invoked from here
1197
1232
  def _on_message(
@@ -1211,23 +1246,51 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
1211
1246
  _LOGGER.info("Rx: %s", msg.payload)
1212
1247
 
1213
1248
  if msg.topic[-3:] != "/rx": # then, e.g. 'RAMSES/GATEWAY/18:017804'
1214
- if msg.payload == b"offline" and self._topic_sub.startswith(msg.topic):
1215
- _LOGGER.warning(
1216
- f"{self}: the MQTT device is offline: {self._topic_sub[:-3]}"
1217
- )
1218
- self._connected = False
1219
- self._protocol.pause_writing()
1249
+ if msg.payload == b"offline":
1250
+ # Check if this offline message is for our current device
1251
+ if (
1252
+ hasattr(self, "_topic_sub")
1253
+ and self._topic_sub
1254
+ and msg.topic == self._topic_sub[:-3]
1255
+ ) or not hasattr(self, "_topic_sub"):
1256
+ _LOGGER.warning(
1257
+ f"{self}: the ESP device is offline (via LWT): {msg.topic}"
1258
+ )
1259
+ # Don't set _connected = False here - that's for MQTT connection, not ESP device
1260
+ if hasattr(self, "_protocol"):
1261
+ self._protocol.pause_writing()
1220
1262
 
1221
1263
  # BUG: using create task (self._loop.ct() & asyncio.ct()) causes the
1222
1264
  # BUG: event look to close early
1223
1265
  elif msg.payload == b"online":
1224
1266
  _LOGGER.info(
1225
- f"{self}: the MQTT device is online: {self._topic_sub[:-3]}"
1267
+ f"{self}: the ESP device is online (via status): {msg.topic}"
1226
1268
  )
1227
1269
  self._create_connection(msg)
1228
1270
 
1229
1271
  return
1230
1272
 
1273
+ # Handle data messages - if we don't have connection established yet but get data,
1274
+ # we can infer the gateway from the topic
1275
+ if not self._connection_established and msg.topic.endswith("/rx"):
1276
+ # Extract gateway ID from topic like "RAMSES/GATEWAY/18:123456/rx"
1277
+ topic_parts = msg.topic.split("/")
1278
+ if len(topic_parts) >= 3 and topic_parts[-2] not in ("+", "*"):
1279
+ gateway_id = topic_parts[-2] # Should be something like "18:123456"
1280
+ _LOGGER.info(
1281
+ f"Inferring gateway connection from data topic: {gateway_id}"
1282
+ )
1283
+
1284
+ # Set up topics and connection
1285
+ self._topic_pub = f"{'/'.join(topic_parts[:-1])}/tx"
1286
+ self._topic_sub = msg.topic
1287
+ self._extra[SZ_ACTIVE_HGI] = gateway_id
1288
+
1289
+ # Mark as connected and establish protocol connection
1290
+ self._connected = True
1291
+ self._connection_established = True
1292
+ self._make_connection(gwy_id=gateway_id) # type: ignore[arg-type]
1293
+
1231
1294
  try:
1232
1295
  payload = json.loads(msg.payload)
1233
1296
  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.3"
3
+ __version__ = "0.51.4"
4
4
  VERSION = __version__