python-roborock 2.8.5__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.8.5 → python_roborock-2.9.2}/PKG-INFO +45 -3
  2. python_roborock-2.9.2/README.md +76 -0
  3. {python_roborock-2.8.5 → python_roborock-2.9.2}/pyproject.toml +6 -3
  4. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/api.py +27 -21
  5. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/cloud_api.py +78 -74
  6. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/code_mappings.py +27 -0
  7. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/const.py +1 -0
  8. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/containers.py +34 -20
  9. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/local_api.py +30 -15
  10. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/roborock_future.py +10 -3
  11. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/roborock_typing.py +6 -0
  12. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/version_1_apis/roborock_client_v1.py +23 -15
  13. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/version_1_apis/roborock_local_client_v1.py +21 -8
  14. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/version_1_apis/roborock_mqtt_client_v1.py +21 -15
  15. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/version_a01_apis/roborock_client_a01.py +12 -5
  16. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +12 -6
  17. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/web_api.py +31 -11
  18. python_roborock-2.8.5/README.md +0 -32
  19. {python_roborock-2.8.5 → python_roborock-2.9.2}/LICENSE +0 -0
  20. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/__init__.py +0 -0
  21. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/cli.py +0 -0
  22. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/command_cache.py +0 -0
  23. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/exceptions.py +0 -0
  24. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/protocol.py +0 -0
  25. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/py.typed +0 -0
  26. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/roborock_message.py +0 -0
  27. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/util.py +0 -0
  28. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/version_1_apis/__init__.py +0 -0
  29. {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/version_a01_apis/__init__.py +0 -0
@@ -1,8 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-roborock
3
- Version: 2.8.5
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.8.5"
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,11 +39,12 @@ 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
50
  branch = "main"
@@ -67,3 +67,6 @@ select=["E", "F", "UP", "I"]
67
67
 
68
68
  [tool.ruff.lint.per-file-ignores]
69
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 Coroutine
10
+ from abc import ABC, abstractmethod
11
11
  from typing import Any
12
12
 
13
13
  from .containers import (
@@ -16,24 +16,27 @@ 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
42
  self._last_device_msg_in = time.monotonic()
@@ -42,7 +45,6 @@ class RoborockClient:
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,17 +61,21 @@ class RoborockClient:
59
61
  def diagnostic_data(self) -> dict:
60
62
  return self._diagnostic_data
61
63
 
64
+ @abstractmethod
62
65
  async def async_connect(self):
63
- raise NotImplementedError
66
+ """Connect to the Roborock device."""
64
67
 
68
+ @abstractmethod
65
69
  def sync_disconnect(self) -> Any:
66
- raise NotImplementedError
70
+ """Disconnect from the Roborock device."""
67
71
 
72
+ @abstractmethod
68
73
  async def async_disconnect(self) -> Any:
69
- raise NotImplementedError
74
+ """Disconnect from the Roborock device."""
70
75
 
76
+ @abstractmethod
71
77
  def on_message_received(self, messages: list[RoborockMessage]) -> None:
72
- raise NotImplementedError
78
+ """Handle received incoming messages from the device."""
73
79
 
74
80
  def on_connection_lost(self, exc: Exception | None) -> None:
75
81
  self._last_disconnection = time.monotonic()
@@ -89,24 +95,22 @@ class RoborockClient:
89
95
  await self.async_disconnect()
90
96
  await self.async_connect()
91
97
 
92
- 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:
93
99
  try:
94
- (response, err) = await queue.async_get(self.queue_timeout)
100
+ response = await queue.async_get(self.queue_timeout)
95
101
  if response == "unknown_method":
96
102
  raise UnknownMethodError("Unknown method")
97
- return response, err
103
+ return response
98
104
  except (asyncio.TimeoutError, asyncio.CancelledError):
99
105
  raise RoborockTimeout(f"id={request_id} Timeout after {self.queue_timeout} seconds") from None
100
106
  finally:
101
107
  self._waiting_queue.pop(request_id, None)
102
108
 
103
- def _async_response(
104
- self, request_id: int, protocol_id: int = 0
105
- ) -> Coroutine[Any, Any, tuple[Any, VacuumError | None]]:
109
+ def _async_response(self, request_id: int, protocol_id: int = 0) -> Any:
106
110
  queue = RoborockFuture(protocol_id)
107
111
  if request_id in self._waiting_queue:
108
112
  new_id = get_next_int(10000, 32767)
109
- _LOGGER.warning(
113
+ self._logger.warning(
110
114
  "Attempting to create a future with an existing id %s (%s)... New id is %s. "
111
115
  "Code may not function properly.",
112
116
  request_id,
@@ -115,14 +119,16 @@ class RoborockClient:
115
119
  )
116
120
  request_id = new_id
117
121
  self._waiting_queue[request_id] = queue
118
- return self._wait_response(request_id, queue)
122
+ return asyncio.ensure_future(self._wait_response(request_id, queue))
119
123
 
124
+ @abstractmethod
120
125
  async def send_message(self, roborock_message: RoborockMessage):
121
- raise NotImplementedError
126
+ """Send a message to the Roborock device."""
122
127
 
128
+ @abstractmethod
123
129
  async def _send_command(
124
130
  self,
125
131
  method: RoborockCommand | str,
126
132
  params: list | dict | int | None = None,
127
133
  ):
128
- 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
@@ -254,6 +254,15 @@ class RoborockFanSpeedQRevoMaster(RoborockFanPowerCode):
254
254
  smart_mode = 110
255
255
 
256
256
 
257
+ class RoborockFanSpeedQRevoCurv(RoborockFanPowerCode):
258
+ quiet = 101
259
+ balanced = 102
260
+ turbo = 103
261
+ max = 104
262
+ max_plus = 105
263
+ smart_mode = 110
264
+
265
+
257
266
  class RoborockFanSpeedP10(RoborockFanPowerCode):
258
267
  off = 105
259
268
  quiet = 101
@@ -279,6 +288,14 @@ class RoborockMopModeCode(RoborockEnum):
279
288
  """Describes the mop mode of the vacuum cleaner."""
280
289
 
281
290
 
291
+ class RoborockMopModeQRevoCurv(RoborockMopModeCode):
292
+ standard = 300
293
+ deep = 301
294
+ deep_plus = 303
295
+ fast = 304
296
+ smart_mode = 306
297
+
298
+
282
299
  class RoborockMopModeS7(RoborockMopModeCode):
283
300
  """Describes the mop mode of the vacuum cleaner."""
284
301
 
@@ -351,6 +368,15 @@ class RoborockMopIntensityQRevoMaster(RoborockMopIntensityCode):
351
368
  smart_mode = 209
352
369
 
353
370
 
371
+ class RoborockMopIntensityQRevoCurv(RoborockMopIntensityCode):
372
+ off = 200
373
+ low = 201
374
+ medium = 202
375
+ high = 203
376
+ custom_water_flow = 207
377
+ smart_mode = 209
378
+
379
+
354
380
  class RoborockMopIntensityP10(RoborockMopIntensityCode):
355
381
  """Describes the mop intensity of the vacuum cleaner."""
356
382
 
@@ -431,6 +457,7 @@ class RoborockDockTypeCode(RoborockEnum):
431
457
  s8_maxv_ultra_dock = 10
432
458
  qrevo_master_dock = 14
433
459
  qrevo_s_dock = 15
460
+ qrevo_curv_dock = 17
434
461
 
435
462
 
436
463
  class RoborockDockDustCollectionModeCode(RoborockEnum):
@@ -31,6 +31,7 @@ ROBOROCK_Q7 = "roborock.vacuum.a40"
31
31
  ROBOROCK_Q7_MAX = "roborock.vacuum.a38"
32
32
  ROBOROCK_Q7PLUS = "roborock.vacuum.a40"
33
33
  ROBOROCK_QREVO_MASTER = "roborock.vacuum.a117"
34
+ ROBOROCK_QREVO_CURV = "roborock.vacuum.a135"
34
35
  ROBOROCK_Q8_MAX = "roborock.vacuum.a73"
35
36
  ROBOROCK_G10S_PRO = "roborock.vacuum.a26"
36
37
  ROBOROCK_G10S = "roborock.vacuum.a46"