python-roborock 5.20.0__tar.gz → 5.20.1__tar.gz

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.
Files changed (108) hide show
  1. {python_roborock-5.20.0 → python_roborock-5.20.1}/PKG-INFO +1 -1
  2. {python_roborock-5.20.0 → python_roborock-5.20.1}/pyproject.toml +1 -1
  3. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/device.py +10 -0
  4. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/rpc/v1_channel.py +67 -37
  5. {python_roborock-5.20.0 → python_roborock-5.20.1}/.gitignore +0 -0
  6. {python_roborock-5.20.0 → python_roborock-5.20.1}/LICENSE +0 -0
  7. {python_roborock-5.20.0 → python_roborock-5.20.1}/README.md +0 -0
  8. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/__init__.py +0 -0
  9. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/broadcast_protocol.py +0 -0
  10. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/callbacks.py +0 -0
  11. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/cli.py +0 -0
  12. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/const.py +0 -0
  13. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/__init__.py +0 -0
  14. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/b01_q10/__init__.py +0 -0
  15. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  16. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  17. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/b01_q7/__init__.py +0 -0
  18. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  19. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  20. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/code_mappings.py +0 -0
  21. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/containers.py +0 -0
  22. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/dyad/__init__.py +0 -0
  23. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  24. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/dyad/dyad_containers.py +0 -0
  25. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/v1/__init__.py +0 -0
  26. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/v1/v1_clean_modes.py +0 -0
  27. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/v1/v1_code_mappings.py +0 -0
  28. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/v1/v1_containers.py +0 -0
  29. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/zeo/__init__.py +0 -0
  30. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  31. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/data/zeo/zeo_containers.py +0 -0
  32. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/device_features.py +0 -0
  33. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/README.md +0 -0
  34. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/__init__.py +0 -0
  35. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/cache.py +0 -0
  36. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/device_manager.py +0 -0
  37. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/file_cache.py +0 -0
  38. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/rpc/__init__.py +0 -0
  39. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/rpc/a01_channel.py +0 -0
  40. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/rpc/b01_q10_channel.py +0 -0
  41. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/rpc/b01_q7_channel.py +0 -0
  42. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/__init__.py +0 -0
  43. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/a01/__init__.py +0 -0
  44. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/b01/__init__.py +0 -0
  45. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  46. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/b01/q10/command.py +0 -0
  47. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/b01/q10/map.py +0 -0
  48. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/b01/q10/remote.py +0 -0
  49. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/b01/q10/status.py +0 -0
  50. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
  51. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/b01/q7/__init__.py +0 -0
  52. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
  53. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/b01/q7/map.py +0 -0
  54. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/b01/q7/map_content.py +0 -0
  55. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/common.py +0 -0
  56. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/traits_mixin.py +0 -0
  57. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/__init__.py +0 -0
  58. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/child_lock.py +0 -0
  59. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/clean_summary.py +0 -0
  60. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/command.py +0 -0
  61. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/common.py +0 -0
  62. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/consumeable.py +0 -0
  63. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/device_features.py +0 -0
  64. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  65. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  66. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  67. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/home.py +0 -0
  68. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/led_status.py +0 -0
  69. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/map_content.py +0 -0
  70. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/maps.py +0 -0
  71. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/network_info.py +0 -0
  72. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/rooms.py +0 -0
  73. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/routines.py +0 -0
  74. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  75. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/status.py +0 -0
  76. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  77. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/volume.py +0 -0
  78. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  79. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/transport/__init__.py +0 -0
  80. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/transport/channel.py +0 -0
  81. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/transport/local_channel.py +0 -0
  82. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/devices/transport/mqtt_channel.py +0 -0
  83. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/diagnostics.py +0 -0
  84. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/exceptions.py +0 -0
  85. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/map/__init__.py +0 -0
  86. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/map/b01_grid_layers.py +0 -0
  87. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/map/b01_map_parser.py +0 -0
  88. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/map/b01_q10_map_parser.py +0 -0
  89. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/map/b01_q10_overlays.py +0 -0
  90. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/map/map_parser.py +0 -0
  91. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/map/proto/__init__.py +0 -0
  92. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/map/proto/b01_scmap.proto +0 -0
  93. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/map/proto/b01_scmap_pb2.py +0 -0
  94. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/mqtt/__init__.py +0 -0
  95. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/mqtt/health_manager.py +0 -0
  96. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/mqtt/roborock_session.py +0 -0
  97. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/mqtt/session.py +0 -0
  98. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/protocol.py +0 -0
  99. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/protocols/__init__.py +0 -0
  100. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/protocols/a01_protocol.py +0 -0
  101. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/protocols/b01_q10_protocol.py +0 -0
  102. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/protocols/b01_q7_protocol.py +0 -0
  103. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/protocols/v1_protocol.py +0 -0
  104. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/py.typed +0 -0
  105. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/roborock_message.py +0 -0
  106. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/roborock_typing.py +0 -0
  107. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/util.py +0 -0
  108. {python_roborock-5.20.0 → python_roborock-5.20.1}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 5.20.0
3
+ Version: 5.20.1
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Project-URL: Repository, https://github.com/python-roborock/python-roborock
6
6
  Project-URL: Documentation, https://python-roborock.readthedocs.io/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-roborock"
3
- version = "5.20.0"
3
+ version = "5.20.1"
4
4
  description = "A package to control Roborock vacuums."
5
5
  authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
6
6
  requires-python = ">=3.11, <4"
@@ -203,6 +203,16 @@ class RoborockDevice(ABC, TraitsMixin):
203
203
  elif self.b01_q10_properties is not None:
204
204
  await self.b01_q10_properties.start()
205
205
  except RoborockException:
206
+ # Expected: start() can fail transiently. Unsubscribe before propagating
207
+ # so the retry by connect_loop() gets a clean channel.
208
+ unsub()
209
+ raise
210
+ except Exception:
211
+ # Not expected here. We normally avoid a bare ``except Exception`` in
212
+ # this codebase, but a leaked subscription would stop connect_loop() from
213
+ # ever reconnecting, so we deliberately catch broadly, log the unexpected
214
+ # error, and release the channel before propagating.
215
+ self._logger.exception("Unexpected error during connect; releasing channel to avoid a leak")
206
216
  unsub()
207
217
  raise
208
218
  self._logger.info("Connected to device")
@@ -295,45 +295,75 @@ class V1Channel(Channel):
295
295
  if self._callback is not None:
296
296
  raise ValueError("Only one subscription allowed at a time")
297
297
 
298
- # Make an initial, optimistic attempt to connect to local with the
299
- # cache. The cache information will be refreshed by the background task.
300
- try:
301
- await self._local_connect(prefer_cache=True)
302
- except RoborockException as err:
303
- self._logger.debug("First local connection attempt failed, will retry: %s", err)
304
-
305
- # Start a background task to manage the local connection health. This
306
- # happens independent of whether we were able to connect locally now.
307
- if self._reconnect_task is None:
308
- loop = asyncio.get_running_loop()
309
- self._reconnect_task = loop.create_task(self._background_reconnect())
310
-
311
- # We maintain an active MQTT subscription even when connected locally to receive
312
- # unsolicited status updates (DPS push messages) directly from the cloud.
298
+ # Claim the subscription up front. Any failure in the setup below routes
299
+ # through _teardown(), which clears this again so the channel is left in
300
+ # a clean, re-subscribable state. Without this, a partially-completed
301
+ # subscribe (e.g. a transient failure later in connect()) would leave a
302
+ # stale callback and the next subscribe() would raise the guard above.
303
+ self._callback = callback
313
304
  try:
314
- self._mqtt_unsub = await self._mqtt_channel.subscribe(self._on_mqtt_message)
315
- except RoborockException as err:
316
- if not self.is_local_connected:
317
- # Propagate error if both local and MQTT failed
318
- self._logger.debug("MQTT connection also failed: %s", err)
319
- raise
320
- self._logger.debug("MQTT subscription failed, continuing with local-only connection: %s", err)
321
-
322
- def unsub() -> None:
323
- """Unsubscribe from all messages."""
324
- if self._reconnect_task:
325
- self._reconnect_task.cancel()
326
- self._reconnect_task = None
327
- if self._mqtt_unsub:
328
- self._mqtt_unsub()
329
- self._mqtt_unsub = None
330
- if self._local_unsub:
331
- self._local_unsub()
332
- self._local_unsub = None
333
- self._logger.debug("Unsubscribed from device")
305
+ # Make an initial, optimistic attempt to connect to local with the
306
+ # cache. The cache information will be refreshed by the background task.
307
+ try:
308
+ await self._local_connect(prefer_cache=True)
309
+ except RoborockException as err:
310
+ self._logger.debug("First local connection attempt failed, will retry: %s", err)
334
311
 
335
- self._callback = callback
336
- return unsub
312
+ # Start a background task to manage the local connection health. This
313
+ # happens independent of whether we were able to connect locally now.
314
+ if self._reconnect_task is None:
315
+ loop = asyncio.get_running_loop()
316
+ self._reconnect_task = loop.create_task(self._background_reconnect())
317
+
318
+ # We maintain an active MQTT subscription even when connected locally to receive
319
+ # unsolicited status updates (DPS push messages) directly from the cloud.
320
+ try:
321
+ self._mqtt_unsub = await self._mqtt_channel.subscribe(self._on_mqtt_message)
322
+ except RoborockException as err:
323
+ if not self.is_local_connected:
324
+ # Propagate error if both local and MQTT failed
325
+ self._logger.debug("MQTT connection also failed: %s", err)
326
+ raise
327
+ self._logger.debug("MQTT subscription failed, continuing with local-only connection: %s", err)
328
+ except RoborockException:
329
+ # Expected failure path (e.g. both local and MQTT transports down).
330
+ # Release the channel so the next subscribe() starts clean.
331
+ self._teardown()
332
+ raise
333
+ except Exception:
334
+ # Not expected here. We normally avoid a bare ``except Exception`` in
335
+ # this codebase, but leaving a partial subscription behind (reconnect
336
+ # task, MQTT subscription, stale callback) would brick the device, so we
337
+ # deliberately catch broadly, log the unexpected error, and tear down
338
+ # before propagating.
339
+ self._logger.exception("Unexpected error during subscribe; tearing down to avoid a leak")
340
+ self._teardown()
341
+ raise
342
+
343
+ self._logger.debug("Subscribed to device")
344
+ return self._teardown
345
+
346
+ def _teardown(self) -> None:
347
+ """Tear down all subscriptions and reset the channel to a re-subscribable state.
348
+
349
+ Returned from subscribe() as the unsubscribe function and also invoked on
350
+ any failure partway through subscribe(). Idempotent: each resource is
351
+ guarded so repeat calls are no-ops.
352
+ """
353
+ if self._reconnect_task:
354
+ self._reconnect_task.cancel()
355
+ self._reconnect_task = None
356
+ if self._mqtt_unsub:
357
+ self._mqtt_unsub()
358
+ self._mqtt_unsub = None
359
+ if self._local_unsub:
360
+ self._local_unsub()
361
+ self._local_unsub = None
362
+ if self._local_channel:
363
+ self._local_channel.close()
364
+ self._local_channel = None
365
+ self._callback = None
366
+ self._logger.debug("Unsubscribed from device")
337
367
 
338
368
  def add_dps_listener(self, listener: Callable[[dict[RoborockDataProtocol, Any]], None]) -> Callable[[], None]:
339
369
  """Add a listener for DPS updates.