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.
- {python_roborock-2.8.5 → python_roborock-2.9.2}/PKG-INFO +45 -3
- python_roborock-2.9.2/README.md +76 -0
- {python_roborock-2.8.5 → python_roborock-2.9.2}/pyproject.toml +6 -3
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/api.py +27 -21
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/cloud_api.py +78 -74
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/code_mappings.py +27 -0
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/const.py +1 -0
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/containers.py +34 -20
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/local_api.py +30 -15
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/roborock_future.py +10 -3
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/roborock_typing.py +6 -0
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/version_1_apis/roborock_client_v1.py +23 -15
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/version_1_apis/roborock_local_client_v1.py +21 -8
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/version_1_apis/roborock_mqtt_client_v1.py +21 -15
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/version_a01_apis/roborock_client_a01.py +12 -5
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +12 -6
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/web_api.py +31 -11
- python_roborock-2.8.5/README.md +0 -32
- {python_roborock-2.8.5 → python_roborock-2.9.2}/LICENSE +0 -0
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/__init__.py +0 -0
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/cli.py +0 -0
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/command_cache.py +0 -0
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/exceptions.py +0 -0
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/protocol.py +0 -0
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/py.typed +0 -0
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/roborock_message.py +0 -0
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/util.py +0 -0
- {python_roborock-2.8.5 → python_roborock-2.9.2}/roborock/version_1_apis/__init__.py +0 -0
- {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.
|
|
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&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.
|
|
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 = "
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
"""Connect to the Roborock device."""
|
|
64
67
|
|
|
68
|
+
@abstractmethod
|
|
65
69
|
def sync_disconnect(self) -> Any:
|
|
66
|
-
|
|
70
|
+
"""Disconnect from the Roborock device."""
|
|
67
71
|
|
|
72
|
+
@abstractmethod
|
|
68
73
|
async def async_disconnect(self) -> Any:
|
|
69
|
-
|
|
74
|
+
"""Disconnect from the Roborock device."""
|
|
70
75
|
|
|
76
|
+
@abstractmethod
|
|
71
77
|
def on_message_received(self, messages: list[RoborockMessage]) -> None:
|
|
72
|
-
|
|
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) ->
|
|
98
|
+
async def _wait_response(self, request_id: int, queue: RoborockFuture) -> Any:
|
|
93
99
|
try:
|
|
94
|
-
|
|
100
|
+
response = await queue.async_get(self.queue_timeout)
|
|
95
101
|
if response == "unknown_method":
|
|
96
102
|
raise UnknownMethodError("Unknown method")
|
|
97
|
-
return response
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
104
|
+
connection_queue.set_result(True)
|
|
82
105
|
|
|
83
|
-
def
|
|
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
|
|
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.
|
|
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.
|
|
123
|
+
connection_queue.set_result(True)
|
|
101
124
|
except Exception as ex:
|
|
102
125
|
self._logger.exception(ex)
|
|
103
126
|
|
|
104
|
-
def
|
|
105
|
-
|
|
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) ->
|
|
131
|
+
def sync_disconnect(self) -> Any:
|
|
119
132
|
if not self.is_connected():
|
|
120
|
-
return
|
|
133
|
+
return None
|
|
121
134
|
|
|
122
135
|
self._logger.info("Disconnecting from mqtt")
|
|
123
|
-
disconnected_future =
|
|
124
|
-
rc =
|
|
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
|
|
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
|
|
147
|
+
return disconnected_future
|
|
135
148
|
|
|
136
|
-
def sync_connect(self) ->
|
|
149
|
+
def sync_connect(self) -> Any:
|
|
137
150
|
if self.is_connected():
|
|
138
|
-
self.
|
|
139
|
-
return
|
|
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 =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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(
|
|
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"
|