python-roborock 3.19.0__tar.gz → 3.20.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.
- {python_roborock-3.19.0 → python_roborock-3.20.0}/PKG-INFO +1 -1
- {python_roborock-3.19.0 → python_roborock-3.20.0}/pyproject.toml +1 -1
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/callbacks.py +1 -1
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +8 -1
- python_roborock-3.20.0/roborock/devices/b01_channel.py +129 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/mqtt_channel.py +1 -1
- python_roborock-3.20.0/roborock/devices/traits/b01/q7/__init__.py +115 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/protocol.py +2 -2
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/protocols/b01_protocol.py +4 -1
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/roborock_typing.py +54 -1
- python_roborock-3.19.0/roborock/devices/b01_channel.py +0 -77
- python_roborock-3.19.0/roborock/devices/traits/b01/q7/__init__.py +0 -33
- {python_roborock-3.19.0 → python_roborock-3.20.0}/.gitignore +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/LICENSE +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/README.md +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/api.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/cli.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/cloud_api.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/command_cache.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/const.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/code_mappings.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/containers.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/device_features.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/README.md +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/a01_channel.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/cache.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/channel.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/device.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/device_manager.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/file_cache.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/local_channel.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/common.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/v1_channel.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/diagnostics.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/exceptions.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/map/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/map/map_parser.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/protocols/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/py.typed +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/roborock_future.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/roborock_message.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/util.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_1_apis/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_a01_apis/__init__.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
- {python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.20.0
|
|
4
4
|
Summary: A package to control Roborock vacuums.
|
|
5
5
|
Project-URL: Repository, https://github.com/humbertogontijo/python-roborock
|
|
6
6
|
Project-URL: Documentation, https://python-roborock.readthedocs.io/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-roborock"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.20.0"
|
|
4
4
|
description = "A package to control Roborock vacuums."
|
|
5
5
|
authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
|
|
6
6
|
requires-python = ">=3.11, <4"
|
|
@@ -121,7 +121,7 @@ def decoder_callback(
|
|
|
121
121
|
|
|
122
122
|
def wrapper(data: K) -> None:
|
|
123
123
|
if not (messages := decoder(data)):
|
|
124
|
-
logger.
|
|
124
|
+
logger.debug("Failed to decode message: %s", data)
|
|
125
125
|
return
|
|
126
126
|
for message in messages:
|
|
127
127
|
logger.debug("Decoded message: %s", message)
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q7/b01_q7_code_mappings.py
RENAMED
|
@@ -24,7 +24,6 @@ class SCWindMapping(RoborockModeEnum):
|
|
|
24
24
|
STANDARD = ("balanced", 1)
|
|
25
25
|
STRONG = ("turbo", 2)
|
|
26
26
|
SUPER_STRONG = ("max", 3)
|
|
27
|
-
MAX = ("max_plus", 4)
|
|
28
27
|
|
|
29
28
|
|
|
30
29
|
class WaterLevelMapping(RoborockModeEnum):
|
|
@@ -50,6 +49,14 @@ class CleanRepeatMapping(RoborockModeEnum):
|
|
|
50
49
|
TWICE = ("twice", 1)
|
|
51
50
|
|
|
52
51
|
|
|
52
|
+
class SCDeviceCleanParam(RoborockModeEnum):
|
|
53
|
+
"""Maps the control values for cleaning tasks."""
|
|
54
|
+
|
|
55
|
+
STOP = ("stop", 0)
|
|
56
|
+
START = ("start", 1)
|
|
57
|
+
PAUSE = ("pause", 2)
|
|
58
|
+
|
|
59
|
+
|
|
53
60
|
class WorkModeMapping(RoborockModeEnum):
|
|
54
61
|
"""Maps the detailed work modes of the robot."""
|
|
55
62
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Thin wrapper around the MQTT channel for Roborock B01 devices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from roborock.exceptions import RoborockException
|
|
11
|
+
from roborock.protocols.b01_protocol import (
|
|
12
|
+
CommandType,
|
|
13
|
+
ParamsType,
|
|
14
|
+
decode_rpc_response,
|
|
15
|
+
encode_mqtt_payload,
|
|
16
|
+
)
|
|
17
|
+
from roborock.roborock_message import RoborockMessage
|
|
18
|
+
from roborock.util import get_next_int
|
|
19
|
+
|
|
20
|
+
from .mqtt_channel import MqttChannel
|
|
21
|
+
|
|
22
|
+
_LOGGER = logging.getLogger(__name__)
|
|
23
|
+
_TIMEOUT = 10.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def send_decoded_command(
|
|
27
|
+
mqtt_channel: MqttChannel,
|
|
28
|
+
dps: int,
|
|
29
|
+
command: CommandType,
|
|
30
|
+
params: ParamsType,
|
|
31
|
+
) -> dict[str, Any] | None:
|
|
32
|
+
"""Send a command on the MQTT channel and get a decoded response."""
|
|
33
|
+
msg_id = str(get_next_int(100000000000, 999999999999))
|
|
34
|
+
_LOGGER.debug(
|
|
35
|
+
"Sending B01 MQTT command: dps=%s method=%s msg_id=%s params=%s",
|
|
36
|
+
dps,
|
|
37
|
+
command,
|
|
38
|
+
msg_id,
|
|
39
|
+
params,
|
|
40
|
+
)
|
|
41
|
+
roborock_message = encode_mqtt_payload(dps, command, params, msg_id)
|
|
42
|
+
future: asyncio.Future[Any] = asyncio.get_running_loop().create_future()
|
|
43
|
+
|
|
44
|
+
def find_response(response_message: RoborockMessage) -> None:
|
|
45
|
+
"""Handle incoming messages and resolve the future."""
|
|
46
|
+
try:
|
|
47
|
+
decoded_dps = decode_rpc_response(response_message)
|
|
48
|
+
except RoborockException as ex:
|
|
49
|
+
_LOGGER.debug(
|
|
50
|
+
"Failed to decode B01 RPC response (expecting method=%s msg_id=%s): %s: %s",
|
|
51
|
+
command,
|
|
52
|
+
msg_id,
|
|
53
|
+
response_message,
|
|
54
|
+
ex,
|
|
55
|
+
)
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
for dps_value in decoded_dps.values():
|
|
59
|
+
# valid responses are JSON strings wrapped in the dps value
|
|
60
|
+
if not isinstance(dps_value, str):
|
|
61
|
+
_LOGGER.debug("Received unexpected response: %s", dps_value)
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
inner = json.loads(dps_value)
|
|
66
|
+
except (json.JSONDecodeError, TypeError):
|
|
67
|
+
_LOGGER.debug("Received unexpected response: %s", dps_value)
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if isinstance(inner, dict) and inner.get("msgId") == msg_id:
|
|
71
|
+
_LOGGER.debug("Received query response: %s", inner)
|
|
72
|
+
# Check for error code (0 = success, non-zero = error)
|
|
73
|
+
code = inner.get("code", 0)
|
|
74
|
+
if code != 0:
|
|
75
|
+
error_msg = (
|
|
76
|
+
f"B01 command failed with code {code} "
|
|
77
|
+
f"(method={command}, msg_id={msg_id}, dps={dps}, params={params})"
|
|
78
|
+
)
|
|
79
|
+
_LOGGER.debug("B01 error response: %s", error_msg)
|
|
80
|
+
if not future.done():
|
|
81
|
+
future.set_exception(RoborockException(error_msg))
|
|
82
|
+
return
|
|
83
|
+
data = inner.get("data")
|
|
84
|
+
# All get commands should be dicts
|
|
85
|
+
if command.endswith(".get") and not isinstance(data, dict):
|
|
86
|
+
if not future.done():
|
|
87
|
+
future.set_exception(
|
|
88
|
+
RoborockException(
|
|
89
|
+
f"Unexpected data type for response "
|
|
90
|
+
f"(method={command}, msg_id={msg_id}, dps={dps}, params={params})"
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
return
|
|
94
|
+
if not future.done():
|
|
95
|
+
future.set_result(data)
|
|
96
|
+
|
|
97
|
+
unsub = await mqtt_channel.subscribe(find_response)
|
|
98
|
+
|
|
99
|
+
_LOGGER.debug("Sending MQTT message: %s", roborock_message)
|
|
100
|
+
try:
|
|
101
|
+
await mqtt_channel.publish(roborock_message)
|
|
102
|
+
return await asyncio.wait_for(future, timeout=_TIMEOUT)
|
|
103
|
+
except TimeoutError as ex:
|
|
104
|
+
raise RoborockException(
|
|
105
|
+
f"B01 command timed out after {_TIMEOUT}s (method={command}, msg_id={msg_id}, dps={dps}, params={params})"
|
|
106
|
+
) from ex
|
|
107
|
+
except RoborockException as ex:
|
|
108
|
+
_LOGGER.warning(
|
|
109
|
+
"Error sending B01 decoded command (method=%s msg_id=%s dps=%s params=%s): %s",
|
|
110
|
+
command,
|
|
111
|
+
msg_id,
|
|
112
|
+
dps,
|
|
113
|
+
params,
|
|
114
|
+
ex,
|
|
115
|
+
)
|
|
116
|
+
raise
|
|
117
|
+
|
|
118
|
+
except Exception as ex:
|
|
119
|
+
_LOGGER.exception(
|
|
120
|
+
"Error sending B01 decoded command (method=%s msg_id=%s dps=%s params=%s): %s",
|
|
121
|
+
command,
|
|
122
|
+
msg_id,
|
|
123
|
+
dps,
|
|
124
|
+
params,
|
|
125
|
+
ex,
|
|
126
|
+
)
|
|
127
|
+
raise
|
|
128
|
+
finally:
|
|
129
|
+
unsub()
|
|
@@ -87,7 +87,7 @@ class MqttChannel(Channel):
|
|
|
87
87
|
try:
|
|
88
88
|
return await self._mqtt_session.publish(self._publish_topic, encoded_msg)
|
|
89
89
|
except MqttSessionException as e:
|
|
90
|
-
self._logger.
|
|
90
|
+
self._logger.debug("Error publishing MQTT message: %s", e)
|
|
91
91
|
raise RoborockException(f"Failed to publish MQTT message: {e}") from e
|
|
92
92
|
|
|
93
93
|
async def restart(self) -> None:
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Traits for Q7 B01 devices.
|
|
2
|
+
Potentially other devices may fall into this category in the future."""
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from roborock import B01Props
|
|
7
|
+
from roborock.data.b01_q7.b01_q7_code_mappings import (
|
|
8
|
+
CleanTaskTypeMapping,
|
|
9
|
+
SCDeviceCleanParam,
|
|
10
|
+
SCWindMapping,
|
|
11
|
+
WaterLevelMapping,
|
|
12
|
+
)
|
|
13
|
+
from roborock.devices.b01_channel import send_decoded_command
|
|
14
|
+
from roborock.devices.mqtt_channel import MqttChannel
|
|
15
|
+
from roborock.devices.traits import Trait
|
|
16
|
+
from roborock.roborock_message import RoborockB01Props
|
|
17
|
+
from roborock.roborock_typing import RoborockB01Q7Methods
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Q7PropertiesApi",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Q7PropertiesApi(Trait):
|
|
25
|
+
"""API for interacting with B01 devices."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, channel: MqttChannel) -> None:
|
|
28
|
+
"""Initialize the B01Props API."""
|
|
29
|
+
self._channel = channel
|
|
30
|
+
|
|
31
|
+
async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
|
|
32
|
+
"""Query the device for the values of the given Q7 properties."""
|
|
33
|
+
result = await self.send(
|
|
34
|
+
RoborockB01Q7Methods.GET_PROP,
|
|
35
|
+
{"property": props},
|
|
36
|
+
)
|
|
37
|
+
if not isinstance(result, dict):
|
|
38
|
+
raise TypeError(f"Unexpected response type for GET_PROP: {type(result).__name__}: {result!r}")
|
|
39
|
+
return B01Props.from_dict(result)
|
|
40
|
+
|
|
41
|
+
async def set_prop(self, prop: RoborockB01Props, value: Any) -> None:
|
|
42
|
+
"""Set a property on the device."""
|
|
43
|
+
await self.send(
|
|
44
|
+
command=RoborockB01Q7Methods.SET_PROP,
|
|
45
|
+
params={prop: value},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
async def set_fan_speed(self, fan_speed: SCWindMapping) -> None:
|
|
49
|
+
"""Set the fan speed (wind)."""
|
|
50
|
+
await self.set_prop(RoborockB01Props.WIND, fan_speed.code)
|
|
51
|
+
|
|
52
|
+
async def set_water_level(self, water_level: WaterLevelMapping) -> None:
|
|
53
|
+
"""Set the water level (water)."""
|
|
54
|
+
await self.set_prop(RoborockB01Props.WATER, water_level.code)
|
|
55
|
+
|
|
56
|
+
async def start_clean(self) -> None:
|
|
57
|
+
"""Start cleaning."""
|
|
58
|
+
await self.send(
|
|
59
|
+
command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
|
|
60
|
+
params={
|
|
61
|
+
"clean_type": CleanTaskTypeMapping.ALL.code,
|
|
62
|
+
"ctrl_value": SCDeviceCleanParam.START.code,
|
|
63
|
+
"room_ids": [],
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def pause_clean(self) -> None:
|
|
68
|
+
"""Pause cleaning."""
|
|
69
|
+
await self.send(
|
|
70
|
+
command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
|
|
71
|
+
params={
|
|
72
|
+
"clean_type": CleanTaskTypeMapping.ALL.code,
|
|
73
|
+
"ctrl_value": SCDeviceCleanParam.PAUSE.code,
|
|
74
|
+
"room_ids": [],
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
async def stop_clean(self) -> None:
|
|
79
|
+
"""Stop cleaning."""
|
|
80
|
+
await self.send(
|
|
81
|
+
command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
|
|
82
|
+
params={
|
|
83
|
+
"clean_type": CleanTaskTypeMapping.ALL.code,
|
|
84
|
+
"ctrl_value": SCDeviceCleanParam.STOP.code,
|
|
85
|
+
"room_ids": [],
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async def return_to_dock(self) -> None:
|
|
90
|
+
"""Return to dock."""
|
|
91
|
+
await self.send(
|
|
92
|
+
command=RoborockB01Q7Methods.START_RECHARGE,
|
|
93
|
+
params={},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def find_me(self) -> None:
|
|
97
|
+
"""Locate the robot."""
|
|
98
|
+
await self.send(
|
|
99
|
+
command=RoborockB01Q7Methods.FIND_DEVICE,
|
|
100
|
+
params={},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
async def send(self, command: RoborockB01Q7Methods, params: dict) -> Any:
|
|
104
|
+
"""Send a command to the device."""
|
|
105
|
+
return await send_decoded_command(
|
|
106
|
+
self._channel,
|
|
107
|
+
dps=10000,
|
|
108
|
+
command=command,
|
|
109
|
+
params=params,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def create(channel: MqttChannel) -> Q7PropertiesApi:
|
|
114
|
+
"""Create traits for B01 devices."""
|
|
115
|
+
return Q7PropertiesApi(channel)
|
|
@@ -276,7 +276,7 @@ class EncryptionAdapter(Construct):
|
|
|
276
276
|
if context.version == b"A01":
|
|
277
277
|
iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24]
|
|
278
278
|
decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
|
|
279
|
-
return decipher.encrypt(
|
|
279
|
+
return decipher.encrypt(obj)
|
|
280
280
|
elif context.version == b"B01":
|
|
281
281
|
iv = md5hex(f"{context.random:08x}" + B01_HASH)[9:25]
|
|
282
282
|
decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
|
|
@@ -300,7 +300,7 @@ class EncryptionAdapter(Construct):
|
|
|
300
300
|
if context.version == b"A01":
|
|
301
301
|
iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24]
|
|
302
302
|
decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
|
|
303
|
-
return
|
|
303
|
+
return decipher.decrypt(obj)
|
|
304
304
|
elif context.version == b"B01":
|
|
305
305
|
iv = md5hex(f"{context.random:08x}" + B01_HASH)[9:25]
|
|
306
306
|
decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
|
|
@@ -28,7 +28,10 @@ def encode_mqtt_payload(dps: int, command: CommandType, params: ParamsType, msg_
|
|
|
28
28
|
dps: {
|
|
29
29
|
"method": str(command),
|
|
30
30
|
"msgId": msg_id,
|
|
31
|
-
|
|
31
|
+
# Important: some B01 methods use an empty object `{}` (not `[]`) for
|
|
32
|
+
# "no params", and some setters legitimately send `0` which is falsy.
|
|
33
|
+
# Only default to `[]` when params is actually None.
|
|
34
|
+
"params": params if params is not None else [],
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
}
|
|
@@ -280,6 +280,60 @@ class RoborockCommand(str, Enum):
|
|
|
280
280
|
class RoborockB01Q7Methods(StrEnum):
|
|
281
281
|
"""Methods used by the Roborock Q7 model."""
|
|
282
282
|
|
|
283
|
+
# NOTE: In the Q7 Hermes dump these appear as suffixes and are also used
|
|
284
|
+
# with an "event." prefix at runtime (see `hermes/.../module_524.js`).
|
|
285
|
+
ADD_CLEAN_FAILED_POST = "add_clean_failed.post"
|
|
286
|
+
EVENT_ADD_CLEAN_FAILED_POST = "event.add_clean_failed.post"
|
|
287
|
+
CLEAN_FINISH_POST = "clean_finish.post"
|
|
288
|
+
EVENT_CLEAN_FINISH_POST = "event.clean_finish.post"
|
|
289
|
+
EVENT_BUILD_MAP_FINISH_POST = "event.BuildMapFinish.post"
|
|
290
|
+
EVENT_MAP_CHANGE_POST = "event.map_change.post"
|
|
291
|
+
EVENT_WORK_APPOINT_CLEAN_FAILED_POST = "event.work_appoint_clean_failed.post"
|
|
292
|
+
START_CLEAN_POST = "startClean.post"
|
|
293
|
+
ADD_ORDER = "service.add_order"
|
|
294
|
+
ADD_SWEEP_CLEAN = "service.add_sweep_clean"
|
|
295
|
+
ARRANGE_ROOM = "service.arrange_room"
|
|
296
|
+
DEL_MAP = "service.del_map"
|
|
297
|
+
DEL_ORDER = "service.del_order"
|
|
298
|
+
DEL_ORDERS = "service.del_orders"
|
|
299
|
+
DELETE_RECORD_BY_URL = "service.delete_record_by_url"
|
|
300
|
+
DOWNLOAD_VOICE_TYPE = "service.download_voice_type"
|
|
301
|
+
ERASE_PREFERENCE = "service.erase_preference"
|
|
302
|
+
FIND_DEVICE = "service.find_device"
|
|
303
|
+
GET_ROOM_ORDER = "service.get_room_order"
|
|
304
|
+
GET_VOICE_DOWNLOAD = "service.get_voice_download"
|
|
305
|
+
HELLO_WIKKA = "service.hello_wikka"
|
|
306
|
+
RENAME_MAP = "service.rename_map"
|
|
307
|
+
RENAME_ROOM = "service.rename_room"
|
|
308
|
+
RENAME_ROOMS = "service.rename_rooms"
|
|
309
|
+
REPLACE_MAP = "service.replace_map"
|
|
310
|
+
RESET_CONSUMABLE = "service.reset_consumable"
|
|
311
|
+
SAVE_CARPET = "service.save_carpet"
|
|
312
|
+
SAVE_RECOMMEND_FB = "service.save_recommend_fb"
|
|
313
|
+
SAVE_SILL = "service.save_sill"
|
|
314
|
+
SET_AREA_START = "service.set_area_start"
|
|
315
|
+
SET_AREAS_START = "service.set_areas_start"
|
|
316
|
+
SET_CUR_MAP = "service.set_cur_map"
|
|
317
|
+
SET_DIRECTION = "service.set_direction"
|
|
318
|
+
SET_GLOBAL_SORT = "service.set_global_sort"
|
|
319
|
+
SET_MAP_HIDE = "service.set_map_hide"
|
|
320
|
+
SET_MULTI_ROOM_MATERIAL = "service.set_multi_room_material"
|
|
321
|
+
SET_POINT_CLEAN = "service.set_point_clean"
|
|
322
|
+
SET_PREFERENCE = "service.set_preference"
|
|
323
|
+
SET_PREFERENCE_TYPE = "service.set_preference_type"
|
|
324
|
+
SET_QUIET_TIME = "service.set_quiet_time"
|
|
325
|
+
SET_ROOM_CLEAN = "service.set_room_clean"
|
|
326
|
+
SET_ROOM_ORDER = "service.set_room_order"
|
|
327
|
+
SET_VIRTUAL_WALL = "service.set_virtual_wall"
|
|
328
|
+
SET_ZONE_CLEAN = "service.set_zone_clean"
|
|
329
|
+
SET_ZONE_POINTS = "service.set_zone_points"
|
|
330
|
+
SPLIT_ROOM = "service.split_room"
|
|
331
|
+
START_EXPLORE = "service.start_explore"
|
|
332
|
+
START_POINT_CLEAN = "service.start_point_clean"
|
|
333
|
+
START_RECHARGE = "service.start_recharge"
|
|
334
|
+
STOP_RECHARGE = "service.stop_recharge"
|
|
335
|
+
UPLOAD_BY_MAPID = "service.upload_by_mapid"
|
|
336
|
+
UPLOAD_RECORD_BY_URL = "service.upload_record_by_url"
|
|
283
337
|
GET_PROP = "prop.get"
|
|
284
338
|
GET_MAP_LIST = "service.get_map_list"
|
|
285
339
|
UPLOAD_BY_MAPTYPE = "service.upload_by_maptype"
|
|
@@ -287,7 +341,6 @@ class RoborockB01Q7Methods(StrEnum):
|
|
|
287
341
|
GET_PREFERENCE = "service.get_preference"
|
|
288
342
|
GET_RECORD_LIST = "service.get_record_list"
|
|
289
343
|
GET_ORDER = "service.get_order"
|
|
290
|
-
EVENT_ORDER_LIST_POST = "event.order_list.post"
|
|
291
344
|
POST_PROP = "prop.post"
|
|
292
345
|
|
|
293
346
|
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
"""Thin wrapper around the MQTT channel for Roborock B01 devices."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import json
|
|
7
|
-
import logging
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from roborock.exceptions import RoborockException
|
|
11
|
-
from roborock.protocols.b01_protocol import (
|
|
12
|
-
CommandType,
|
|
13
|
-
ParamsType,
|
|
14
|
-
decode_rpc_response,
|
|
15
|
-
encode_mqtt_payload,
|
|
16
|
-
)
|
|
17
|
-
from roborock.roborock_message import RoborockMessage
|
|
18
|
-
from roborock.util import get_next_int
|
|
19
|
-
|
|
20
|
-
from .mqtt_channel import MqttChannel
|
|
21
|
-
|
|
22
|
-
_LOGGER = logging.getLogger(__name__)
|
|
23
|
-
_TIMEOUT = 10.0
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
async def send_decoded_command(
|
|
27
|
-
mqtt_channel: MqttChannel,
|
|
28
|
-
dps: int,
|
|
29
|
-
command: CommandType,
|
|
30
|
-
params: ParamsType,
|
|
31
|
-
) -> dict[str, Any]:
|
|
32
|
-
"""Send a command on the MQTT channel and get a decoded response."""
|
|
33
|
-
_LOGGER.debug("Sending MQTT command: %s", params)
|
|
34
|
-
msg_id = str(get_next_int(100000000000, 999999999999))
|
|
35
|
-
roborock_message = encode_mqtt_payload(dps, command, params, msg_id)
|
|
36
|
-
future: asyncio.Future[dict[str, Any]] = asyncio.get_running_loop().create_future()
|
|
37
|
-
|
|
38
|
-
def find_response(response_message: RoborockMessage) -> None:
|
|
39
|
-
"""Handle incoming messages and resolve the future."""
|
|
40
|
-
try:
|
|
41
|
-
decoded_dps = decode_rpc_response(response_message)
|
|
42
|
-
except RoborockException as ex:
|
|
43
|
-
_LOGGER.info("Failed to decode b01 message: %s: %s", response_message, ex)
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
for dps_value in decoded_dps.values():
|
|
47
|
-
# valid responses are JSON strings wrapped in the dps value
|
|
48
|
-
if not isinstance(dps_value, str):
|
|
49
|
-
_LOGGER.debug("Received unexpected response: %s", dps_value)
|
|
50
|
-
continue
|
|
51
|
-
|
|
52
|
-
try:
|
|
53
|
-
inner = json.loads(dps_value)
|
|
54
|
-
except (json.JSONDecodeError, TypeError):
|
|
55
|
-
_LOGGER.debug("Received unexpected response: %s", dps_value)
|
|
56
|
-
continue
|
|
57
|
-
|
|
58
|
-
if isinstance(inner, dict) and inner.get("msgId") == msg_id:
|
|
59
|
-
_LOGGER.debug("Received query response: %s", inner)
|
|
60
|
-
data = inner.get("data")
|
|
61
|
-
if not future.done():
|
|
62
|
-
if isinstance(data, dict):
|
|
63
|
-
future.set_result(data)
|
|
64
|
-
else:
|
|
65
|
-
future.set_exception(RoborockException(f"Unexpected data type for response: {data}"))
|
|
66
|
-
|
|
67
|
-
unsub = await mqtt_channel.subscribe(find_response)
|
|
68
|
-
|
|
69
|
-
_LOGGER.debug("Sending MQTT message: %s", roborock_message)
|
|
70
|
-
try:
|
|
71
|
-
await mqtt_channel.publish(roborock_message)
|
|
72
|
-
try:
|
|
73
|
-
return await asyncio.wait_for(future, timeout=_TIMEOUT)
|
|
74
|
-
except TimeoutError as ex:
|
|
75
|
-
raise RoborockException(f"Command timed out after {_TIMEOUT}s") from ex
|
|
76
|
-
finally:
|
|
77
|
-
unsub()
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"""Traits for Q7 B01 devices.
|
|
2
|
-
Potentially other devices may fall into this category in the future."""
|
|
3
|
-
|
|
4
|
-
from roborock import B01Props
|
|
5
|
-
from roborock.devices.b01_channel import send_decoded_command
|
|
6
|
-
from roborock.devices.mqtt_channel import MqttChannel
|
|
7
|
-
from roborock.devices.traits import Trait
|
|
8
|
-
from roborock.roborock_message import RoborockB01Props
|
|
9
|
-
from roborock.roborock_typing import RoborockB01Q7Methods
|
|
10
|
-
|
|
11
|
-
__all__ = [
|
|
12
|
-
"Q7PropertiesApi",
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Q7PropertiesApi(Trait):
|
|
17
|
-
"""API for interacting with B01 devices."""
|
|
18
|
-
|
|
19
|
-
def __init__(self, channel: MqttChannel) -> None:
|
|
20
|
-
"""Initialize the B01Props API."""
|
|
21
|
-
self._channel = channel
|
|
22
|
-
|
|
23
|
-
async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
|
|
24
|
-
"""Query the device for the values of the given Q7 properties."""
|
|
25
|
-
result = await send_decoded_command(
|
|
26
|
-
self._channel, dps=10000, command=RoborockB01Q7Methods.GET_PROP, params={"property": props}
|
|
27
|
-
)
|
|
28
|
-
return B01Props.from_dict(result)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def create(channel: MqttChannel) -> Q7PropertiesApi:
|
|
32
|
-
"""Create traits for B01 devices."""
|
|
33
|
-
return Q7PropertiesApi(channel)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q10/b01_q10_code_mappings.py
RENAMED
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/data/b01_q10/b01_q10_containers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/b01/q10/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/device_features.py
RENAMED
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/dust_collection_mode.py
RENAMED
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/flow_led_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/network_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/smart_wash_params.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/devices/traits/v1/wash_towel_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_1_apis/roborock_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-3.19.0 → python_roborock-3.20.0}/roborock/version_a01_apis/roborock_client_a01.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|