python-roborock 4.21.0__tar.gz → 4.23.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-4.21.0 → python_roborock-4.23.0}/PKG-INFO +1 -1
- {python_roborock-4.21.0 → python_roborock-4.23.0}/pyproject.toml +1 -1
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/b01_q7/b01_q7_containers.py +27 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/rpc/b01_q7_channel.py +76 -34
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q7/__init__.py +22 -2
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q7/clean_summary.py +2 -2
- python_roborock-4.23.0/roborock/devices/traits/b01/q7/map.py +59 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/child_lock.py +1 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/clean_summary.py +37 -30
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/common.py +65 -62
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/consumeable.py +1 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/device_features.py +24 -14
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/do_not_disturb.py +1 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/dust_collection_mode.py +1 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/flow_led_status.py +1 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/home.py +2 -6
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/led_status.py +18 -14
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/map_content.py +20 -11
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/maps.py +19 -15
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/network_info.py +12 -7
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/rooms.py +56 -42
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/smart_wash_params.py +1 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/status.py +1 -9
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/valley_electricity_timer.py +1 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/volume.py +5 -7
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/wash_towel_mode.py +1 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/protocols/b01_q7_protocol.py +1 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/.gitignore +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/LICENSE +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/README.md +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/callbacks.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/cli.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/const.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/code_mappings.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/containers.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/device_features.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/README.md +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/cache.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/device.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/device_manager.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/file_cache.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/rpc/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/rpc/a01_channel.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/rpc/v1_channel.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/command.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/common.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/status.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/transport/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/transport/channel.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/transport/local_channel.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/transport/mqtt_channel.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/diagnostics.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/exceptions.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/map/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/map/map_parser.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/protocol.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/protocols/__init__.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/protocols/b01_q10_protocol.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/py.typed +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/roborock_message.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/util.py +0 -0
- {python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.23.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 = "4.
|
|
3
|
+
version = "4.23.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"
|
|
@@ -75,6 +75,33 @@ class Recommend(RoborockBase):
|
|
|
75
75
|
room_id: list[int] = field(default_factory=list)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
@dataclass
|
|
79
|
+
class Q7MapListEntry(RoborockBase):
|
|
80
|
+
"""Single map list entry returned by `service.get_map_list`."""
|
|
81
|
+
|
|
82
|
+
id: int | None = None
|
|
83
|
+
cur: bool | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class Q7MapList(RoborockBase):
|
|
88
|
+
"""Map list response returned by `service.get_map_list`."""
|
|
89
|
+
|
|
90
|
+
map_list: list[Q7MapListEntry] = field(default_factory=list)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def current_map_id(self) -> int | None:
|
|
94
|
+
"""Current map id, preferring the entry marked current."""
|
|
95
|
+
if not self.map_list:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
ordered = sorted(self.map_list, key=lambda entry: entry.cur or False, reverse=True)
|
|
99
|
+
first = next(iter(ordered), None)
|
|
100
|
+
if first is None or not isinstance(first.id, int):
|
|
101
|
+
return None
|
|
102
|
+
return first.id
|
|
103
|
+
|
|
104
|
+
|
|
78
105
|
@dataclass
|
|
79
106
|
class B01Props(RoborockBase):
|
|
80
107
|
"""
|
|
@@ -5,31 +5,67 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
-
from
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Any, TypeVar
|
|
9
10
|
|
|
10
11
|
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
11
12
|
from roborock.exceptions import RoborockException
|
|
12
|
-
from roborock.protocols.b01_q7_protocol import
|
|
13
|
-
|
|
14
|
-
decode_rpc_response,
|
|
15
|
-
encode_mqtt_payload,
|
|
16
|
-
)
|
|
17
|
-
from roborock.roborock_message import RoborockMessage
|
|
13
|
+
from roborock.protocols.b01_q7_protocol import B01_VERSION, Q7RequestMessage, decode_rpc_response, encode_mqtt_payload
|
|
14
|
+
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
|
|
18
15
|
|
|
19
16
|
_LOGGER = logging.getLogger(__name__)
|
|
20
17
|
_TIMEOUT = 10.0
|
|
18
|
+
_T = TypeVar("_T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _matches_map_response(response_message: RoborockMessage, *, version: bytes | None) -> bytes | None:
|
|
22
|
+
"""Return raw map payload bytes for matching MAP_RESPONSE messages."""
|
|
23
|
+
if (
|
|
24
|
+
response_message.protocol == RoborockMessageProtocol.MAP_RESPONSE
|
|
25
|
+
and response_message.payload
|
|
26
|
+
and response_message.version == version
|
|
27
|
+
):
|
|
28
|
+
return response_message.payload
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def _send_command(
|
|
33
|
+
mqtt_channel: MqttChannel,
|
|
34
|
+
request_message: Q7RequestMessage,
|
|
35
|
+
*,
|
|
36
|
+
response_matcher: Callable[[RoborockMessage], _T | None],
|
|
37
|
+
) -> _T:
|
|
38
|
+
"""Publish a B01 command and resolve on the first matching response."""
|
|
39
|
+
roborock_message = encode_mqtt_payload(request_message)
|
|
40
|
+
future: asyncio.Future[_T] = asyncio.get_running_loop().create_future()
|
|
41
|
+
|
|
42
|
+
def on_message(response_message: RoborockMessage) -> None:
|
|
43
|
+
if future.done():
|
|
44
|
+
return
|
|
45
|
+
try:
|
|
46
|
+
response = response_matcher(response_message)
|
|
47
|
+
except Exception as ex:
|
|
48
|
+
future.set_exception(ex)
|
|
49
|
+
return
|
|
50
|
+
if response is not None:
|
|
51
|
+
future.set_result(response)
|
|
52
|
+
|
|
53
|
+
unsub = await mqtt_channel.subscribe(on_message)
|
|
54
|
+
try:
|
|
55
|
+
await mqtt_channel.publish(roborock_message)
|
|
56
|
+
return await asyncio.wait_for(future, timeout=_TIMEOUT)
|
|
57
|
+
finally:
|
|
58
|
+
unsub()
|
|
21
59
|
|
|
22
60
|
|
|
23
61
|
async def send_decoded_command(
|
|
24
62
|
mqtt_channel: MqttChannel,
|
|
25
63
|
request_message: Q7RequestMessage,
|
|
26
|
-
) ->
|
|
64
|
+
) -> Any:
|
|
27
65
|
"""Send a command on the MQTT channel and get a decoded response."""
|
|
28
66
|
_LOGGER.debug("Sending B01 MQTT command: %s", request_message)
|
|
29
|
-
roborock_message = encode_mqtt_payload(request_message)
|
|
30
|
-
future: asyncio.Future[Any] = asyncio.get_running_loop().create_future()
|
|
31
67
|
|
|
32
|
-
def find_response(response_message: RoborockMessage) -> None:
|
|
68
|
+
def find_response(response_message: RoborockMessage) -> Any | None:
|
|
33
69
|
"""Handle incoming messages and resolve the future."""
|
|
34
70
|
try:
|
|
35
71
|
decoded_dps = decode_rpc_response(response_message)
|
|
@@ -41,7 +77,7 @@ async def send_decoded_command(
|
|
|
41
77
|
response_message,
|
|
42
78
|
ex,
|
|
43
79
|
)
|
|
44
|
-
return
|
|
80
|
+
return None
|
|
45
81
|
for dps_value in decoded_dps.values():
|
|
46
82
|
# valid responses are JSON strings wrapped in the dps value
|
|
47
83
|
if not isinstance(dps_value, str):
|
|
@@ -55,31 +91,23 @@ async def send_decoded_command(
|
|
|
55
91
|
continue
|
|
56
92
|
if isinstance(inner, dict) and inner.get("msgId") == str(request_message.msg_id):
|
|
57
93
|
_LOGGER.debug("Received query response: %s", inner)
|
|
58
|
-
# Check for error code (0 = success, non-zero = error)
|
|
59
94
|
code = inner.get("code", 0)
|
|
60
95
|
if code != 0:
|
|
61
96
|
error_msg = f"B01 command failed with code {code} ({request_message})"
|
|
62
97
|
_LOGGER.debug("B01 error response: %s", error_msg)
|
|
63
|
-
|
|
64
|
-
future.set_exception(RoborockException(error_msg))
|
|
65
|
-
return
|
|
98
|
+
raise RoborockException(error_msg)
|
|
66
99
|
data = inner.get("data")
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
)
|
|
73
|
-
return
|
|
74
|
-
if not future.done():
|
|
75
|
-
future.set_result(data)
|
|
76
|
-
|
|
77
|
-
unsub = await mqtt_channel.subscribe(find_response)
|
|
78
|
-
|
|
79
|
-
_LOGGER.debug("Sending MQTT message: %s", roborock_message)
|
|
100
|
+
if request_message.command == "prop.get" and not isinstance(data, dict):
|
|
101
|
+
raise RoborockException(f"Unexpected data type for response {data} ({request_message})")
|
|
102
|
+
return data
|
|
103
|
+
return None
|
|
104
|
+
|
|
80
105
|
try:
|
|
81
|
-
await
|
|
82
|
-
|
|
106
|
+
return await _send_command(
|
|
107
|
+
mqtt_channel,
|
|
108
|
+
request_message,
|
|
109
|
+
response_matcher=find_response,
|
|
110
|
+
)
|
|
83
111
|
except TimeoutError as ex:
|
|
84
112
|
raise RoborockException(f"B01 command timed out after {_TIMEOUT}s ({request_message})") from ex
|
|
85
113
|
except RoborockException as ex:
|
|
@@ -89,7 +117,6 @@ async def send_decoded_command(
|
|
|
89
117
|
ex,
|
|
90
118
|
)
|
|
91
119
|
raise
|
|
92
|
-
|
|
93
120
|
except Exception as ex:
|
|
94
121
|
_LOGGER.exception(
|
|
95
122
|
"Error sending B01 decoded command (%ss): %s",
|
|
@@ -97,5 +124,20 @@ async def send_decoded_command(
|
|
|
97
124
|
ex,
|
|
98
125
|
)
|
|
99
126
|
raise
|
|
100
|
-
|
|
101
|
-
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def send_map_command(mqtt_channel: MqttChannel, request_message: Q7RequestMessage) -> bytes:
|
|
130
|
+
"""Send map upload command and wait for MAP_RESPONSE payload bytes.
|
|
131
|
+
|
|
132
|
+
This stays separate from ``send_decoded_command()`` because map uploads arrive as
|
|
133
|
+
raw ``MAP_RESPONSE`` payload bytes instead of a decoded RPC ``data`` payload.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
return await _send_command(
|
|
138
|
+
mqtt_channel,
|
|
139
|
+
request_message,
|
|
140
|
+
response_matcher=lambda response_message: _matches_map_response(response_message, version=B01_VERSION),
|
|
141
|
+
)
|
|
142
|
+
except TimeoutError as ex:
|
|
143
|
+
raise RoborockException(f"B01 map command timed out after {_TIMEOUT}s ({request_message})") from ex
|
{python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q7/__init__.py
RENAMED
|
@@ -4,6 +4,7 @@ Potentially other devices may fall into this category in the future."""
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from roborock import B01Props
|
|
7
|
+
from roborock.data import Q7MapList, Q7MapListEntry
|
|
7
8
|
from roborock.data.b01_q7.b01_q7_code_mappings import (
|
|
8
9
|
CleanPathPreferenceMapping,
|
|
9
10
|
CleanRepeatMapping,
|
|
@@ -16,15 +17,19 @@ from roborock.data.b01_q7.b01_q7_code_mappings import (
|
|
|
16
17
|
from roborock.devices.rpc.b01_q7_channel import send_decoded_command
|
|
17
18
|
from roborock.devices.traits import Trait
|
|
18
19
|
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
19
|
-
from roborock.protocols.b01_q7_protocol import CommandType, ParamsType, Q7RequestMessage
|
|
20
|
+
from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, CommandType, ParamsType, Q7RequestMessage
|
|
20
21
|
from roborock.roborock_message import RoborockB01Props
|
|
21
22
|
from roborock.roborock_typing import RoborockB01Q7Methods
|
|
22
23
|
|
|
23
24
|
from .clean_summary import CleanSummaryTrait
|
|
25
|
+
from .map import MapTrait
|
|
24
26
|
|
|
25
27
|
__all__ = [
|
|
26
28
|
"Q7PropertiesApi",
|
|
27
29
|
"CleanSummaryTrait",
|
|
30
|
+
"MapTrait",
|
|
31
|
+
"Q7MapList",
|
|
32
|
+
"Q7MapListEntry",
|
|
28
33
|
]
|
|
29
34
|
|
|
30
35
|
|
|
@@ -34,10 +39,14 @@ class Q7PropertiesApi(Trait):
|
|
|
34
39
|
clean_summary: CleanSummaryTrait
|
|
35
40
|
"""Trait for clean records / clean summary (Q7 `service.get_record_list`)."""
|
|
36
41
|
|
|
42
|
+
map: MapTrait
|
|
43
|
+
"""Trait for map list metadata + raw map payload retrieval."""
|
|
44
|
+
|
|
37
45
|
def __init__(self, channel: MqttChannel) -> None:
|
|
38
46
|
"""Initialize the B01Props API."""
|
|
39
47
|
self._channel = channel
|
|
40
48
|
self.clean_summary = CleanSummaryTrait(channel)
|
|
49
|
+
self.map = MapTrait(channel)
|
|
41
50
|
|
|
42
51
|
async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
|
|
43
52
|
"""Query the device for the values of the given Q7 properties."""
|
|
@@ -87,6 +96,17 @@ class Q7PropertiesApi(Trait):
|
|
|
87
96
|
},
|
|
88
97
|
)
|
|
89
98
|
|
|
99
|
+
async def clean_segments(self, segment_ids: list[int]) -> None:
|
|
100
|
+
"""Start segment cleaning for the given ids (Q7 uses room ids)."""
|
|
101
|
+
await self.send(
|
|
102
|
+
command=RoborockB01Q7Methods.SET_ROOM_CLEAN,
|
|
103
|
+
params={
|
|
104
|
+
"clean_type": CleanTaskTypeMapping.ROOM.code,
|
|
105
|
+
"ctrl_value": SCDeviceCleanParam.START.code,
|
|
106
|
+
"room_ids": segment_ids,
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
|
|
90
110
|
async def pause_clean(self) -> None:
|
|
91
111
|
"""Pause cleaning."""
|
|
92
112
|
await self.send(
|
|
@@ -127,7 +147,7 @@ class Q7PropertiesApi(Trait):
|
|
|
127
147
|
"""Send a command to the device."""
|
|
128
148
|
return await send_decoded_command(
|
|
129
149
|
self._channel,
|
|
130
|
-
Q7RequestMessage(dps=
|
|
150
|
+
Q7RequestMessage(dps=B01_Q7_DPS, command=command, params=params),
|
|
131
151
|
)
|
|
132
152
|
|
|
133
153
|
|
{python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/b01/q7/clean_summary.py
RENAMED
|
@@ -13,7 +13,7 @@ from roborock.devices.rpc.b01_q7_channel import send_decoded_command
|
|
|
13
13
|
from roborock.devices.traits import Trait
|
|
14
14
|
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
15
15
|
from roborock.exceptions import RoborockException
|
|
16
|
-
from roborock.protocols.b01_q7_protocol import Q7RequestMessage
|
|
16
|
+
from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, Q7RequestMessage
|
|
17
17
|
from roborock.roborock_typing import RoborockB01Q7Methods
|
|
18
18
|
|
|
19
19
|
__all__ = [
|
|
@@ -50,7 +50,7 @@ class CleanSummaryTrait(CleanRecordSummary, Trait):
|
|
|
50
50
|
"""Fetch the raw device clean record list (`service.get_record_list`)."""
|
|
51
51
|
result = await send_decoded_command(
|
|
52
52
|
self._channel,
|
|
53
|
-
Q7RequestMessage(dps=
|
|
53
|
+
Q7RequestMessage(dps=B01_Q7_DPS, command=RoborockB01Q7Methods.GET_RECORD_LIST, params={}),
|
|
54
54
|
)
|
|
55
55
|
|
|
56
56
|
if not isinstance(result, dict):
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Map trait for B01 Q7 devices."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from roborock.data import Q7MapList
|
|
6
|
+
from roborock.devices.rpc.b01_q7_channel import send_decoded_command, send_map_command
|
|
7
|
+
from roborock.devices.traits import Trait
|
|
8
|
+
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
9
|
+
from roborock.exceptions import RoborockException
|
|
10
|
+
from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, Q7RequestMessage
|
|
11
|
+
from roborock.roborock_typing import RoborockB01Q7Methods
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MapTrait(Q7MapList, Trait):
|
|
15
|
+
"""Map retrieval + map metadata helpers for Q7 devices."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, channel: MqttChannel) -> None:
|
|
18
|
+
super().__init__()
|
|
19
|
+
self._channel = channel
|
|
20
|
+
# Map uploads are serialized per-device to avoid response cross-wiring.
|
|
21
|
+
self._map_command_lock = asyncio.Lock()
|
|
22
|
+
self._loaded = False
|
|
23
|
+
|
|
24
|
+
async def refresh(self) -> None:
|
|
25
|
+
"""Refresh cached map list metadata from the device."""
|
|
26
|
+
response = await send_decoded_command(
|
|
27
|
+
self._channel,
|
|
28
|
+
Q7RequestMessage(dps=B01_Q7_DPS, command=RoborockB01Q7Methods.GET_MAP_LIST, params={}),
|
|
29
|
+
)
|
|
30
|
+
if not isinstance(response, dict):
|
|
31
|
+
raise RoborockException(
|
|
32
|
+
f"Unexpected response type for GET_MAP_LIST: {type(response).__name__}: {response!r}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if (parsed := Q7MapList.from_dict(response)) is None:
|
|
36
|
+
raise RoborockException(f"Failed to decode map list response: {response!r}")
|
|
37
|
+
|
|
38
|
+
self.map_list = parsed.map_list
|
|
39
|
+
self._loaded = True
|
|
40
|
+
|
|
41
|
+
async def _get_map_payload(self, *, map_id: int) -> bytes:
|
|
42
|
+
"""Fetch raw map payload bytes for the given map id."""
|
|
43
|
+
request = Q7RequestMessage(
|
|
44
|
+
dps=B01_Q7_DPS,
|
|
45
|
+
command=RoborockB01Q7Methods.UPLOAD_BY_MAPID,
|
|
46
|
+
params={"map_id": map_id},
|
|
47
|
+
)
|
|
48
|
+
async with self._map_command_lock:
|
|
49
|
+
return await send_map_command(self._channel, request)
|
|
50
|
+
|
|
51
|
+
async def get_current_map_payload(self) -> bytes:
|
|
52
|
+
"""Fetch raw map payload bytes for the currently selected map."""
|
|
53
|
+
if not self._loaded:
|
|
54
|
+
await self.refresh()
|
|
55
|
+
|
|
56
|
+
map_id = self.current_map_id
|
|
57
|
+
if map_id is None:
|
|
58
|
+
raise RoborockException(f"Unable to determine map_id from map list response: {self!r}")
|
|
59
|
+
return await self._get_map_payload(map_id=map_id)
|
|
@@ -9,6 +9,7 @@ class ChildLockTrait(ChildLockStatus, common.V1TraitMixin, common.RoborockSwitch
|
|
|
9
9
|
"""Trait for controlling the child lock of a Roborock device."""
|
|
10
10
|
|
|
11
11
|
command = RoborockCommand.GET_CHILD_LOCK_STATUS
|
|
12
|
+
converter = common.DefaultConverter(ChildLockStatus)
|
|
12
13
|
requires_feature = "is_set_child_supported"
|
|
13
14
|
|
|
14
15
|
@property
|
{python_roborock-4.21.0 → python_roborock-4.23.0}/roborock/devices/traits/v1/clean_summary.py
RENAMED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Self
|
|
3
2
|
|
|
4
|
-
from roborock.data import CleanRecord, CleanSummaryWithDetail
|
|
3
|
+
from roborock.data import CleanRecord, CleanSummaryWithDetail, RoborockBase
|
|
5
4
|
from roborock.devices.traits.v1 import common
|
|
6
5
|
from roborock.roborock_typing import RoborockCommand
|
|
7
6
|
from roborock.util import unpack_list
|
|
@@ -9,48 +8,30 @@ from roborock.util import unpack_list
|
|
|
9
8
|
_LOGGER = logging.getLogger(__name__)
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
class
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
command = RoborockCommand.GET_CLEAN_SUMMARY
|
|
16
|
-
|
|
17
|
-
async def refresh(self) -> None:
|
|
18
|
-
"""Refresh the clean summary data and last clean record.
|
|
19
|
-
|
|
20
|
-
Assumes that the clean summary has already been fetched.
|
|
21
|
-
"""
|
|
22
|
-
await super().refresh()
|
|
23
|
-
if not self.records:
|
|
24
|
-
_LOGGER.debug("No clean records available in clean summary.")
|
|
25
|
-
self.last_clean_record = None
|
|
26
|
-
return
|
|
27
|
-
last_record_id = self.records[0]
|
|
28
|
-
self.last_clean_record = await self.get_clean_record(last_record_id)
|
|
11
|
+
class CleanSummaryConverter(common.V1TraitDataConverter):
|
|
12
|
+
"""Converter for CleanSummaryWithDetail objects."""
|
|
29
13
|
|
|
30
|
-
|
|
31
|
-
def _parse_type_response(cls, response: common.V1ResponseData) -> Self:
|
|
14
|
+
def convert(self, response: common.V1ResponseData) -> RoborockBase:
|
|
32
15
|
"""Parse the response from the device into a CleanSummary."""
|
|
33
16
|
if isinstance(response, dict):
|
|
34
|
-
return
|
|
17
|
+
return CleanSummaryWithDetail.from_dict(response)
|
|
35
18
|
elif isinstance(response, list):
|
|
36
19
|
clean_time, clean_area, clean_count, records = unpack_list(response, 4)
|
|
37
|
-
return
|
|
20
|
+
return CleanSummaryWithDetail(
|
|
38
21
|
clean_time=clean_time,
|
|
39
22
|
clean_area=clean_area,
|
|
40
23
|
clean_count=clean_count,
|
|
41
24
|
records=records,
|
|
42
25
|
)
|
|
43
26
|
elif isinstance(response, int):
|
|
44
|
-
return
|
|
27
|
+
return CleanSummaryWithDetail(clean_time=response)
|
|
45
28
|
raise ValueError(f"Unexpected clean summary format: {response!r}")
|
|
46
29
|
|
|
47
|
-
async def get_clean_record(self, record_id: int) -> CleanRecord:
|
|
48
|
-
"""Load a specific clean record by ID."""
|
|
49
|
-
response = await self.rpc_channel.send_command(RoborockCommand.GET_CLEAN_RECORD, params=[record_id])
|
|
50
|
-
return self._parse_clean_record_response(response)
|
|
51
30
|
|
|
52
|
-
|
|
53
|
-
|
|
31
|
+
class CleanRecordConverter(common.V1TraitDataConverter):
|
|
32
|
+
"""Convert server responses to a CleanRecord."""
|
|
33
|
+
|
|
34
|
+
def convert(self, response: common.V1ResponseData) -> CleanRecord:
|
|
54
35
|
"""Parse the response from the device into a CleanRecord."""
|
|
55
36
|
if isinstance(response, list) and len(response) == 1:
|
|
56
37
|
response = response[0]
|
|
@@ -81,3 +62,29 @@ class CleanSummaryTrait(CleanSummaryWithDetail, common.V1TraitMixin):
|
|
|
81
62
|
begin, end, duration, area = unpack_list(response, 4)
|
|
82
63
|
return CleanRecord(begin=begin, end=end, duration=duration, area=area)
|
|
83
64
|
raise ValueError(f"Unexpected clean record format: {response!r}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CleanSummaryTrait(CleanSummaryWithDetail, common.V1TraitMixin):
|
|
68
|
+
"""Trait for managing the clean summary of Roborock devices."""
|
|
69
|
+
|
|
70
|
+
command = RoborockCommand.GET_CLEAN_SUMMARY
|
|
71
|
+
converter = CleanSummaryConverter()
|
|
72
|
+
clean_record_converter = CleanRecordConverter()
|
|
73
|
+
|
|
74
|
+
async def refresh(self) -> None:
|
|
75
|
+
"""Refresh the clean summary data and last clean record.
|
|
76
|
+
|
|
77
|
+
Assumes that the clean summary has already been fetched.
|
|
78
|
+
"""
|
|
79
|
+
await super().refresh()
|
|
80
|
+
if not self.records:
|
|
81
|
+
_LOGGER.debug("No clean records available in clean summary.")
|
|
82
|
+
self.last_clean_record = None
|
|
83
|
+
return
|
|
84
|
+
last_record_id = self.records[0]
|
|
85
|
+
self.last_clean_record = await self.get_clean_record(last_record_id)
|
|
86
|
+
|
|
87
|
+
async def get_clean_record(self, record_id: int) -> CleanRecord:
|
|
88
|
+
"""Load a specific clean record by ID."""
|
|
89
|
+
response = await self.rpc_channel.send_command(RoborockCommand.GET_CLEAN_RECORD, params=[record_id])
|
|
90
|
+
return self.clean_record_converter.convert(response)
|
|
@@ -5,8 +5,8 @@ This is an internal library and should not be used directly by consumers.
|
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
|
-
from dataclasses import
|
|
9
|
-
from typing import ClassVar
|
|
8
|
+
from dataclasses import fields
|
|
9
|
+
from typing import ClassVar
|
|
10
10
|
|
|
11
11
|
from roborock.data import RoborockBase
|
|
12
12
|
from roborock.protocols.v1_protocol import V1RpcChannel
|
|
@@ -14,10 +14,24 @@ from roborock.roborock_typing import RoborockCommand
|
|
|
14
14
|
|
|
15
15
|
_LOGGER = logging.getLogger(__name__)
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
V1ResponseData = dict | list | int | str
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
class V1TraitDataConverter(ABC):
|
|
22
|
+
"""Converts responses to RoborockBase objects.
|
|
23
|
+
|
|
24
|
+
This is an internal class and should not be used directly by consumers.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def convert(self, response: V1ResponseData) -> RoborockBase:
|
|
29
|
+
"""Convert the values to a dict that can be parsed as a RoborockBase."""
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
return self.__class__.__name__
|
|
33
|
+
|
|
34
|
+
|
|
21
35
|
class V1TraitMixin(ABC):
|
|
22
36
|
"""Base model that supports v1 traits.
|
|
23
37
|
|
|
@@ -42,37 +56,13 @@ class V1TraitMixin(ABC):
|
|
|
42
56
|
"""
|
|
43
57
|
|
|
44
58
|
command: ClassVar[RoborockCommand]
|
|
59
|
+
"""The RoborockCommand used to fetch the trait data from the device (internal only)."""
|
|
45
60
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"""Parse the response from the device into a a RoborockBase.
|
|
49
|
-
|
|
50
|
-
Subclasses should override this method to implement custom parsing
|
|
51
|
-
logic as needed.
|
|
52
|
-
"""
|
|
53
|
-
if not issubclass(cls, RoborockBase):
|
|
54
|
-
raise NotImplementedError(f"Trait {cls} does not implement RoborockBase")
|
|
55
|
-
# Subclasses can override to implement custom parsing logic
|
|
56
|
-
if isinstance(response, list):
|
|
57
|
-
response = response[0]
|
|
58
|
-
if not isinstance(response, dict):
|
|
59
|
-
raise ValueError(f"Unexpected {cls} response format: {response!r}")
|
|
60
|
-
return cls.from_dict(response)
|
|
61
|
-
|
|
62
|
-
def _parse_response(self, response: V1ResponseData) -> RoborockBase:
|
|
63
|
-
"""Parse the response from the device into a a RoborockBase.
|
|
64
|
-
|
|
65
|
-
This is used by subclasses that want to override the class
|
|
66
|
-
behavior with instance-specific data.
|
|
67
|
-
"""
|
|
68
|
-
return self._parse_type_response(response)
|
|
69
|
-
|
|
70
|
-
def __post_init__(self) -> None:
|
|
71
|
-
"""Post-initialization to set up the RPC channel.
|
|
61
|
+
converter: V1TraitDataConverter
|
|
62
|
+
"""The converter used to parse the response from the device (internal only)."""
|
|
72
63
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"""
|
|
64
|
+
def __init__(self) -> None:
|
|
65
|
+
"""Initialize the V1TraitMixin."""
|
|
76
66
|
self._rpc_channel = None
|
|
77
67
|
|
|
78
68
|
@property
|
|
@@ -85,32 +75,42 @@ class V1TraitMixin(ABC):
|
|
|
85
75
|
async def refresh(self) -> None:
|
|
86
76
|
"""Refresh the contents of this trait."""
|
|
87
77
|
response = await self.rpc_channel.send_command(self.command)
|
|
88
|
-
new_data = self.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
78
|
+
new_data = self.converter.convert(response)
|
|
79
|
+
merge_trait_values(self, new_data) # type: ignore[arg-type]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def merge_trait_values(target: RoborockBase, new_object: RoborockBase) -> bool:
|
|
83
|
+
"""Update the target object with set fields in new_object."""
|
|
84
|
+
updated = False
|
|
85
|
+
for field in fields(new_object):
|
|
86
|
+
old_value = getattr(target, field.name, None)
|
|
87
|
+
new_value = getattr(new_object, field.name, None)
|
|
88
|
+
if new_value != old_value:
|
|
89
|
+
setattr(target, field.name, new_value)
|
|
90
|
+
updated = True
|
|
91
|
+
return updated
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DefaultConverter(V1TraitDataConverter):
|
|
95
|
+
"""Converts responses to RoborockBase objects."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, dataclass_type: type[RoborockBase]) -> None:
|
|
98
|
+
"""Initialize the converter."""
|
|
99
|
+
self._dataclass_type = dataclass_type
|
|
100
|
+
|
|
101
|
+
def convert(self, response: V1ResponseData) -> RoborockBase:
|
|
102
|
+
"""Convert the values to a dict that can be parsed as a RoborockBase.
|
|
103
|
+
|
|
104
|
+
Subclasses can override to implement custom parsing logic
|
|
105
|
+
"""
|
|
106
|
+
if isinstance(response, list):
|
|
107
|
+
response = response[0]
|
|
108
|
+
if not isinstance(response, dict):
|
|
109
|
+
raise ValueError(f"Unexpected {self._dataclass_type.__name__} response format: {response!r}")
|
|
110
|
+
return self._dataclass_type.from_dict(response)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class SingleValueConverter(DefaultConverter):
|
|
114
114
|
"""Base class for traits that represent a single value.
|
|
115
115
|
|
|
116
116
|
This class is intended to be subclassed by traits that represent a single
|
|
@@ -119,15 +119,18 @@ class RoborockValueBase(V1TraitMixin, RoborockBase):
|
|
|
119
119
|
represents the main value of the trait.
|
|
120
120
|
"""
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
def __init__(self, dataclass_type: type[RoborockBase], value_field: str) -> None:
|
|
123
|
+
"""Initialize the converter."""
|
|
124
|
+
super().__init__(dataclass_type)
|
|
125
|
+
self._value_field = value_field
|
|
126
|
+
|
|
127
|
+
def convert(self, response: V1ResponseData) -> RoborockBase:
|
|
124
128
|
"""Parse the response from the device into a RoborockValueBase."""
|
|
125
129
|
if isinstance(response, list):
|
|
126
130
|
response = response[0]
|
|
127
131
|
if not isinstance(response, int):
|
|
128
132
|
raise ValueError(f"Unexpected response format: {response!r}")
|
|
129
|
-
|
|
130
|
-
return cls(**{value_field: response})
|
|
133
|
+
return super().convert({self._value_field: response})
|
|
131
134
|
|
|
132
135
|
|
|
133
136
|
class RoborockSwitchBase(ABC):
|
|
@@ -41,6 +41,7 @@ class ConsumableTrait(Consumable, common.V1TraitMixin):
|
|
|
41
41
|
"""
|
|
42
42
|
|
|
43
43
|
command = RoborockCommand.GET_CONSUMABLE
|
|
44
|
+
converter = common.DefaultConverter(Consumable)
|
|
44
45
|
|
|
45
46
|
async def reset_consumable(self, consumable: ConsumableAttribute) -> None:
|
|
46
47
|
"""Reset a specific consumable attribute on the device."""
|