python-roborock 5.0.0__tar.gz → 5.2.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-5.0.0 → python_roborock-5.2.0}/PKG-INFO +3 -2
- {python_roborock-5.0.0 → python_roborock-5.2.0}/pyproject.toml +9 -2
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/broadcast_protocol.py +0 -2
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/code_mappings.py +6 -8
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/v1/v1_code_mappings.py +3 -1
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/device_features.py +2 -4
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/device_manager.py +1 -1
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/rpc/b01_q10_channel.py +0 -2
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/rpc/b01_q7_channel.py +39 -17
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/b01/q7/__init__.py +38 -10
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -2
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/b01/q7/map.py +6 -28
- python_roborock-5.2.0/roborock/devices/traits/b01/q7/map_content.py +100 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/network_info.py +0 -2
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/diagnostics.py +4 -6
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/exceptions.py +0 -2
- python_roborock-5.2.0/roborock/map/b01_map_parser.py +124 -0
- python_roborock-5.2.0/roborock/map/proto/__init__.py +1 -0
- python_roborock-5.2.0/roborock/map/proto/b01_scmap.proto +70 -0
- python_roborock-5.2.0/roborock/map/proto/b01_scmap_pb2.py +48 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/protocol.py +0 -2
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/protocols/b01_q7_protocol.py +50 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/roborock_message.py +3 -4
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/roborock_typing.py +2 -3
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/util.py +0 -2
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/web_api.py +0 -2
- {python_roborock-5.0.0 → python_roborock-5.2.0}/.gitignore +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/LICENSE +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/README.md +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/callbacks.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/cli.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/const.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/containers.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/README.md +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/cache.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/device.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/file_cache.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/rpc/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/rpc/a01_channel.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/rpc/v1_channel.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/b01/q10/command.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/b01/q10/common.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/b01/q10/status.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/common.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/transport/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/transport/channel.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/transport/local_channel.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/transport/mqtt_channel.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/map/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/map/map_parser.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/protocols/__init__.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/protocols/b01_q10_protocol.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/py.typed +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.0
|
|
4
4
|
Summary: A package to control Roborock vacuums.
|
|
5
|
-
Project-URL: Repository, https://github.com/
|
|
5
|
+
Project-URL: Repository, https://github.com/python-roborock/python-roborock
|
|
6
6
|
Project-URL: Documentation, https://python-roborock.readthedocs.io/
|
|
7
7
|
Author: Lash-L, allenporter
|
|
8
8
|
Author-email: humbertogontijo <humbertogontijo@users.noreply.github.com>
|
|
@@ -21,6 +21,7 @@ Requires-Dist: click-shell~=2.1
|
|
|
21
21
|
Requires-Dist: click>=8
|
|
22
22
|
Requires-Dist: construct<3,>=2.10.57
|
|
23
23
|
Requires-Dist: paho-mqtt<3.0.0,>=1.6.1
|
|
24
|
+
Requires-Dist: protobuf<7,>=5
|
|
24
25
|
Requires-Dist: pycryptodomex~=3.18; sys_platform == 'darwin'
|
|
25
26
|
Requires-Dist: pycryptodome~=3.18
|
|
26
27
|
Requires-Dist: pyrate-limiter<5,>=4.0.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-roborock"
|
|
3
|
-
version = "5.
|
|
3
|
+
version = "5.2.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"
|
|
@@ -25,6 +25,7 @@ dependencies = [
|
|
|
25
25
|
"pycryptodomex~=3.18 ; sys_platform == 'darwin'",
|
|
26
26
|
"paho-mqtt>=1.6.1,<3.0.0",
|
|
27
27
|
"construct>=2.10.57,<3",
|
|
28
|
+
"protobuf>=5,<7",
|
|
28
29
|
"vacuum-map-parser-roborock",
|
|
29
30
|
"pyrate-limiter>=4.0.0,<5",
|
|
30
31
|
"aiomqtt>=2.5.0,<3",
|
|
@@ -32,7 +33,7 @@ dependencies = [
|
|
|
32
33
|
]
|
|
33
34
|
|
|
34
35
|
[project.urls]
|
|
35
|
-
Repository = "https://github.com/
|
|
36
|
+
Repository = "https://github.com/python-roborock/python-roborock"
|
|
36
37
|
Documentation = "https://python-roborock.readthedocs.io/"
|
|
37
38
|
|
|
38
39
|
[project.scripts]
|
|
@@ -97,9 +98,15 @@ major_tags= ["refactor"]
|
|
|
97
98
|
lint.ignore = ["F403", "E741"]
|
|
98
99
|
lint.select=["E", "F", "UP", "I"]
|
|
99
100
|
line-length = 120
|
|
101
|
+
extend-exclude = ["roborock/map/proto/*_pb2.py"]
|
|
100
102
|
|
|
101
103
|
[tool.ruff.lint.per-file-ignores]
|
|
102
104
|
"*/__init__.py" = ["F401"]
|
|
105
|
+
"roborock/map/proto/*_pb2.py" = ["E501", "I001", "UP009"]
|
|
106
|
+
|
|
107
|
+
[[tool.mypy.overrides]]
|
|
108
|
+
module = ["roborock.map.proto.*"]
|
|
109
|
+
ignore_errors = true
|
|
103
110
|
|
|
104
111
|
[tool.pytest.ini_options]
|
|
105
112
|
asyncio_mode = "auto"
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import logging
|
|
4
2
|
from collections import namedtuple
|
|
5
3
|
from enum import Enum, IntEnum, StrEnum
|
|
@@ -17,7 +15,7 @@ class RoborockEnum(IntEnum):
|
|
|
17
15
|
return super().name.lower()
|
|
18
16
|
|
|
19
17
|
@classmethod
|
|
20
|
-
def _missing_(cls: type[
|
|
18
|
+
def _missing_(cls: type[Self], key) -> Self:
|
|
21
19
|
if hasattr(cls, "unknown"):
|
|
22
20
|
warning = f"Missing {cls.__name__} code: {key} - defaulting to 'unknown'"
|
|
23
21
|
if warning not in completed_warnings:
|
|
@@ -32,23 +30,23 @@ class RoborockEnum(IntEnum):
|
|
|
32
30
|
return default_value
|
|
33
31
|
|
|
34
32
|
@classmethod
|
|
35
|
-
def as_dict(cls: type[
|
|
33
|
+
def as_dict(cls: type[Self]):
|
|
36
34
|
return {i.name: i.value for i in cls if i.name != "missing"}
|
|
37
35
|
|
|
38
36
|
@classmethod
|
|
39
|
-
def as_enum_dict(cls: type[
|
|
37
|
+
def as_enum_dict(cls: type[Self]):
|
|
40
38
|
return {i.value: i for i in cls if i.name != "missing"}
|
|
41
39
|
|
|
42
40
|
@classmethod
|
|
43
|
-
def values(cls: type[
|
|
41
|
+
def values(cls: type[Self]) -> list[int]:
|
|
44
42
|
return list(cls.as_dict().values())
|
|
45
43
|
|
|
46
44
|
@classmethod
|
|
47
|
-
def keys(cls: type[
|
|
45
|
+
def keys(cls: type[Self]) -> list[str]:
|
|
48
46
|
return list(cls.as_dict().keys())
|
|
49
47
|
|
|
50
48
|
@classmethod
|
|
51
|
-
def items(cls: type[
|
|
49
|
+
def items(cls: type[Self]):
|
|
52
50
|
return cls.as_dict().items()
|
|
53
51
|
|
|
54
52
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
1
3
|
from ..code_mappings import RoborockEnum
|
|
2
4
|
|
|
3
5
|
|
|
@@ -91,7 +93,7 @@ class RoborockStartType(RoborockEnum):
|
|
|
91
93
|
|
|
92
94
|
class RoborockDssCodes(RoborockEnum):
|
|
93
95
|
@classmethod
|
|
94
|
-
def _missing_(cls: type[
|
|
96
|
+
def _missing_(cls: type[Self], key) -> Self:
|
|
95
97
|
# If the calculated value is not provided, then it should be viewed as okay.
|
|
96
98
|
# As the math will sometimes result in you getting numbers that don't matter.
|
|
97
99
|
return cls.okay # type: ignore
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
from dataclasses import dataclass, field, fields
|
|
4
2
|
from enum import IntEnum, StrEnum
|
|
5
|
-
from typing import Any
|
|
3
|
+
from typing import Any, Self
|
|
6
4
|
|
|
7
5
|
from roborock.data.code_mappings import RoborockProductNickname
|
|
8
6
|
from roborock.data.containers import RoborockBase
|
|
@@ -566,7 +564,7 @@ class DeviceFeatures(RoborockBase):
|
|
|
566
564
|
new_feature_info_str: str,
|
|
567
565
|
feature_info: list[int],
|
|
568
566
|
product_nickname: RoborockProductNickname | None,
|
|
569
|
-
) ->
|
|
567
|
+
) -> Self:
|
|
570
568
|
"""Creates a DeviceFeatures instance from raw feature flags.
|
|
571
569
|
:param new_feature_info: A int from get_init_status (sometimes can be found in homedata, but it is not always)
|
|
572
570
|
:param new_feature_info_str: A hex string from get_init_status or home_data.
|
|
@@ -251,7 +251,7 @@ async def create_device_manager(
|
|
|
251
251
|
trait = b01.q10.create(channel)
|
|
252
252
|
elif "sc" in model_part:
|
|
253
253
|
# Q7 devices start with 'sc' in their model naming.
|
|
254
|
-
trait = b01.q7.create(channel)
|
|
254
|
+
trait = b01.q7.create(product, device, channel)
|
|
255
255
|
else:
|
|
256
256
|
raise UnsupportedDeviceError(f"Device {device.name} has unsupported B01 model: {product.model}")
|
|
257
257
|
case _:
|
|
@@ -6,16 +6,24 @@ import asyncio
|
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
8
|
from collections.abc import Callable
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import TypeAlias, TypeVar
|
|
10
10
|
|
|
11
11
|
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
12
12
|
from roborock.exceptions import RoborockException
|
|
13
|
-
from roborock.protocols.b01_q7_protocol import
|
|
13
|
+
from roborock.protocols.b01_q7_protocol import (
|
|
14
|
+
B01_VERSION,
|
|
15
|
+
MapKey,
|
|
16
|
+
Q7RequestMessage,
|
|
17
|
+
decode_map_payload,
|
|
18
|
+
decode_rpc_response,
|
|
19
|
+
encode_mqtt_payload,
|
|
20
|
+
)
|
|
14
21
|
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
|
|
15
22
|
|
|
16
23
|
_LOGGER = logging.getLogger(__name__)
|
|
17
24
|
_TIMEOUT = 10.0
|
|
18
25
|
_T = TypeVar("_T")
|
|
26
|
+
DecodedB01Response: TypeAlias = dict[str, object] | str
|
|
19
27
|
|
|
20
28
|
|
|
21
29
|
def _matches_map_response(response_message: RoborockMessage, *, version: bytes | None) -> bytes | None:
|
|
@@ -61,11 +69,11 @@ async def _send_command(
|
|
|
61
69
|
async def send_decoded_command(
|
|
62
70
|
mqtt_channel: MqttChannel,
|
|
63
71
|
request_message: Q7RequestMessage,
|
|
64
|
-
) ->
|
|
72
|
+
) -> DecodedB01Response:
|
|
65
73
|
"""Send a command on the MQTT channel and get a decoded response."""
|
|
66
74
|
_LOGGER.debug("Sending B01 MQTT command: %s", request_message)
|
|
67
75
|
|
|
68
|
-
def find_response(response_message: RoborockMessage) ->
|
|
76
|
+
def find_response(response_message: RoborockMessage) -> DecodedB01Response | None:
|
|
69
77
|
"""Handle incoming messages and resolve the future."""
|
|
70
78
|
try:
|
|
71
79
|
decoded_dps = decode_rpc_response(response_message)
|
|
@@ -126,18 +134,32 @@ async def send_decoded_command(
|
|
|
126
134
|
raise
|
|
127
135
|
|
|
128
136
|
|
|
129
|
-
|
|
130
|
-
"""
|
|
137
|
+
class MapRpcChannel:
|
|
138
|
+
"""RPC channel for map-related commands on B01/Q7 devices."""
|
|
131
139
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
def __init__(self, mqtt_channel: MqttChannel, map_key: MapKey) -> None:
|
|
141
|
+
self._mqtt_channel = mqtt_channel
|
|
142
|
+
self._map_key = map_key
|
|
135
143
|
|
|
136
|
-
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
+
async def send_map_command(self, request_message: Q7RequestMessage) -> bytes:
|
|
145
|
+
"""Send a map upload command and return decoded SCMap bytes.
|
|
146
|
+
|
|
147
|
+
This publishes the request and waits for a matching ``MAP_RESPONSE`` message
|
|
148
|
+
with the correct protocol version. The raw ``MAP_RESPONSE`` payload bytes are
|
|
149
|
+
then decoded/inflated via :func:`decode_map_payload` using this channel's
|
|
150
|
+
``map_key``, and the resulting SCMap bytes are returned.
|
|
151
|
+
|
|
152
|
+
The returned value is the decoded map data bytes suitable for passing to the
|
|
153
|
+
map parser library, not the raw MQTT ``MAP_RESPONSE`` payload bytes.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
raw_payload = await _send_command(
|
|
158
|
+
self._mqtt_channel,
|
|
159
|
+
request_message,
|
|
160
|
+
response_matcher=lambda response_message: _matches_map_response(response_message, version=B01_VERSION),
|
|
161
|
+
)
|
|
162
|
+
except TimeoutError as ex:
|
|
163
|
+
raise RoborockException(f"B01 map command timed out after {_TIMEOUT}s ({request_message})") from ex
|
|
164
|
+
|
|
165
|
+
return decode_map_payload(raw_payload, map_key=self._map_key)
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"""Traits for Q7 B01 devices.
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
Potentially other devices may fall into this category in the future.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
3
7
|
|
|
4
8
|
from typing import Any
|
|
5
9
|
|
|
6
10
|
from roborock import B01Props
|
|
7
|
-
from roborock.data import Q7MapList, Q7MapListEntry
|
|
11
|
+
from roborock.data import HomeDataDevice, HomeDataProduct, Q7MapList, Q7MapListEntry
|
|
8
12
|
from roborock.data.b01_q7.b01_q7_code_mappings import (
|
|
9
13
|
CleanPathPreferenceMapping,
|
|
10
14
|
CleanRepeatMapping,
|
|
@@ -14,27 +18,30 @@ from roborock.data.b01_q7.b01_q7_code_mappings import (
|
|
|
14
18
|
SCWindMapping,
|
|
15
19
|
WaterLevelMapping,
|
|
16
20
|
)
|
|
17
|
-
from roborock.devices.rpc.b01_q7_channel import send_decoded_command
|
|
21
|
+
from roborock.devices.rpc.b01_q7_channel import MapRpcChannel, send_decoded_command
|
|
18
22
|
from roborock.devices.traits import Trait
|
|
19
23
|
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
20
|
-
from roborock.
|
|
24
|
+
from roborock.exceptions import RoborockException
|
|
25
|
+
from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, CommandType, ParamsType, Q7RequestMessage, create_map_key
|
|
21
26
|
from roborock.roborock_message import RoborockB01Props
|
|
22
27
|
from roborock.roborock_typing import RoborockB01Q7Methods
|
|
23
28
|
|
|
24
29
|
from .clean_summary import CleanSummaryTrait
|
|
25
30
|
from .map import MapTrait
|
|
31
|
+
from .map_content import MapContentTrait
|
|
26
32
|
|
|
27
33
|
__all__ = [
|
|
28
34
|
"Q7PropertiesApi",
|
|
29
35
|
"CleanSummaryTrait",
|
|
30
36
|
"MapTrait",
|
|
37
|
+
"MapContentTrait",
|
|
31
38
|
"Q7MapList",
|
|
32
39
|
"Q7MapListEntry",
|
|
33
40
|
]
|
|
34
41
|
|
|
35
42
|
|
|
36
43
|
class Q7PropertiesApi(Trait):
|
|
37
|
-
"""API for interacting with B01 devices."""
|
|
44
|
+
"""API for interacting with B01 Q7 devices."""
|
|
38
45
|
|
|
39
46
|
clean_summary: CleanSummaryTrait
|
|
40
47
|
"""Trait for clean records / clean summary (Q7 `service.get_record_list`)."""
|
|
@@ -42,11 +49,27 @@ class Q7PropertiesApi(Trait):
|
|
|
42
49
|
map: MapTrait
|
|
43
50
|
"""Trait for map list metadata + raw map payload retrieval."""
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
map_content: MapContentTrait
|
|
53
|
+
"""Trait for fetching parsed current map content."""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self, channel: MqttChannel, map_rpc_channel: MapRpcChannel, device: HomeDataDevice, product: HomeDataProduct
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Initialize the Q7 API."""
|
|
47
59
|
self._channel = channel
|
|
60
|
+
self._map_rpc_channel = map_rpc_channel
|
|
61
|
+
self._device = device
|
|
62
|
+
self._product = product
|
|
63
|
+
|
|
64
|
+
if not device.sn or not product.model:
|
|
65
|
+
raise ValueError("B01 Q7 map content requires device serial number and product model metadata")
|
|
66
|
+
|
|
48
67
|
self.clean_summary = CleanSummaryTrait(channel)
|
|
49
68
|
self.map = MapTrait(channel)
|
|
69
|
+
self.map_content = MapContentTrait(
|
|
70
|
+
self._map_rpc_channel,
|
|
71
|
+
self.map,
|
|
72
|
+
)
|
|
50
73
|
|
|
51
74
|
async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
|
|
52
75
|
"""Query the device for the values of the given Q7 properties."""
|
|
@@ -151,6 +174,11 @@ class Q7PropertiesApi(Trait):
|
|
|
151
174
|
)
|
|
152
175
|
|
|
153
176
|
|
|
154
|
-
def create(channel: MqttChannel) -> Q7PropertiesApi:
|
|
155
|
-
"""Create traits for B01 devices."""
|
|
156
|
-
|
|
177
|
+
def create(product: HomeDataProduct, device: HomeDataDevice, channel: MqttChannel) -> Q7PropertiesApi:
|
|
178
|
+
"""Create traits for B01 Q7 devices."""
|
|
179
|
+
if device.sn is None or product.model is None:
|
|
180
|
+
raise RoborockException(
|
|
181
|
+
f"Device serial number and product model are required (sn:: {device.sn}, model: {product.model})"
|
|
182
|
+
)
|
|
183
|
+
map_rpc_channel = MapRpcChannel(channel, map_key=create_map_key(serial=device.sn, model=product.model))
|
|
184
|
+
return Q7PropertiesApi(channel, device=device, product=product, map_rpc_channel=map_rpc_channel)
|
{python_roborock-5.0.0 → python_roborock-5.2.0}/roborock/devices/traits/b01/q7/clean_summary.py
RENAMED
|
@@ -4,8 +4,6 @@ For B01/Q7, the Roborock app uses `service.get_record_list` which returns totals
|
|
|
4
4
|
and a `record_list` whose items contain a JSON string in `detail`.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
7
|
import logging
|
|
10
8
|
|
|
11
9
|
from roborock import CleanRecordDetail, CleanRecordList, CleanRecordSummary
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"""Map trait for B01 Q7 devices."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
|
-
|
|
5
3
|
from roborock.data import Q7MapList
|
|
6
|
-
from roborock.devices.rpc.b01_q7_channel import send_decoded_command
|
|
4
|
+
from roborock.devices.rpc.b01_q7_channel import send_decoded_command
|
|
7
5
|
from roborock.devices.traits import Trait
|
|
8
6
|
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
9
7
|
from roborock.exceptions import RoborockException
|
|
@@ -12,14 +10,15 @@ from roborock.roborock_typing import RoborockB01Q7Methods
|
|
|
12
10
|
|
|
13
11
|
|
|
14
12
|
class MapTrait(Q7MapList, Trait):
|
|
15
|
-
"""Map
|
|
13
|
+
"""Map trait for B01/Q7 devices, responsible for fetching and caching map list metadata.
|
|
14
|
+
|
|
15
|
+
The MapContent is fetched from the MapContent trait, which relies on this trait to determine the
|
|
16
|
+
current map ID to fetch.
|
|
17
|
+
"""
|
|
16
18
|
|
|
17
19
|
def __init__(self, channel: MqttChannel) -> None:
|
|
18
20
|
super().__init__()
|
|
19
21
|
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
22
|
|
|
24
23
|
async def refresh(self) -> None:
|
|
25
24
|
"""Refresh cached map list metadata from the device."""
|
|
@@ -36,24 +35,3 @@ class MapTrait(Q7MapList, Trait):
|
|
|
36
35
|
raise RoborockException(f"Failed to decode map list response: {response!r}")
|
|
37
36
|
|
|
38
37
|
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)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Trait for fetching parsed map content from B01/Q7 devices.
|
|
2
|
+
|
|
3
|
+
This intentionally mirrors the v1 `MapContentTrait` contract:
|
|
4
|
+
- `refresh()` performs I/O and populates cached fields
|
|
5
|
+
- `parse_map_content()` reparses cached raw bytes without I/O
|
|
6
|
+
- fields `image_content`, `map_data`, and `raw_api_response` are then readable
|
|
7
|
+
|
|
8
|
+
For B01/Q7 devices, the underlying raw map payload is retrieved via `MapTrait`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from vacuum_map_parser_base.map_data import MapData
|
|
15
|
+
|
|
16
|
+
from roborock.data import RoborockBase
|
|
17
|
+
from roborock.devices.rpc.b01_q7_channel import MapRpcChannel
|
|
18
|
+
from roborock.devices.traits import Trait
|
|
19
|
+
from roborock.exceptions import RoborockException
|
|
20
|
+
from roborock.map.b01_map_parser import B01MapParser, B01MapParserConfig
|
|
21
|
+
from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, Q7RequestMessage
|
|
22
|
+
from roborock.roborock_typing import RoborockB01Q7Methods
|
|
23
|
+
|
|
24
|
+
from .map import MapTrait
|
|
25
|
+
|
|
26
|
+
_TRUNCATE_LENGTH = 20
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class MapContent(RoborockBase):
|
|
31
|
+
"""Dataclass representing map content."""
|
|
32
|
+
|
|
33
|
+
image_content: bytes | None = None
|
|
34
|
+
"""The rendered image of the map in PNG format."""
|
|
35
|
+
|
|
36
|
+
map_data: MapData | None = None
|
|
37
|
+
"""Parsed map data (metadata for points on the map)."""
|
|
38
|
+
|
|
39
|
+
raw_api_response: bytes | None = None
|
|
40
|
+
"""Raw bytes of the map payload from the device.
|
|
41
|
+
|
|
42
|
+
This should be treated as an opaque blob used only internally by this
|
|
43
|
+
library to re-parse the map data when needed.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
img = self.image_content
|
|
48
|
+
if img and len(img) > _TRUNCATE_LENGTH:
|
|
49
|
+
img = img[: _TRUNCATE_LENGTH - 3] + b"..."
|
|
50
|
+
return f"MapContent(image_content={img!r}, map_data={self.map_data!r})"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MapContentTrait(MapContent, Trait):
|
|
54
|
+
"""Trait for fetching parsed map content for Q7 devices."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
map_rpc_channel: MapRpcChannel,
|
|
59
|
+
map_trait: MapTrait,
|
|
60
|
+
*,
|
|
61
|
+
map_parser_config: B01MapParserConfig | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
super().__init__()
|
|
64
|
+
self._map_rpc_channel = map_rpc_channel
|
|
65
|
+
self._map_trait = map_trait
|
|
66
|
+
self._map_parser = B01MapParser(map_parser_config)
|
|
67
|
+
# Map uploads are serialized per-device to avoid response cross-wiring.
|
|
68
|
+
self._map_command_lock = asyncio.Lock()
|
|
69
|
+
|
|
70
|
+
async def refresh(self) -> None:
|
|
71
|
+
"""Fetch, decode, and parse the current map payload.
|
|
72
|
+
|
|
73
|
+
This relies on the Map Trait already having fetched the map list metadata
|
|
74
|
+
so it can determine the current map_id.
|
|
75
|
+
"""
|
|
76
|
+
# Users must call first
|
|
77
|
+
if (map_id := self._map_trait.current_map_id) is None:
|
|
78
|
+
raise RoborockException("Unable to determine current map ID")
|
|
79
|
+
|
|
80
|
+
request = Q7RequestMessage(
|
|
81
|
+
dps=B01_Q7_DPS,
|
|
82
|
+
command=RoborockB01Q7Methods.UPLOAD_BY_MAPID,
|
|
83
|
+
params={"map_id": map_id},
|
|
84
|
+
)
|
|
85
|
+
async with self._map_command_lock:
|
|
86
|
+
raw_payload = await self._map_rpc_channel.send_map_command(request)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
parsed_data = self._map_parser.parse(raw_payload)
|
|
90
|
+
except RoborockException:
|
|
91
|
+
raise
|
|
92
|
+
except Exception as ex:
|
|
93
|
+
raise RoborockException("Failed to parse B01 map data") from ex
|
|
94
|
+
|
|
95
|
+
if parsed_data.image_content is None:
|
|
96
|
+
raise RoborockException("Failed to render B01 map image")
|
|
97
|
+
|
|
98
|
+
self.image_content = parsed_data.image_content
|
|
99
|
+
self.map_data = parsed_data.map_data
|
|
100
|
+
self.raw_api_response = raw_payload
|
|
@@ -9,13 +9,11 @@ data is collected and exposed to clients via higher level APIs like the
|
|
|
9
9
|
DeviceManager.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
12
|
import time
|
|
15
13
|
from collections import Counter
|
|
16
14
|
from collections.abc import Generator, Mapping
|
|
17
15
|
from contextlib import contextmanager
|
|
18
|
-
from typing import Any, TypeVar, cast
|
|
16
|
+
from typing import Any, Self, TypeVar, cast
|
|
19
17
|
|
|
20
18
|
|
|
21
19
|
class Diagnostics:
|
|
@@ -28,7 +26,7 @@ class Diagnostics:
|
|
|
28
26
|
def __init__(self) -> None:
|
|
29
27
|
"""Initialize Diagnostics."""
|
|
30
28
|
self._counter: Counter = Counter()
|
|
31
|
-
self._subkeys: dict[str,
|
|
29
|
+
self._subkeys: dict[str, Self] = {}
|
|
32
30
|
|
|
33
31
|
def increment(self, key: str, count: int = 1) -> None:
|
|
34
32
|
"""Increment a counter for the specified key/event."""
|
|
@@ -49,7 +47,7 @@ class Diagnostics:
|
|
|
49
47
|
data[k] = v
|
|
50
48
|
return data
|
|
51
49
|
|
|
52
|
-
def subkey(self, key: str) ->
|
|
50
|
+
def subkey(self, key: str) -> Self:
|
|
53
51
|
"""Return sub-Diagnostics object with the specified subkey.
|
|
54
52
|
|
|
55
53
|
This will create a new Diagnostics object if one does not already exist
|
|
@@ -63,7 +61,7 @@ class Diagnostics:
|
|
|
63
61
|
The Diagnostics object for the specified subkey.
|
|
64
62
|
"""
|
|
65
63
|
if key not in self._subkeys:
|
|
66
|
-
self._subkeys[key] =
|
|
64
|
+
self._subkeys[key] = type(self)()
|
|
67
65
|
return self._subkeys[key]
|
|
68
66
|
|
|
69
67
|
@contextmanager
|