python-roborock 2.9.0rc1__tar.gz → 2.9.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 (29) hide show
  1. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/PKG-INFO +46 -4
  2. python_roborock-2.9.2/README.md +76 -0
  3. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/pyproject.toml +7 -13
  4. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/api.py +36 -36
  5. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/cloud_api.py +78 -74
  6. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/code_mappings.py +42 -2
  7. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/const.py +1 -0
  8. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/containers.py +39 -23
  9. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/local_api.py +30 -15
  10. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/roborock_future.py +10 -3
  11. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/roborock_typing.py +6 -0
  12. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/version_1_apis/roborock_client_v1.py +30 -16
  13. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/version_1_apis/roborock_local_client_v1.py +22 -8
  14. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/version_1_apis/roborock_mqtt_client_v1.py +22 -15
  15. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/version_a01_apis/roborock_client_a01.py +12 -5
  16. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +12 -6
  17. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/web_api.py +31 -11
  18. python_roborock-2.9.0rc1/README.md +0 -32
  19. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/LICENSE +0 -0
  20. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/__init__.py +0 -0
  21. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/cli.py +0 -0
  22. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/command_cache.py +0 -0
  23. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/exceptions.py +0 -0
  24. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/protocol.py +0 -0
  25. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/py.typed +0 -0
  26. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/roborock_message.py +0 -0
  27. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/util.py +0 -0
  28. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/version_1_apis/__init__.py +0 -0
  29. {python_roborock-2.9.0rc1 → python_roborock-2.9.2}/roborock/version_a01_apis/__init__.py +0 -0
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: python-roborock
3
- Version: 2.9.0rc1
3
+ Version: 2.9.2
4
4
  Summary: A package to control Roborock vacuums.
5
- Home-page: https://github.com/humbertogontijo/python-roborock
6
5
  License: GPL-3.0-only
7
6
  Keywords: roborock,vacuum,homeassistant
8
7
  Author: humbertogontijo
@@ -22,7 +21,6 @@ Requires-Dist: aiohttp (>=3.8.2,<4.0.0)
22
21
  Requires-Dist: async-timeout
23
22
  Requires-Dist: click (>=8)
24
23
  Requires-Dist: construct (>=2.10.57,<3.0.0)
25
- Requires-Dist: dacite (>=1.8.0,<2.0.0)
26
24
  Requires-Dist: paho-mqtt (>=1.6.1,<2.0.0)
27
25
  Requires-Dist: pycryptodome (>=3.18,<4.0)
28
26
  Requires-Dist: pycryptodomex (>=3.18,<4.0) ; sys_platform == "darwin"
@@ -53,6 +51,50 @@ Install this via pip (or your favourite package manager):
53
51
 
54
52
  You can see all of the commands supported [here]("https://python-roborock.readthedocs.io/en/latest/api_commands.html")
55
53
 
54
+ ## Sending Commands
55
+
56
+ Here is an example that requires no manual intervention and can be done all automatically. You can skip some steps by
57
+ caching values or looking at them and grabbing them manually.
58
+ ```python
59
+ import asyncio
60
+
61
+ from roborock import HomeDataProduct, DeviceData, RoborockCommand
62
+ from roborock.version_1_apis import RoborockMqttClientV1, RoborockLocalClientV1
63
+ from roborock.web_api import RoborockApiClient
64
+
65
+ async def main():
66
+ web_api = RoborockApiClient(username="youremailhere")
67
+ # Login via your password
68
+ user_data = await web_api.pass_login(password="pass_here")
69
+ # Or login via a code
70
+ await web_api.request_code()
71
+ code = input("What is the code?")
72
+ user_data = await web_api.code_login(code)
73
+
74
+ # Get home data
75
+ home_data = await web_api.get_home_data_v2(user_data)
76
+
77
+ # Get the device you want
78
+ device = home_data.devices[0]
79
+
80
+ # Get product ids:
81
+ product_info: dict[str, HomeDataProduct] = {
82
+ product.id: product for product in home_data.products
83
+ }
84
+ # Create the Mqtt(aka cloud required) Client
85
+ device_data = DeviceData(device, product_info[device.product_id].model)
86
+ mqtt_client = RoborockMqttClientV1(user_data, device_data)
87
+ networking = await mqtt_client.get_networking()
88
+ local_device_data = DeviceData(device, product_info[device.product_id].model, networking.ip)
89
+ local_client = RoborockLocalClientV1(local_device_data)
90
+ # You can use the send_command to send any command to the device
91
+ status = await local_client.send_command(RoborockCommand.GET_STATUS)
92
+ # Or use existing functions that will give you data classes
93
+ status = await local_client.get_status()
94
+
95
+ asyncio.run(main())
96
+ ```
97
+
56
98
  ## Supported devices
57
99
 
58
100
  You can find what devices are supported
@@ -0,0 +1,76 @@
1
+ # Roborock
2
+
3
+ <p align="center">
4
+ <a href="https://pypi.org/project/python-roborock/">
5
+ <img src="https://img.shields.io/pypi/v/python-roborock.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
6
+ </a>
7
+ <img src="https://img.shields.io/pypi/pyversions/python-roborock.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">
8
+ <img src="https://img.shields.io/pypi/l/python-roborock.svg?style=flat-square" alt="License">
9
+ </p>
10
+
11
+ Roborock library for online and offline control of your vacuums.
12
+
13
+ ## Installation
14
+
15
+ Install this via pip (or your favourite package manager):
16
+
17
+ `pip install python-roborock`
18
+
19
+ ## Functionality
20
+
21
+ You can see all of the commands supported [here]("https://python-roborock.readthedocs.io/en/latest/api_commands.html")
22
+
23
+ ## Sending Commands
24
+
25
+ Here is an example that requires no manual intervention and can be done all automatically. You can skip some steps by
26
+ caching values or looking at them and grabbing them manually.
27
+ ```python
28
+ import asyncio
29
+
30
+ from roborock import HomeDataProduct, DeviceData, RoborockCommand
31
+ from roborock.version_1_apis import RoborockMqttClientV1, RoborockLocalClientV1
32
+ from roborock.web_api import RoborockApiClient
33
+
34
+ async def main():
35
+ web_api = RoborockApiClient(username="youremailhere")
36
+ # Login via your password
37
+ user_data = await web_api.pass_login(password="pass_here")
38
+ # Or login via a code
39
+ await web_api.request_code()
40
+ code = input("What is the code?")
41
+ user_data = await web_api.code_login(code)
42
+
43
+ # Get home data
44
+ home_data = await web_api.get_home_data_v2(user_data)
45
+
46
+ # Get the device you want
47
+ device = home_data.devices[0]
48
+
49
+ # Get product ids:
50
+ product_info: dict[str, HomeDataProduct] = {
51
+ product.id: product for product in home_data.products
52
+ }
53
+ # Create the Mqtt(aka cloud required) Client
54
+ device_data = DeviceData(device, product_info[device.product_id].model)
55
+ mqtt_client = RoborockMqttClientV1(user_data, device_data)
56
+ networking = await mqtt_client.get_networking()
57
+ local_device_data = DeviceData(device, product_info[device.product_id].model, networking.ip)
58
+ local_client = RoborockLocalClientV1(local_device_data)
59
+ # You can use the send_command to send any command to the device
60
+ status = await local_client.send_command(RoborockCommand.GET_STATUS)
61
+ # Or use existing functions that will give you data classes
62
+ status = await local_client.get_status()
63
+
64
+ asyncio.run(main())
65
+ ```
66
+
67
+ ## Supported devices
68
+
69
+ You can find what devices are supported
70
+ [here]("https://python-roborock.readthedocs.io/en/latest/supported_devices.html").
71
+ Please note this may not immediately contain the latest devices.
72
+
73
+
74
+ ## Credits
75
+
76
+ Thanks @rovo89 for https://gist.github.com/rovo89/dff47ed19fca0dfdda77503e66c2b7c7 And thanks @PiotrMachowski for https://github.com/PiotrMachowski/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-roborock"
3
- version = "2.9.0-rc.1"
3
+ version = "2.9.2"
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"
@@ -28,7 +28,6 @@ async-timeout = "*"
28
28
  pycryptodome = "^3.18"
29
29
  pycryptodomex = {version = "^3.18", markers = "sys_platform == 'darwin'"}
30
30
  paho-mqtt = "^1.6.1"
31
- dacite = "^1.8.0"
32
31
  construct = "^2.10.57"
33
32
  vacuum-map-parser-roborock = "*"
34
33
 
@@ -40,25 +39,17 @@ build-backend = "poetry.core.masonry.api"
40
39
  [tool.poetry.group.dev.dependencies]
41
40
  pytest-asyncio = "*"
42
41
  pytest = "*"
43
- pre-commit = "^3.5.0"
42
+ pre-commit = ">=3.5,<5.0"
44
43
  mypy = "*"
45
44
  ruff = "*"
46
45
  codespell = "*"
47
46
  pyshark = "^0.6"
47
+ aioresponses = "^0.7.7"
48
48
 
49
49
  [tool.semantic_release]
50
+ branch = "main"
50
51
  version_toml = ["pyproject.toml:tool.poetry.version"]
51
52
  build_command = "pip install poetry && poetry build"
52
-
53
- [semantic_release.branches.main]
54
- match = "release"
55
- prerelease = false
56
-
57
- [tool.semantic_release.branches.other]
58
- match = ".*"
59
- prerelease_token = "rc"
60
- prerelease = true
61
-
62
53
  [tool.semantic_release.commit_parser_options]
63
54
  allowed_tags = [
64
55
  "chore",
@@ -76,3 +67,6 @@ select=["E", "F", "UP", "I"]
76
67
 
77
68
  [tool.ruff.lint.per-file-ignores]
78
69
  "*/__init__.py" = ["F401"]
70
+
71
+ [tool.pytest.ini_options]
72
+ asyncio_mode = "auto"
@@ -7,7 +7,7 @@ import base64
7
7
  import logging
8
8
  import secrets
9
9
  import time
10
- from collections.abc import Callable, Coroutine
10
+ from abc import ABC, abstractmethod
11
11
  from typing import Any
12
12
 
13
13
  from .containers import (
@@ -16,33 +16,35 @@ from .containers import (
16
16
  from .exceptions import (
17
17
  RoborockTimeout,
18
18
  UnknownMethodError,
19
- VacuumError,
20
19
  )
21
20
  from .roborock_future import RoborockFuture
22
21
  from .roborock_message import (
23
22
  RoborockMessage,
24
23
  )
25
24
  from .roborock_typing import RoborockCommand
26
- from .util import RoborockLoggerAdapter, get_next_int, get_running_loop_or_create_one
25
+ from .util import get_next_int, get_running_loop_or_create_one
27
26
 
28
27
  _LOGGER = logging.getLogger(__name__)
29
28
  KEEPALIVE = 60
30
29
 
31
30
 
32
- class RoborockClient:
33
- def __init__(self, endpoint: str, device_info: DeviceData, queue_timeout: int = 4) -> None:
31
+ class RoborockClient(ABC):
32
+ """Roborock client base class."""
33
+
34
+ _logger: logging.LoggerAdapter
35
+
36
+ def __init__(self, device_info: DeviceData, queue_timeout: int = 4) -> None:
37
+ """Initialize RoborockClient."""
34
38
  self.event_loop = get_running_loop_or_create_one()
35
39
  self.device_info = device_info
36
- self._endpoint = endpoint
37
40
  self._nonce = secrets.token_bytes(16)
38
41
  self._waiting_queue: dict[int, RoborockFuture] = {}
39
- self._last_device_msg_in = self.time_func()
40
- self._last_disconnection = self.time_func()
42
+ self._last_device_msg_in = time.monotonic()
43
+ self._last_disconnection = time.monotonic()
41
44
  self.keep_alive = KEEPALIVE
42
45
  self._diagnostic_data: dict[str, dict[str, Any]] = {
43
46
  "misc_info": {"Nonce": base64.b64encode(self._nonce).decode("utf-8")}
44
47
  }
45
- self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER)
46
48
  self.is_available: bool = True
47
49
  self.queue_timeout = queue_timeout
48
50
 
@@ -59,35 +61,30 @@ class RoborockClient:
59
61
  def diagnostic_data(self) -> dict:
60
62
  return self._diagnostic_data
61
63
 
62
- @property
63
- def time_func(self) -> Callable[[], float]:
64
- try:
65
- # Use monotonic clock if available
66
- time_func = time.monotonic
67
- except AttributeError:
68
- time_func = time.time
69
- return time_func
70
-
64
+ @abstractmethod
71
65
  async def async_connect(self):
72
- raise NotImplementedError
66
+ """Connect to the Roborock device."""
73
67
 
68
+ @abstractmethod
74
69
  def sync_disconnect(self) -> Any:
75
- raise NotImplementedError
70
+ """Disconnect from the Roborock device."""
76
71
 
72
+ @abstractmethod
77
73
  async def async_disconnect(self) -> Any:
78
- raise NotImplementedError
74
+ """Disconnect from the Roborock device."""
79
75
 
76
+ @abstractmethod
80
77
  def on_message_received(self, messages: list[RoborockMessage]) -> None:
81
- raise NotImplementedError
78
+ """Handle received incoming messages from the device."""
82
79
 
83
80
  def on_connection_lost(self, exc: Exception | None) -> None:
84
- self._last_disconnection = self.time_func()
81
+ self._last_disconnection = time.monotonic()
85
82
  self._logger.info("Roborock client disconnected")
86
83
  if exc is not None:
87
84
  self._logger.warning(exc)
88
85
 
89
86
  def should_keepalive(self) -> bool:
90
- now = self.time_func()
87
+ now = time.monotonic()
91
88
  # noinspection PyUnresolvedReferences
92
89
  if now - self._last_disconnection > self.keep_alive**2 and now - self._last_device_msg_in > self.keep_alive:
93
90
  return False
@@ -98,37 +95,40 @@ class RoborockClient:
98
95
  await self.async_disconnect()
99
96
  await self.async_connect()
100
97
 
101
- async def _wait_response(self, request_id: int, queue: RoborockFuture) -> tuple[Any, VacuumError | None]:
98
+ async def _wait_response(self, request_id: int, queue: RoborockFuture) -> Any:
102
99
  try:
103
- (response, err) = await queue.async_get(self.queue_timeout)
100
+ response = await queue.async_get(self.queue_timeout)
104
101
  if response == "unknown_method":
105
102
  raise UnknownMethodError("Unknown method")
106
- return response, err
103
+ return response
107
104
  except (asyncio.TimeoutError, asyncio.CancelledError):
108
105
  raise RoborockTimeout(f"id={request_id} Timeout after {self.queue_timeout} seconds") from None
109
106
  finally:
110
107
  self._waiting_queue.pop(request_id, None)
111
108
 
112
- def _async_response(
113
- self, request_id: int, protocol_id: int = 0
114
- ) -> Coroutine[Any, Any, tuple[Any, VacuumError | None]]:
109
+ def _async_response(self, request_id: int, protocol_id: int = 0) -> Any:
115
110
  queue = RoborockFuture(protocol_id)
116
111
  if request_id in self._waiting_queue:
117
112
  new_id = get_next_int(10000, 32767)
118
- _LOGGER.warning(
119
- f"Attempting to create a future with an existing request_id... New id is {new_id}. "
120
- f"Code may not function properly."
113
+ self._logger.warning(
114
+ "Attempting to create a future with an existing id %s (%s)... New id is %s. "
115
+ "Code may not function properly.",
116
+ request_id,
117
+ protocol_id,
118
+ new_id,
121
119
  )
122
120
  request_id = new_id
123
121
  self._waiting_queue[request_id] = queue
124
- return self._wait_response(request_id, queue)
122
+ return asyncio.ensure_future(self._wait_response(request_id, queue))
125
123
 
124
+ @abstractmethod
126
125
  async def send_message(self, roborock_message: RoborockMessage):
127
- raise NotImplementedError
126
+ """Send a message to the Roborock device."""
128
127
 
128
+ @abstractmethod
129
129
  async def _send_command(
130
130
  self,
131
131
  method: RoborockCommand | str,
132
132
  params: list | dict | int | None = None,
133
133
  ):
134
- raise NotImplementedError
134
+ """Send a command to the Roborock device."""
@@ -1,12 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
4
- import base64
5
3
  import logging
6
4
  import threading
7
- import typing
8
5
  import uuid
9
- from asyncio import Lock, Task
6
+ from abc import ABC
7
+ from asyncio import Lock
10
8
  from typing import Any
11
9
  from urllib.parse import urlparse
12
10
 
@@ -15,31 +13,52 @@ import paho.mqtt.client as mqtt
15
13
  from .api import KEEPALIVE, RoborockClient
16
14
  from .containers import DeviceData, UserData
17
15
  from .exceptions import RoborockException, VacuumError
18
- from .protocol import MessageParser, Utils, md5hex
16
+ from .protocol import MessageParser, md5hex
19
17
  from .roborock_future import RoborockFuture
20
- from .roborock_message import RoborockMessage
21
- from .roborock_typing import RoborockCommand
22
- from .util import RoborockLoggerAdapter
23
18
 
24
- if typing.TYPE_CHECKING:
25
- pass
26
19
  _LOGGER = logging.getLogger(__name__)
27
20
  CONNECT_REQUEST_ID = 0
28
21
  DISCONNECT_REQUEST_ID = 1
29
22
 
30
23
 
31
- class RoborockMqttClient(RoborockClient, mqtt.Client):
24
+ class _Mqtt(mqtt.Client):
25
+ """Internal MQTT client.
26
+
27
+ This is a subclass of the Paho MQTT client that adds some additional functionality
28
+ for error cases where things get stuck.
29
+ """
30
+
32
31
  _thread: threading.Thread
33
32
  _client_id: str
34
33
 
34
+ def __init__(self) -> None:
35
+ """Initialize the MQTT client."""
36
+ super().__init__(protocol=mqtt.MQTTv5)
37
+ self.reset_client_id()
38
+
39
+ def reset_client_id(self):
40
+ """Generate a new client id to make a new session when reconnecting."""
41
+ self._client_id = mqtt.base62(uuid.uuid4().int, padding=22)
42
+
43
+ def maybe_restart_loop(self) -> None:
44
+ """Ensure that the MQTT loop is running in case it previously exited."""
45
+ if not self._thread or not self._thread.is_alive():
46
+ if self._thread:
47
+ _LOGGER.info("Stopping mqtt loop")
48
+ super().loop_stop()
49
+ _LOGGER.info("Starting mqtt loop")
50
+ super().loop_start()
51
+
52
+
53
+ class RoborockMqttClient(RoborockClient, ABC):
54
+ """Roborock MQTT client base class."""
55
+
35
56
  def __init__(self, user_data: UserData, device_info: DeviceData, queue_timeout: int = 10) -> None:
57
+ """Initialize the Roborock MQTT client."""
36
58
  rriot = user_data.rriot
37
59
  if rriot is None:
38
60
  raise RoborockException("Got no rriot data from user_data")
39
- endpoint = base64.b64encode(Utils.md5(rriot.k.encode())[8:14]).decode()
40
- RoborockClient.__init__(self, endpoint, device_info, queue_timeout)
41
- mqtt.Client.__init__(self, protocol=mqtt.MQTTv5)
42
- self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER)
61
+ RoborockClient.__init__(self, device_info, queue_timeout)
43
62
  self._mqtt_user = rriot.u
44
63
  self._hashed_user = md5hex(self._mqtt_user + ":" + rriot.k)[2:10]
45
64
  url = urlparse(rriot.r.m)
@@ -48,39 +67,43 @@ class RoborockMqttClient(RoborockClient, mqtt.Client):
48
67
  self._mqtt_host = str(url.hostname)
49
68
  self._mqtt_port = url.port
50
69
  self._mqtt_ssl = url.scheme == "ssl"
70
+
71
+ self._mqtt_client = _Mqtt()
72
+ self._mqtt_client.on_connect = self._mqtt_on_connect
73
+ self._mqtt_client.on_message = self._mqtt_on_message
74
+ self._mqtt_client.on_disconnect = self._mqtt_on_disconnect
51
75
  if self._mqtt_ssl:
52
- super().tls_set()
76
+ self._mqtt_client.tls_set()
77
+
53
78
  self._mqtt_password = rriot.s
54
79
  self._hashed_password = md5hex(self._mqtt_password + ":" + rriot.k)[16:]
55
- super().username_pw_set(self._hashed_user, self._hashed_password)
56
- self._endpoint = base64.b64encode(Utils.md5(rriot.k.encode())[8:14]).decode()
80
+ self._mqtt_client.username_pw_set(self._hashed_user, self._hashed_password)
57
81
  self._waiting_queue: dict[int, RoborockFuture] = {}
58
82
  self._mutex = Lock()
59
- self.update_client_id()
60
83
 
61
- def on_connect(self, *args, **kwargs):
84
+ def _mqtt_on_connect(self, *args, **kwargs):
62
85
  _, __, ___, rc, ____ = args
63
86
  connection_queue = self._waiting_queue.get(CONNECT_REQUEST_ID)
64
87
  if rc != mqtt.MQTT_ERR_SUCCESS:
65
88
  message = f"Failed to connect ({mqtt.error_string(rc)})"
66
89
  self._logger.error(message)
67
90
  if connection_queue:
68
- connection_queue.resolve((None, VacuumError(message)))
91
+ connection_queue.set_exception(VacuumError(message))
69
92
  return
70
93
  self._logger.info(f"Connected to mqtt {self._mqtt_host}:{self._mqtt_port}")
71
94
  topic = f"rr/m/o/{self._mqtt_user}/{self._hashed_user}/{self.device_info.device.duid}"
72
- (result, mid) = self.subscribe(topic)
95
+ (result, mid) = self._mqtt_client.subscribe(topic)
73
96
  if result != 0:
74
97
  message = f"Failed to subscribe ({mqtt.error_string(rc)})"
75
98
  self._logger.error(message)
76
99
  if connection_queue:
77
- connection_queue.resolve((None, VacuumError(message)))
100
+ connection_queue.set_exception(VacuumError(message))
78
101
  return
79
102
  self._logger.info(f"Subscribed to topic {topic}")
80
103
  if connection_queue:
81
- connection_queue.resolve((True, None))
104
+ connection_queue.set_result(True)
82
105
 
83
- def on_message(self, *args, **kwargs):
106
+ def _mqtt_on_message(self, *args, **kwargs):
84
107
  client, __, msg = args
85
108
  try:
86
109
  messages, _ = MessageParser.parse(msg.payload, local_key=self.device_info.device.local_key)
@@ -88,93 +111,74 @@ class RoborockMqttClient(RoborockClient, mqtt.Client):
88
111
  except Exception as ex:
89
112
  self._logger.exception(ex)
90
113
 
91
- def on_disconnect(self, *args, **kwargs):
114
+ def _mqtt_on_disconnect(self, *args, **kwargs):
92
115
  _, __, rc, ___ = args
93
116
  try:
94
117
  exc = RoborockException(mqtt.error_string(rc)) if rc != mqtt.MQTT_ERR_SUCCESS else None
95
118
  super().on_connection_lost(exc)
96
119
  if rc == mqtt.MQTT_ERR_PROTOCOL:
97
- self.update_client_id()
120
+ self._mqtt_client.reset_client_id()
98
121
  connection_queue = self._waiting_queue.get(DISCONNECT_REQUEST_ID)
99
122
  if connection_queue:
100
- connection_queue.resolve((True, None))
123
+ connection_queue.set_result(True)
101
124
  except Exception as ex:
102
125
  self._logger.exception(ex)
103
126
 
104
- def update_client_id(self):
105
- self._client_id = mqtt.base62(uuid.uuid4().int, padding=22)
106
-
107
- def sync_stop_loop(self) -> None:
108
- if self._thread:
109
- self._logger.info("Stopping mqtt loop")
110
- super().loop_stop()
111
-
112
- def sync_start_loop(self) -> None:
113
- if not self._thread or not self._thread.is_alive():
114
- self.sync_stop_loop()
115
- self._logger.info("Starting mqtt loop")
116
- super().loop_start()
127
+ def is_connected(self) -> bool:
128
+ """Check if the mqtt client is connected."""
129
+ return self._mqtt_client.is_connected()
117
130
 
118
- def sync_disconnect(self) -> tuple[bool, Task[tuple[Any, VacuumError | None]] | None]:
131
+ def sync_disconnect(self) -> Any:
119
132
  if not self.is_connected():
120
- return False, None
133
+ return None
121
134
 
122
135
  self._logger.info("Disconnecting from mqtt")
123
- disconnected_future = asyncio.ensure_future(self._async_response(DISCONNECT_REQUEST_ID))
124
- rc = super().disconnect()
136
+ disconnected_future = self._async_response(DISCONNECT_REQUEST_ID)
137
+ rc = self._mqtt_client.disconnect()
125
138
 
126
139
  if rc == mqtt.MQTT_ERR_NO_CONN:
127
140
  disconnected_future.cancel()
128
- return False, None
141
+ return None
129
142
 
130
143
  if rc != mqtt.MQTT_ERR_SUCCESS:
131
144
  disconnected_future.cancel()
132
145
  raise RoborockException(f"Failed to disconnect ({mqtt.error_string(rc)})")
133
146
 
134
- return True, disconnected_future
147
+ return disconnected_future
135
148
 
136
- def sync_connect(self) -> tuple[bool, Task[tuple[Any, VacuumError | None]] | None]:
149
+ def sync_connect(self) -> Any:
137
150
  if self.is_connected():
138
- self.sync_start_loop()
139
- return False, None
151
+ self._mqtt_client.maybe_restart_loop()
152
+ return None
140
153
 
141
154
  if self._mqtt_port is None or self._mqtt_host is None:
142
155
  raise RoborockException("Mqtt information was not entered. Cannot connect.")
143
156
 
144
157
  self._logger.debug("Connecting to mqtt")
145
- connected_future = asyncio.ensure_future(self._async_response(CONNECT_REQUEST_ID))
146
- super().connect(host=self._mqtt_host, port=self._mqtt_port, keepalive=KEEPALIVE)
147
-
148
- self.sync_start_loop()
149
- return True, connected_future
158
+ connected_future = self._async_response(CONNECT_REQUEST_ID)
159
+ self._mqtt_client.connect(host=self._mqtt_host, port=self._mqtt_port, keepalive=KEEPALIVE)
160
+ self._mqtt_client.maybe_restart_loop()
161
+ return connected_future
150
162
 
151
163
  async def async_disconnect(self) -> None:
152
164
  async with self._mutex:
153
- (disconnecting, disconnected_future) = self.sync_disconnect()
154
- if disconnecting and disconnected_future:
155
- (_, err) = await disconnected_future
156
- if err:
165
+ if disconnected_future := self.sync_disconnect():
166
+ try:
167
+ await disconnected_future
168
+ except VacuumError as err:
157
169
  raise RoborockException(err) from err
158
170
 
159
171
  async def async_connect(self) -> None:
160
172
  async with self._mutex:
161
- (connecting, connected_future) = self.sync_connect()
162
- if connecting and connected_future:
163
- (_, err) = await connected_future
164
- if err:
173
+ if connected_future := self.sync_connect():
174
+ try:
175
+ await connected_future
176
+ except VacuumError as err:
165
177
  raise RoborockException(err) from err
166
178
 
167
179
  def _send_msg_raw(self, msg: bytes) -> None:
168
- info = self.publish(f"rr/m/i/{self._mqtt_user}/{self._hashed_user}/{self.device_info.device.duid}", msg)
180
+ info = self._mqtt_client.publish(
181
+ f"rr/m/i/{self._mqtt_user}/{self._hashed_user}/{self.device_info.device.duid}", msg
182
+ )
169
183
  if info.rc != mqtt.MQTT_ERR_SUCCESS:
170
184
  raise RoborockException(f"Failed to publish ({mqtt.error_string(info.rc)})")
171
-
172
- async def send_message(self, roborock_message: RoborockMessage):
173
- raise NotImplementedError
174
-
175
- async def _send_command(
176
- self,
177
- method: RoborockCommand | str,
178
- params: list | dict | int | None = None,
179
- ):
180
- raise NotImplementedError