python-roborock 2.45.0__tar.gz → 2.46.0__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 (59) hide show
  1. {python_roborock-2.45.0 → python_roborock-2.46.0}/PKG-INFO +1 -1
  2. {python_roborock-2.45.0 → python_roborock-2.46.0}/pyproject.toml +1 -1
  3. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/api.py +1 -7
  4. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/cloud_api.py +92 -13
  5. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/exceptions.py +1 -0
  6. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/version_1_apis/roborock_local_client_v1.py +7 -1
  7. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +1 -1
  8. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +1 -1
  9. {python_roborock-2.45.0 → python_roborock-2.46.0}/LICENSE +0 -0
  10. {python_roborock-2.45.0 → python_roborock-2.46.0}/README.md +0 -0
  11. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/__init__.py +0 -0
  12. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/b01_containers.py +0 -0
  13. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/broadcast_protocol.py +0 -0
  14. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/callbacks.py +0 -0
  15. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/clean_modes.py +0 -0
  16. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/cli.py +0 -0
  17. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/code_mappings.py +0 -0
  18. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/command_cache.py +0 -0
  19. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/const.py +0 -0
  20. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/containers.py +0 -0
  21. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/device_features.py +0 -0
  22. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/README.md +0 -0
  23. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/__init__.py +0 -0
  24. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/a01_channel.py +0 -0
  25. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/b01_channel.py +0 -0
  26. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/cache.py +0 -0
  27. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/channel.py +0 -0
  28. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/device.py +0 -0
  29. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/device_manager.py +0 -0
  30. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/local_channel.py +0 -0
  31. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/mqtt_channel.py +0 -0
  32. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/traits/b01/__init__.py +0 -0
  33. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/traits/b01/props.py +0 -0
  34. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/traits/clean_summary.py +0 -0
  35. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/traits/dnd.py +0 -0
  36. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/traits/dyad.py +0 -0
  37. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/traits/sound_volume.py +0 -0
  38. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/traits/status.py +0 -0
  39. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/traits/trait.py +0 -0
  40. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/traits/zeo.py +0 -0
  41. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/v1_channel.py +0 -0
  42. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/devices/v1_rpc_channel.py +0 -0
  43. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/mqtt/__init__.py +0 -0
  44. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/mqtt/roborock_session.py +0 -0
  45. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/mqtt/session.py +0 -0
  46. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/protocol.py +0 -0
  47. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/protocols/a01_protocol.py +0 -0
  48. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/protocols/b01_protocol.py +0 -0
  49. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/protocols/v1_protocol.py +0 -0
  50. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/py.typed +0 -0
  51. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/roborock_future.py +0 -0
  52. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/roborock_message.py +0 -0
  53. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/roborock_typing.py +0 -0
  54. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/util.py +0 -0
  55. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/version_1_apis/__init__.py +0 -0
  56. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  57. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/version_a01_apis/__init__.py +0 -0
  58. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  59. {python_roborock-2.45.0 → python_roborock-2.46.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-roborock
3
- Version: 2.45.0
3
+ Version: 2.46.0
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Home-page: https://github.com/humbertogontijo/python-roborock
6
6
  License: GPL-3.0-only
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-roborock"
3
- version = "2.45.0"
3
+ version = "2.46.0"
4
4
  description = "A package to control Roborock vacuums."
5
5
  authors = ["humbertogontijo <humbertogontijo@users.noreply.github.com>"]
6
6
  license = "GPL-3.0-only"
@@ -23,7 +23,7 @@ from .roborock_message import (
23
23
  from .util import get_next_int
24
24
 
25
25
  _LOGGER = logging.getLogger(__name__)
26
- KEEPALIVE = 60
26
+ KEEPALIVE = 70
27
27
 
28
28
 
29
29
  class RoborockClient(ABC):
@@ -78,12 +78,6 @@ class RoborockClient(ABC):
78
78
  return False
79
79
  return True
80
80
 
81
- async def validate_connection(self) -> None:
82
- if not self.should_keepalive():
83
- self._logger.info("Resetting Roborock connection due to keepalive timeout")
84
- await self.async_disconnect()
85
- await self.async_connect()
86
-
87
81
  async def _wait_response(self, request_id: int, queue: RoborockFuture) -> Any:
88
82
  try:
89
83
  response = await queue.async_get(self.queue_timeout)
@@ -8,6 +8,10 @@ from asyncio import Lock
8
8
  from typing import Any
9
9
 
10
10
  import paho.mqtt.client as mqtt
11
+ from paho.mqtt.enums import MQTTErrorCode
12
+
13
+ # Mypy is not seeing this for some reason. It wants me to use the depreciated ReasonCodes
14
+ from paho.mqtt.reasoncodes import ReasonCode # type: ignore
11
15
 
12
16
  from .api import KEEPALIVE, RoborockClient
13
17
  from .containers import DeviceData, UserData
@@ -67,7 +71,8 @@ class RoborockMqttClient(RoborockClient, ABC):
67
71
  self._mqtt_client = _Mqtt()
68
72
  self._mqtt_client.on_connect = self._mqtt_on_connect
69
73
  self._mqtt_client.on_message = self._mqtt_on_message
70
- self._mqtt_client.on_disconnect = self._mqtt_on_disconnect
74
+ # Due to the incorrect ReasonCode, it is confused by typing
75
+ self._mqtt_client.on_disconnect = self._mqtt_on_disconnect # type: ignore
71
76
  if mqtt_params.tls:
72
77
  self._mqtt_client.tls_set()
73
78
 
@@ -76,12 +81,20 @@ class RoborockMqttClient(RoborockClient, ABC):
76
81
  self._mutex = Lock()
77
82
  self._decoder: Decoder = create_mqtt_decoder(device_info.device.local_key)
78
83
  self._encoder: Encoder = create_mqtt_encoder(device_info.device.local_key)
84
+ self.received_message_since_last_disconnect = False
85
+ self._topic = f"rr/m/o/{self._mqtt_user}/{self._hashed_user}/{self.device_info.device.duid}"
79
86
 
80
- def _mqtt_on_connect(self, *args, **kwargs):
81
- _, __, ___, rc, ____ = args
87
+ def _mqtt_on_connect(
88
+ self,
89
+ client: mqtt.Client,
90
+ userdata: object,
91
+ flags: dict[str, int],
92
+ rc: ReasonCode,
93
+ properties: mqtt.Properties | None = None,
94
+ ):
82
95
  connection_queue = self._waiting_queue.get(CONNECT_REQUEST_ID)
83
- if rc != mqtt.MQTT_ERR_SUCCESS:
84
- message = f"Failed to connect ({mqtt.error_string(rc)})"
96
+ if rc.is_failure:
97
+ message = f"Failed to connect ({rc})"
85
98
  self._logger.error(message)
86
99
  if connection_queue:
87
100
  connection_queue.set_exception(VacuumError(message))
@@ -89,19 +102,19 @@ class RoborockMqttClient(RoborockClient, ABC):
89
102
  self._logger.debug("Failed to notify connect future, not in queue")
90
103
  return
91
104
  self._logger.info(f"Connected to mqtt {self._mqtt_host}:{self._mqtt_port}")
92
- topic = f"rr/m/o/{self._mqtt_user}/{self._hashed_user}/{self.device_info.device.duid}"
93
- (result, mid) = self._mqtt_client.subscribe(topic)
105
+ (result, mid) = self._mqtt_client.subscribe(self._topic)
94
106
  if result != 0:
95
- message = f"Failed to subscribe ({mqtt.error_string(rc)})"
107
+ message = f"Failed to subscribe ({str(rc)})"
96
108
  self._logger.error(message)
97
109
  if connection_queue:
98
110
  connection_queue.set_exception(VacuumError(message))
99
111
  return
100
- self._logger.info(f"Subscribed to topic {topic}")
112
+ self._logger.info(f"Subscribed to topic {self._topic}")
101
113
  if connection_queue:
102
114
  connection_queue.set_result(True)
103
115
 
104
116
  def _mqtt_on_message(self, *args, **kwargs):
117
+ self.received_message_since_last_disconnect = True
105
118
  client, __, msg = args
106
119
  try:
107
120
  messages = self._decoder(msg.payload)
@@ -109,10 +122,16 @@ class RoborockMqttClient(RoborockClient, ABC):
109
122
  except Exception as ex:
110
123
  self._logger.exception(ex)
111
124
 
112
- def _mqtt_on_disconnect(self, *args, **kwargs):
113
- _, __, rc, ___ = args
125
+ def _mqtt_on_disconnect(
126
+ self,
127
+ client: mqtt.Client,
128
+ data: object,
129
+ flags: dict[str, int],
130
+ rc: ReasonCode | None,
131
+ properties: mqtt.Properties | None = None,
132
+ ):
114
133
  try:
115
- exc = RoborockException(mqtt.error_string(rc)) if rc != mqtt.MQTT_ERR_SUCCESS else None
134
+ exc = RoborockException(str(rc)) if rc is not None and rc.is_failure else None
116
135
  super().on_connection_lost(exc)
117
136
  connection_queue = self._waiting_queue.get(DISCONNECT_REQUEST_ID)
118
137
  if connection_queue:
@@ -138,7 +157,7 @@ class RoborockMqttClient(RoborockClient, ABC):
138
157
 
139
158
  if rc != mqtt.MQTT_ERR_SUCCESS:
140
159
  disconnected_future.cancel()
141
- raise RoborockException(f"Failed to disconnect ({mqtt.error_string(rc)})")
160
+ raise RoborockException(f"Failed to disconnect ({str(rc)})")
142
161
 
143
162
  return disconnected_future
144
163
 
@@ -178,3 +197,63 @@ class RoborockMqttClient(RoborockClient, ABC):
178
197
  )
179
198
  if info.rc != mqtt.MQTT_ERR_SUCCESS:
180
199
  raise RoborockException(f"Failed to publish ({mqtt.error_string(info.rc)})")
200
+
201
+ async def _unsubscribe(self) -> MQTTErrorCode:
202
+ """Unsubscribe from the topic."""
203
+ loop = asyncio.get_running_loop()
204
+ (result, mid) = await loop.run_in_executor(None, self._mqtt_client.unsubscribe, self._topic)
205
+
206
+ if result != 0:
207
+ message = f"Failed to unsubscribe ({mqtt.error_string(result)})"
208
+ self._logger.error(message)
209
+ else:
210
+ self._logger.info(f"Unsubscribed from topic {self._topic}")
211
+ return result
212
+
213
+ async def _subscribe(self) -> MQTTErrorCode:
214
+ """Subscribe to the topic."""
215
+ loop = asyncio.get_running_loop()
216
+ (result, mid) = await loop.run_in_executor(None, self._mqtt_client.subscribe, self._topic)
217
+
218
+ if result != 0:
219
+ message = f"Failed to subscribe ({mqtt.error_string(result)})"
220
+ self._logger.error(message)
221
+ else:
222
+ self._logger.info(f"Subscribed to topic {self._topic}")
223
+ return result
224
+
225
+ async def _reconnect(self) -> None:
226
+ """Reconnect to the MQTT broker."""
227
+ await self.async_disconnect()
228
+ await self.async_connect()
229
+
230
+ async def _validate_connection(self) -> None:
231
+ """Override the default validate connection to try to re-subscribe rather than disconnect.
232
+ When something seems to be wrong with our connection, we should follow the following steps:
233
+ 1. Try to unsubscribe and resubscribe from the topic.
234
+ 2. If we don't end up getting a message, we should completely disconnect and reconnect to the MQTT broker.
235
+ 3. We will continue to try to disconnect and reconnect until we get a message.
236
+ 4. If we get a message, the next time connection is lost, We will go back to step 1.
237
+ """
238
+ # If we should no longer keep the current connection alive...
239
+ if not self.should_keepalive():
240
+ self._logger.info("Resetting Roborock connection due to keepalive timeout")
241
+ if not self.received_message_since_last_disconnect:
242
+ # If we have already tried to unsub and resub, and we are still in this state,
243
+ # we should try to reconnect.
244
+ return await self._reconnect()
245
+ try:
246
+ # Mark that we have tried to unsubscribe and resubscribe
247
+ self.received_message_since_last_disconnect = False
248
+ if await self._unsubscribe() != 0:
249
+ # If we fail to unsubscribe, reconnect to the broker
250
+ return await self._reconnect()
251
+ if await self._subscribe() != 0:
252
+ # If we fail to subscribe, reconnected to the broker.
253
+ return await self._reconnect()
254
+
255
+ except Exception: # noqa
256
+ # If we get any errors at all, we should just reconnect.
257
+ return await self._reconnect()
258
+ # Call connect to make sure everything is still in a good state.
259
+ await self.async_connect()
@@ -1,4 +1,5 @@
1
1
  """Roborock exceptions."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
 
@@ -138,6 +138,12 @@ class RoborockLocalClientV1(RoborockClientV1, RoborockClient):
138
138
  response_protocol=RoborockMessageProtocol.PING_RESPONSE,
139
139
  )
140
140
 
141
+ async def _validate_connection(self) -> None:
142
+ if not self.should_keepalive():
143
+ self._logger.info("Resetting Roborock connection due to keepalive timeout")
144
+ await self.async_disconnect()
145
+ await self.async_connect()
146
+
141
147
  def _send_msg_raw(self, data: bytes):
142
148
  try:
143
149
  if not self.transport:
@@ -172,7 +178,7 @@ class RoborockLocalClientV1(RoborockClientV1, RoborockClient):
172
178
  method: str | None = None,
173
179
  params: list | dict | int | None = None,
174
180
  ) -> RoborockMessage:
175
- await self.validate_connection()
181
+ await self._validate_connection()
176
182
  msg = self._encoder(roborock_message)
177
183
  if method:
178
184
  self._logger.debug(f"id={request_id} Requesting method {method} with {params}")
@@ -51,7 +51,7 @@ class RoborockMqttClientV1(RoborockMqttClient, RoborockClientV1):
51
51
  )
52
52
  self._logger.debug("Building message id %s for method %s", request_message.request_id, method)
53
53
 
54
- await self.validate_connection()
54
+ await self._validate_connection()
55
55
  request_id = request_message.request_id
56
56
  response_protocol = (
57
57
  RoborockMessageProtocol.MAP_RESPONSE if method in COMMANDS_SECURED else RoborockMessageProtocol.RPC_RESPONSE
@@ -40,7 +40,7 @@ class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01):
40
40
  self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER)
41
41
 
42
42
  async def _send_message(self, roborock_message: RoborockMessage):
43
- await self.validate_connection()
43
+ await self._validate_connection()
44
44
  response_protocol = RoborockMessageProtocol.RPC_RESPONSE
45
45
 
46
46
  m = self._encoder(roborock_message)