python-roborock 3.10.0__tar.gz → 3.10.2__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 (96) hide show
  1. {python_roborock-3.10.0 → python_roborock-3.10.2}/PKG-INFO +1 -1
  2. {python_roborock-3.10.0 → python_roborock-3.10.2}/pyproject.toml +1 -1
  3. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/containers.py +3 -1
  4. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/mqtt/roborock_session.py +56 -7
  5. {python_roborock-3.10.0 → python_roborock-3.10.2}/.gitignore +0 -0
  6. {python_roborock-3.10.0 → python_roborock-3.10.2}/LICENSE +0 -0
  7. {python_roborock-3.10.0 → python_roborock-3.10.2}/README.md +0 -0
  8. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/__init__.py +0 -0
  9. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/api.py +0 -0
  10. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/broadcast_protocol.py +0 -0
  11. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/callbacks.py +0 -0
  12. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/cli.py +0 -0
  13. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/cloud_api.py +0 -0
  14. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/command_cache.py +0 -0
  15. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/const.py +0 -0
  16. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/__init__.py +0 -0
  17. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/b01_q10/__init__.py +0 -0
  18. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  19. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  20. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/b01_q7/__init__.py +0 -0
  21. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  22. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  23. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/code_mappings.py +0 -0
  24. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/dyad/__init__.py +0 -0
  25. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  26. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/dyad/dyad_containers.py +0 -0
  27. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/v1/__init__.py +0 -0
  28. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/v1/v1_clean_modes.py +0 -0
  29. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/v1/v1_code_mappings.py +0 -0
  30. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/v1/v1_containers.py +0 -0
  31. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/zeo/__init__.py +0 -0
  32. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  33. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/data/zeo/zeo_containers.py +0 -0
  34. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/device_features.py +0 -0
  35. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/README.md +0 -0
  36. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/__init__.py +0 -0
  37. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/a01_channel.py +0 -0
  38. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/b01_channel.py +0 -0
  39. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/cache.py +0 -0
  40. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/channel.py +0 -0
  41. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/device.py +0 -0
  42. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/device_manager.py +0 -0
  43. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/file_cache.py +0 -0
  44. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/local_channel.py +0 -0
  45. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/mqtt_channel.py +0 -0
  46. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/__init__.py +0 -0
  47. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/a01/__init__.py +0 -0
  48. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/b01/__init__.py +0 -0
  49. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/traits_mixin.py +0 -0
  50. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/__init__.py +0 -0
  51. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/child_lock.py +0 -0
  52. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/clean_summary.py +0 -0
  53. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/command.py +0 -0
  54. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/common.py +0 -0
  55. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/consumeable.py +0 -0
  56. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/device_features.py +0 -0
  57. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  58. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  59. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  60. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/home.py +0 -0
  61. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/led_status.py +0 -0
  62. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/map_content.py +0 -0
  63. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/maps.py +0 -0
  64. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/network_info.py +0 -0
  65. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/rooms.py +0 -0
  66. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/routines.py +0 -0
  67. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  68. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/status.py +0 -0
  69. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  70. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/volume.py +0 -0
  71. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  72. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/devices/v1_channel.py +0 -0
  73. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/exceptions.py +0 -0
  74. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/map/__init__.py +0 -0
  75. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/map/map_parser.py +0 -0
  76. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/mqtt/__init__.py +0 -0
  77. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/mqtt/health_manager.py +0 -0
  78. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/mqtt/session.py +0 -0
  79. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/protocol.py +0 -0
  80. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/protocols/__init__.py +0 -0
  81. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/protocols/a01_protocol.py +0 -0
  82. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/protocols/b01_protocol.py +0 -0
  83. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/protocols/v1_protocol.py +0 -0
  84. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/py.typed +0 -0
  85. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/roborock_future.py +0 -0
  86. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/roborock_message.py +0 -0
  87. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/roborock_typing.py +0 -0
  88. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/util.py +0 -0
  89. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/version_1_apis/__init__.py +0 -0
  90. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  91. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  92. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  93. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/version_a01_apis/__init__.py +0 -0
  94. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  95. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  96. {python_roborock-3.10.0 → python_roborock-3.10.2}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 3.10.0
3
+ Version: 3.10.2
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Project-URL: Repository, https://github.com/humbertogontijo/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 = "3.10.0"
3
+ version = "3.10.2"
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"
@@ -67,7 +67,9 @@ class RoborockBase:
67
67
  sub_type = get_args(class_type)[0]
68
68
  return [RoborockBase._convert_to_class_obj(sub_type, obj) for obj in value]
69
69
  if get_origin(class_type) is dict:
70
- _, value_type = get_args(class_type) # assume keys are only basic types
70
+ key_type, value_type = get_args(class_type)
71
+ if key_type is not None:
72
+ return {key_type(k): RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()}
71
73
  return {k: RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()}
72
74
  if inspect.isclass(class_type):
73
75
  if issubclass(class_type, RoborockBase):
@@ -24,7 +24,8 @@ from .session import MqttParams, MqttSession, MqttSessionException
24
24
  _LOGGER = logging.getLogger(__name__)
25
25
  _MQTT_LOGGER = logging.getLogger(f"{__name__}.aiomqtt")
26
26
 
27
- KEEPALIVE = 60
27
+ CLIENT_KEEPALIVE = datetime.timedelta(seconds=120)
28
+ TOPIC_KEEPALIVE = datetime.timedelta(seconds=60)
28
29
 
29
30
  # Exponential backoff parameters
30
31
  MIN_BACKOFF_INTERVAL = datetime.timedelta(seconds=10)
@@ -47,7 +48,11 @@ class RoborockMqttSession(MqttSession):
47
48
  re-established.
48
49
  """
49
50
 
50
- def __init__(self, params: MqttParams):
51
+ def __init__(
52
+ self,
53
+ params: MqttParams,
54
+ topic_idle_timeout: datetime.timedelta = TOPIC_KEEPALIVE,
55
+ ):
51
56
  self._params = params
52
57
  self._reconnect_task: asyncio.Task[None] | None = None
53
58
  self._healthy = False
@@ -57,6 +62,8 @@ class RoborockMqttSession(MqttSession):
57
62
  self._client_lock = asyncio.Lock()
58
63
  self._listeners: CallbackMap[str, bytes] = CallbackMap(_LOGGER)
59
64
  self._connection_task: asyncio.Task[None] | None = None
65
+ self._topic_idle_timeout = topic_idle_timeout
66
+ self._idle_timers: dict[str, asyncio.Task[None]] = {}
60
67
 
61
68
  @property
62
69
  def connected(self) -> bool:
@@ -86,11 +93,15 @@ class RoborockMqttSession(MqttSession):
86
93
  async def close(self) -> None:
87
94
  """Cancels the MQTT loop and shutdown the client library."""
88
95
  self._stop = True
89
- tasks = [task for task in [self._connection_task, self._reconnect_task] if task]
96
+ tasks = [task for task in [self._connection_task, self._reconnect_task, *self._idle_timers.values()] if task]
97
+ self._connection_task = None
98
+ self._reconnect_task = None
99
+ self._idle_timers.clear()
100
+
90
101
  for task in tasks:
91
102
  task.cancel()
92
103
  try:
93
- await asyncio.gather(*tasks)
104
+ await asyncio.gather(*tasks, return_exceptions=True)
94
105
  except asyncio.CancelledError:
95
106
  pass
96
107
 
@@ -183,7 +194,7 @@ class RoborockMqttSession(MqttSession):
183
194
  port=params.port,
184
195
  username=params.username,
185
196
  password=params.password,
186
- keepalive=KEEPALIVE,
197
+ keepalive=int(CLIENT_KEEPALIVE.total_seconds()),
187
198
  protocol=aiomqtt.ProtocolVersion.V5,
188
199
  tls_params=TLSParameters() if params.tls else None,
189
200
  timeout=params.timeout,
@@ -210,9 +221,17 @@ class RoborockMqttSession(MqttSession):
210
221
  The callback will be called with the message payload as a bytes object. The callback
211
222
  should not block since it runs in the async loop. It should not raise any exceptions.
212
223
 
213
- The returned callable unsubscribes from the topic when called.
224
+ The returned callable unsubscribes from the topic when called, but will delay actual
225
+ unsubscription for the idle timeout period. If a new subscription comes in during the
226
+ timeout, the timer is cancelled and the subscription is reused.
214
227
  """
215
228
  _LOGGER.debug("Subscribing to topic %s", topic)
229
+
230
+ # If there is an idle timer for this topic, cancel it (reuse subscription)
231
+ if idle_timer := self._idle_timers.pop(topic, None):
232
+ idle_timer.cancel()
233
+ _LOGGER.debug("Cancelled idle timer for topic %s (reused subscription)", topic)
234
+
216
235
  unsub = self._listeners.add_callback(topic, callback)
217
236
 
218
237
  async with self._client_lock:
@@ -221,11 +240,41 @@ class RoborockMqttSession(MqttSession):
221
240
  try:
222
241
  await self._client.subscribe(topic)
223
242
  except MqttError as err:
243
+ # Clean up the callback if subscription fails
244
+ unsub()
224
245
  raise MqttSessionException(f"Error subscribing to topic: {err}") from err
225
246
  else:
226
247
  _LOGGER.debug("Client not connected, will establish subscription later")
227
248
 
228
- return unsub
249
+ def schedule_unsubscribe():
250
+ async def idle_unsubscribe():
251
+ try:
252
+ await asyncio.sleep(self._topic_idle_timeout.total_seconds())
253
+ # Only unsubscribe if there are no callbacks left for this topic
254
+ if not self._listeners.get_callbacks(topic):
255
+ async with self._client_lock:
256
+ if self._client:
257
+ _LOGGER.debug("Idle timeout expired, unsubscribing from topic %s", topic)
258
+ try:
259
+ await self._client.unsubscribe(topic)
260
+ except MqttError as err:
261
+ _LOGGER.warning("Error unsubscribing from topic %s: %s", topic, err)
262
+ # Clean up timer from dict
263
+ self._idle_timers.pop(topic, None)
264
+ except asyncio.CancelledError:
265
+ _LOGGER.debug("Idle unsubscribe for topic %s cancelled", topic)
266
+
267
+ # Start the idle timer task
268
+ task = asyncio.create_task(idle_unsubscribe())
269
+ self._idle_timers[topic] = task
270
+
271
+ def delayed_unsub():
272
+ unsub() # Remove the callback from CallbackMap
273
+ # If no more callbacks for this topic, start idle timer
274
+ if not self._listeners.get_callbacks(topic):
275
+ schedule_unsubscribe()
276
+
277
+ return delayed_unsub
229
278
 
230
279
  async def publish(self, topic: str, message: bytes) -> None:
231
280
  """Publish a message on the topic."""