python-roborock 7.6.0__tar.gz → 7.7.1__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-7.6.0 → python_roborock-7.7.1}/PKG-INFO +1 -1
- {python_roborock-7.6.0 → python_roborock-7.7.1}/pyproject.toml +1 -1
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/b01_q10/b01_q10_code_mappings.py +2 -7
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/b01_q10/b01_q10_containers.py +56 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/__init__.py +2 -1
- python_roborock-7.7.1/roborock/devices/traits/b01/q10/goto.py +158 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/map.py +15 -5
- python_roborock-7.7.1/roborock/devices/traits/b01/q10/vacuum.py +291 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/map/b01_q10_map_parser.py +6 -1
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/protocols/b01_q10_protocol.py +48 -0
- python_roborock-7.6.0/roborock/devices/traits/b01/q10/vacuum.py +0 -109
- {python_roborock-7.6.0 → python_roborock-7.7.1}/.gitignore +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/LICENSE +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/README.md +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/callbacks.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/cli.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/const.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/code_mappings.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/containers.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/mower/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/mower/mower_code_mappings.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/mower/mower_containers.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/device_features.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/README.md +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/cache.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/device.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/device_manager.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/file_cache.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/rpc/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/rpc/a01_channel.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/rpc/b01_q10_channel.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/rpc/b01_q7_channel.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/rpc/v1_channel.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/a01/device_feature.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/button_light.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/child_lock.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/clean_history.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/command.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/common.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/consumable.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/do_not_disturb.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/dust_collection.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/maps.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/network_info.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/remote.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/status.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q10/volume.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q7/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q7/map.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/b01/q7/map_content.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/common.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/common.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/obstacle_photos.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/transport/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/transport/channel.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/transport/local_channel.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/devices/transport/mqtt_channel.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/diagnostics.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/exceptions.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/map/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/map/b01_grid_layers.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/map/b01_map_parser.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/map/b01_q10_overlays.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/map/b01_q10_render.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/map/map_parser.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/map/proto/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/map/proto/b01_scmap.proto +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/map/proto/b01_scmap_pb2.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/map/room_colors.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/mqtt/session.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/protocol.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/protocols/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/protocols/b01_q7_protocol.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/py.typed +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/roborock_message.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/roborock_typing.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/testing/__init__.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/testing/a01_simulator.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/testing/b01_q10_simulator.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/testing/channel.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/testing/cloud.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/testing/simulator.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/testing/v1_simulator.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/util.py +0 -0
- {python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.7.1
|
|
4
4
|
Summary: A package to control Roborock vacuums.
|
|
5
5
|
Project-URL: Repository, https://github.com/python-roborock/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 = "7.
|
|
3
|
+
version = "7.7.1"
|
|
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"
|
{python_roborock-7.6.0 → python_roborock-7.7.1}/roborock/data/b01_q10/b01_q10_code_mappings.py
RENAMED
|
@@ -101,13 +101,8 @@ class B01_Q10_DP(RoborockModeEnum):
|
|
|
101
101
|
VOICE_VERSION = ("dpVoiceVersion", 108)
|
|
102
102
|
ROBOT_COUNTRY_CODE = ("dpRobotCountryCode", 109)
|
|
103
103
|
HEARTBEAT = ("dpHeartbeat", 110)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# across every observed state (docked/charging, segment cleaning, lifted-off-
|
|
107
|
-
# ground fault, returning to dock, dustbin removed), so their meaning is not
|
|
108
|
-
# yet known. They are intentionally left unmapped; ``decode_rpc_response``
|
|
109
|
-
# silently ignores unknown codes via ``from_code_optional``, so they do not
|
|
110
|
-
# produce "not a valid code" warnings. Map them here once identified.
|
|
104
|
+
UNKNOWN_112 = ("dpUnknown112", 112)
|
|
105
|
+
UNKNOWN_113 = ("dpUnknown113", 113)
|
|
111
106
|
STATUS = ("dpStatus", 121)
|
|
112
107
|
BATTERY = ("dpBattery", 122)
|
|
113
108
|
FAN_LEVEL = ("dpFanLevel", 123) # NOTE: typo "dpfunLevel" in source code
|
|
@@ -28,6 +28,62 @@ from .b01_q10_code_mappings import (
|
|
|
28
28
|
YXWaterLevel,
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
_ROBOROCK_COORDINATE_OFFSET_MM = 25_500
|
|
32
|
+
_Q10_TRACE_UNIT_MM = 2.5
|
|
33
|
+
_Q10_VECTOR_UNIT_MM = 5
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class Q10RoborockPoint:
|
|
38
|
+
"""A point in the common Roborock millimetre coordinate space.
|
|
39
|
+
|
|
40
|
+
Q10 trace and vector coordinates are firmware details. Public Q10 APIs use
|
|
41
|
+
this coordinate system, matching other Roborock devices and placing the dock
|
|
42
|
+
at ``(25500, 25500)``.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
x: int
|
|
46
|
+
y: int
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_trace(cls, x: int, y: int) -> "Q10RoborockPoint":
|
|
50
|
+
"""Convert Q10 trace coordinates to common Roborock coordinates."""
|
|
51
|
+
for value in (x, y):
|
|
52
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
53
|
+
raise ValueError("trace coordinates must be integers")
|
|
54
|
+
return cls(
|
|
55
|
+
x=round(_ROBOROCK_COORDINATE_OFFSET_MM + x * _Q10_TRACE_UNIT_MM),
|
|
56
|
+
y=round(_ROBOROCK_COORDINATE_OFFSET_MM + y * _Q10_TRACE_UNIT_MM),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_vector(cls, x: int, y: int) -> "Q10RoborockPoint":
|
|
61
|
+
"""Convert Q10 vector coordinates to common Roborock coordinates."""
|
|
62
|
+
for value in (x, y):
|
|
63
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
64
|
+
raise ValueError("vector coordinates must be integers")
|
|
65
|
+
if not -(2**15) <= value < 2**15:
|
|
66
|
+
raise ValueError("vector coordinates are outside the Q10 map range")
|
|
67
|
+
return cls(
|
|
68
|
+
x=_ROBOROCK_COORDINATE_OFFSET_MM + x * _Q10_VECTOR_UNIT_MM,
|
|
69
|
+
y=_ROBOROCK_COORDINATE_OFFSET_MM + y * _Q10_VECTOR_UNIT_MM,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def to_vector(self) -> tuple[int, int]:
|
|
73
|
+
"""Convert common Roborock coordinates to the Q10 vector grid."""
|
|
74
|
+
coordinates: list[int] = []
|
|
75
|
+
for value in (self.x, self.y):
|
|
76
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
77
|
+
raise ValueError("coordinates must be integers")
|
|
78
|
+
relative_mm = value - _ROBOROCK_COORDINATE_OFFSET_MM
|
|
79
|
+
if relative_mm % _Q10_VECTOR_UNIT_MM:
|
|
80
|
+
raise ValueError("coordinates must align to the Q10 5 mm grid")
|
|
81
|
+
coordinate = relative_mm // _Q10_VECTOR_UNIT_MM
|
|
82
|
+
if not -(2**15) <= coordinate < 2**15:
|
|
83
|
+
raise ValueError("coordinates are outside the Q10 map range")
|
|
84
|
+
coordinates.append(coordinate)
|
|
85
|
+
return coordinates[0], coordinates[1]
|
|
86
|
+
|
|
31
87
|
|
|
32
88
|
@dataclass
|
|
33
89
|
class dpCleanRecord(RoborockBase):
|
|
@@ -96,7 +96,6 @@ class Q10PropertiesApi(Trait):
|
|
|
96
96
|
"""Initialize the B01Props API."""
|
|
97
97
|
self._channel = channel
|
|
98
98
|
self.command = CommandTrait(channel)
|
|
99
|
-
self.vacuum = VacuumTrait(self.command)
|
|
100
99
|
self.remote = RemoteTrait(self.command)
|
|
101
100
|
self.status = StatusTrait()
|
|
102
101
|
self.volume = SoundVolumeTrait(self.command)
|
|
@@ -109,6 +108,7 @@ class Q10PropertiesApi(Trait):
|
|
|
109
108
|
self._map_dps = MapDpsTrait()
|
|
110
109
|
self.maps = MapsTrait(self.command)
|
|
111
110
|
self.map = MapContentTrait(self._map_dps, self.maps, self.command)
|
|
111
|
+
self.vacuum = VacuumTrait(self.command, self.status, self.map)
|
|
112
112
|
self.clean_history = CleanHistoryTrait(self.command)
|
|
113
113
|
# Read-model traits updated from the device's DPS push stream.
|
|
114
114
|
self._updatable_traits = [
|
|
@@ -131,6 +131,7 @@ class Q10PropertiesApi(Trait):
|
|
|
131
131
|
|
|
132
132
|
async def close(self) -> None:
|
|
133
133
|
"""Close any resources held by the trait."""
|
|
134
|
+
await self.vacuum.close()
|
|
134
135
|
if self._subscribe_task is not None:
|
|
135
136
|
self._subscribe_task.cancel()
|
|
136
137
|
try:
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""State management for an emulated Q10 goto action."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from math import hypot
|
|
8
|
+
|
|
9
|
+
from roborock.callbacks import CallbackList
|
|
10
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import YXDeviceCleanTask, YXDeviceState
|
|
11
|
+
from roborock.data.b01_q10.b01_q10_containers import Q10RoborockPoint
|
|
12
|
+
|
|
13
|
+
_LOGGER = logging.getLogger(__name__)
|
|
14
|
+
_TERMINAL_STATES = {
|
|
15
|
+
YXDeviceState.IDLE,
|
|
16
|
+
YXDeviceState.PAUSED,
|
|
17
|
+
YXDeviceState.RETURNING_HOME,
|
|
18
|
+
YXDeviceState.CHARGING,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GotoActionCommand(StrEnum):
|
|
23
|
+
"""A command requested by a Q10 goto action."""
|
|
24
|
+
|
|
25
|
+
PAUSE = "pause"
|
|
26
|
+
STOP = "stop"
|
|
27
|
+
COMPLETE = "complete"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class GotoSnapshot:
|
|
32
|
+
"""Device state needed to advance a goto action."""
|
|
33
|
+
|
|
34
|
+
position: Q10RoborockPoint | None
|
|
35
|
+
trace_sequence: int | None
|
|
36
|
+
clean_task_type: YXDeviceCleanTask | None
|
|
37
|
+
status: YXDeviceState | None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GotoAction:
|
|
41
|
+
"""Decide how one emulated goto should react to device updates.
|
|
42
|
+
|
|
43
|
+
The action owns no tasks and sends no device commands. ``VacuumTrait`` feeds
|
|
44
|
+
it push-derived snapshots and performs commands requested by its callbacks.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
target: Q10RoborockPoint,
|
|
50
|
+
previous_trace_sequence: int | None,
|
|
51
|
+
*,
|
|
52
|
+
tolerance: int,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Initialize a goto action waiting for a new trace session."""
|
|
55
|
+
self._target = target
|
|
56
|
+
self._previous_trace_sequence = previous_trace_sequence
|
|
57
|
+
self._tolerance = tolerance
|
|
58
|
+
self._owned_trace_sequence: int | None = None
|
|
59
|
+
self._owned_task_seen = False
|
|
60
|
+
self._command_pending = False
|
|
61
|
+
self._timeout_requested = False
|
|
62
|
+
self._finished = False
|
|
63
|
+
self._latest_snapshot: GotoSnapshot | None = None
|
|
64
|
+
self._callbacks: CallbackList[GotoActionCommand] = CallbackList(logger=_LOGGER)
|
|
65
|
+
|
|
66
|
+
def add_update_listener(self, callback: Callable[[GotoActionCommand], None]) -> Callable[[], None]:
|
|
67
|
+
"""Register a callback for the next command requested by the action."""
|
|
68
|
+
return self._callbacks.add_callback(callback)
|
|
69
|
+
|
|
70
|
+
def update(self, snapshot: GotoSnapshot) -> None:
|
|
71
|
+
"""Process the latest push-derived device state."""
|
|
72
|
+
self._latest_snapshot = snapshot
|
|
73
|
+
self._evaluate(snapshot)
|
|
74
|
+
|
|
75
|
+
def retry(self) -> None:
|
|
76
|
+
"""Re-evaluate the latest state after a requested command failed."""
|
|
77
|
+
if self._finished or self._latest_snapshot is None:
|
|
78
|
+
return
|
|
79
|
+
self._command_pending = False
|
|
80
|
+
if self._timeout_requested:
|
|
81
|
+
self._evaluate_timeout(self._latest_snapshot)
|
|
82
|
+
else:
|
|
83
|
+
self._evaluate(self._latest_snapshot)
|
|
84
|
+
|
|
85
|
+
def timeout(self, snapshot: GotoSnapshot) -> None:
|
|
86
|
+
"""Request a stop only if this action still owns the current zone task."""
|
|
87
|
+
if self._finished:
|
|
88
|
+
return
|
|
89
|
+
self._latest_snapshot = snapshot
|
|
90
|
+
self._timeout_requested = True
|
|
91
|
+
if self._command_pending:
|
|
92
|
+
return
|
|
93
|
+
self._evaluate_timeout(snapshot)
|
|
94
|
+
|
|
95
|
+
def _evaluate_timeout(self, snapshot: GotoSnapshot) -> None:
|
|
96
|
+
"""Derive the safe timeout command from the latest device state."""
|
|
97
|
+
if self.owns(snapshot):
|
|
98
|
+
self._emit(GotoActionCommand.STOP)
|
|
99
|
+
else:
|
|
100
|
+
self._emit(GotoActionCommand.COMPLETE)
|
|
101
|
+
|
|
102
|
+
def complete(self) -> None:
|
|
103
|
+
"""Mark the action complete after its requested command succeeds."""
|
|
104
|
+
self._finished = True
|
|
105
|
+
self._command_pending = False
|
|
106
|
+
|
|
107
|
+
def owns(self, snapshot: GotoSnapshot) -> bool:
|
|
108
|
+
"""Return whether this action owns the current Q10 zone-clean session."""
|
|
109
|
+
return (
|
|
110
|
+
self._owned_trace_sequence is not None
|
|
111
|
+
and snapshot.trace_sequence == self._owned_trace_sequence
|
|
112
|
+
and snapshot.clean_task_type is YXDeviceCleanTask.DIVIDE_AREAS
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _evaluate(self, snapshot: GotoSnapshot) -> None:
|
|
116
|
+
"""Derive the next command from the latest device state."""
|
|
117
|
+
if self._finished or self._command_pending:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if self._owned_trace_sequence is None:
|
|
121
|
+
if snapshot.trace_sequence is not None and snapshot.trace_sequence != self._previous_trace_sequence:
|
|
122
|
+
self._owned_trace_sequence = snapshot.trace_sequence
|
|
123
|
+
elif snapshot.trace_sequence != self._owned_trace_sequence:
|
|
124
|
+
_LOGGER.debug("Q10 goto task was replaced by another cleaning session")
|
|
125
|
+
self._emit(GotoActionCommand.COMPLETE)
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
self._owned_trace_sequence is not None
|
|
130
|
+
and snapshot.clean_task_type is YXDeviceCleanTask.DIVIDE_AREAS
|
|
131
|
+
and snapshot.status not in _TERMINAL_STATES
|
|
132
|
+
):
|
|
133
|
+
self._owned_task_seen = True
|
|
134
|
+
|
|
135
|
+
if self._owned_task_seen and (
|
|
136
|
+
snapshot.clean_task_type is not YXDeviceCleanTask.DIVIDE_AREAS or snapshot.status in _TERMINAL_STATES
|
|
137
|
+
):
|
|
138
|
+
self._emit(GotoActionCommand.COMPLETE)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
self.owns(snapshot)
|
|
143
|
+
and snapshot.position is not None
|
|
144
|
+
and hypot(
|
|
145
|
+
snapshot.position.x - self._target.x,
|
|
146
|
+
snapshot.position.y - self._target.y,
|
|
147
|
+
)
|
|
148
|
+
<= self._tolerance
|
|
149
|
+
):
|
|
150
|
+
self._emit(GotoActionCommand.PAUSE)
|
|
151
|
+
|
|
152
|
+
def _emit(self, command: GotoActionCommand) -> None:
|
|
153
|
+
"""Publish a requested command once until it is handled."""
|
|
154
|
+
if command is GotoActionCommand.COMPLETE:
|
|
155
|
+
self._finished = True
|
|
156
|
+
else:
|
|
157
|
+
self._command_pending = True
|
|
158
|
+
self._callbacks(command)
|
|
@@ -19,6 +19,7 @@ from typing import Any
|
|
|
19
19
|
|
|
20
20
|
from roborock.data import RoborockBase
|
|
21
21
|
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP, YXDeviceState
|
|
22
|
+
from roborock.data.b01_q10.b01_q10_containers import Q10RoborockPoint
|
|
22
23
|
from roborock.devices.traits.common import DpsDataConverter, TraitUpdateListener
|
|
23
24
|
from roborock.exceptions import RoborockException
|
|
24
25
|
from roborock.map.b01_q10_map_parser import (
|
|
@@ -127,13 +128,20 @@ class MapContentTrait(TraitUpdateListener):
|
|
|
127
128
|
|
|
128
129
|
@property
|
|
129
130
|
def path(self) -> list[Q10Point]:
|
|
130
|
-
"""Full path
|
|
131
|
+
"""Full path in the Q10 trace coordinate space used by the map renderer."""
|
|
131
132
|
return self._trace_packet.points if self._trace_packet else []
|
|
132
133
|
|
|
133
134
|
@property
|
|
134
|
-
def robot_position(self) ->
|
|
135
|
-
"""Current position
|
|
136
|
-
|
|
135
|
+
def robot_position(self) -> Q10RoborockPoint | None:
|
|
136
|
+
"""Current position in the common Roborock millimetre coordinate space."""
|
|
137
|
+
if self._trace_packet is None or (position := self._trace_packet.robot_position) is None:
|
|
138
|
+
return None
|
|
139
|
+
return position.to_roborock()
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def trace_sequence(self) -> int | None:
|
|
143
|
+
"""Current cleaning-session sequence from the trace stream."""
|
|
144
|
+
return self._trace_packet.sequence if self._trace_packet else None
|
|
137
145
|
|
|
138
146
|
@property
|
|
139
147
|
def robot_heading(self) -> int | None:
|
|
@@ -181,7 +189,9 @@ class MapContentTrait(TraitUpdateListener):
|
|
|
181
189
|
data = {
|
|
182
190
|
"rooms": [room.as_dict() for room in self.rooms],
|
|
183
191
|
"path": [point.as_dict() for point in self.path],
|
|
184
|
-
"robotPosition":
|
|
192
|
+
"robotPosition": (
|
|
193
|
+
{"x": position.x, "y": position.y} if (position := self.robot_position) is not None else None
|
|
194
|
+
),
|
|
185
195
|
"robotHeading": self.robot_heading,
|
|
186
196
|
}
|
|
187
197
|
for key in exclude_set:
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Traits for Q10 B01 devices."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from math import hypot
|
|
7
|
+
|
|
8
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import (
|
|
9
|
+
B01_Q10_DP,
|
|
10
|
+
YXCleanType,
|
|
11
|
+
YXDeviceCleanTask,
|
|
12
|
+
YXFanLevel,
|
|
13
|
+
)
|
|
14
|
+
from roborock.data.b01_q10.b01_q10_containers import Q10RoborockPoint
|
|
15
|
+
from roborock.exceptions import RoborockException
|
|
16
|
+
from roborock.protocols.b01_q10_protocol import CleanParams, encode_clean_params
|
|
17
|
+
|
|
18
|
+
from .command import CommandTrait
|
|
19
|
+
from .goto import GotoAction, GotoActionCommand, GotoSnapshot
|
|
20
|
+
from .map import MapContentTrait
|
|
21
|
+
from .status import StatusTrait
|
|
22
|
+
|
|
23
|
+
_GOTO_HALF_ZONE_SIZE = 200
|
|
24
|
+
_GOTO_TOLERANCE = 200
|
|
25
|
+
_GOTO_TIMEOUT = 300
|
|
26
|
+
_GOTO_RETRY_INTERVAL = 1
|
|
27
|
+
|
|
28
|
+
_LOGGER = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class VacuumTrait:
|
|
32
|
+
"""Trait for sending vacuum commands.
|
|
33
|
+
|
|
34
|
+
This is a wrapper around the CommandTrait for sending vacuum related
|
|
35
|
+
commands to Q10 devices.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
command: CommandTrait,
|
|
41
|
+
status: StatusTrait,
|
|
42
|
+
map_content: MapContentTrait,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Initialize the VacuumTrait."""
|
|
45
|
+
self._command = command
|
|
46
|
+
self._status = status
|
|
47
|
+
self._map = map_content
|
|
48
|
+
self._goto_action: GotoAction | None = None
|
|
49
|
+
self._goto_action_remove_listener: Callable[[], None] | None = None
|
|
50
|
+
self._goto_timeout_task: asyncio.Task[None] | None = None
|
|
51
|
+
self._goto_command_task: asyncio.Task[None] | None = None
|
|
52
|
+
self._remove_map_listener = self._map.add_update_listener(self._goto_source_updated)
|
|
53
|
+
self._remove_status_listener = self._status.add_update_listener(self._goto_source_updated)
|
|
54
|
+
|
|
55
|
+
async def close(self) -> None:
|
|
56
|
+
"""Cancel background work owned by the trait."""
|
|
57
|
+
self.cancel_goto()
|
|
58
|
+
self._remove_map_listener()
|
|
59
|
+
self._remove_status_listener()
|
|
60
|
+
|
|
61
|
+
def cancel_goto(self) -> None:
|
|
62
|
+
"""Cancel an emulated goto replaced by another command."""
|
|
63
|
+
if self._goto_action is not None:
|
|
64
|
+
self._goto_action.complete()
|
|
65
|
+
self._goto_action = None
|
|
66
|
+
if self._goto_action_remove_listener is not None:
|
|
67
|
+
self._goto_action_remove_listener()
|
|
68
|
+
self._goto_action_remove_listener = None
|
|
69
|
+
current_task = asyncio.current_task()
|
|
70
|
+
for task_name in ("_goto_timeout_task", "_goto_command_task"):
|
|
71
|
+
if (task := getattr(self, task_name)) is not None:
|
|
72
|
+
if task is not current_task:
|
|
73
|
+
task.cancel()
|
|
74
|
+
setattr(self, task_name, None)
|
|
75
|
+
|
|
76
|
+
def _goto_snapshot(self) -> GotoSnapshot:
|
|
77
|
+
"""Return the latest state used by an active goto action."""
|
|
78
|
+
return GotoSnapshot(
|
|
79
|
+
position=self._map.robot_position,
|
|
80
|
+
trace_sequence=self._map.trace_sequence,
|
|
81
|
+
clean_task_type=self._status.clean_task_type,
|
|
82
|
+
status=self._status.status,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _goto_source_updated(self) -> None:
|
|
86
|
+
"""Feed push-derived map or status state to the active goto action."""
|
|
87
|
+
if self._goto_action is not None:
|
|
88
|
+
self._goto_action.update(self._goto_snapshot())
|
|
89
|
+
|
|
90
|
+
def _goto_action_updated(self, action: GotoAction, command: GotoActionCommand) -> None:
|
|
91
|
+
"""Schedule a device command requested by the active goto action."""
|
|
92
|
+
if action is not self._goto_action:
|
|
93
|
+
return
|
|
94
|
+
if command is GotoActionCommand.COMPLETE:
|
|
95
|
+
self.cancel_goto()
|
|
96
|
+
return
|
|
97
|
+
if self._goto_command_task is None:
|
|
98
|
+
self._goto_command_task = asyncio.create_task(
|
|
99
|
+
self._async_handle_goto_command(action, command),
|
|
100
|
+
name="roborock_q10_goto_command",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
async def _async_handle_goto_command(self, action: GotoAction, command: GotoActionCommand) -> None:
|
|
104
|
+
"""Perform a pause or stop requested by the active goto action."""
|
|
105
|
+
current_task = asyncio.current_task()
|
|
106
|
+
dp_command = B01_Q10_DP.PAUSE if command is GotoActionCommand.PAUSE else B01_Q10_DP.STOP
|
|
107
|
+
try:
|
|
108
|
+
await self._command.send(command=dp_command, params=0)
|
|
109
|
+
except RoborockException as err:
|
|
110
|
+
if command is GotoActionCommand.PAUSE:
|
|
111
|
+
_LOGGER.warning("Failed to pause completed Q10 goto task; retrying: %s", err)
|
|
112
|
+
await asyncio.sleep(_GOTO_RETRY_INTERVAL)
|
|
113
|
+
if action is self._goto_action:
|
|
114
|
+
self._goto_command_task = None
|
|
115
|
+
action.retry()
|
|
116
|
+
return
|
|
117
|
+
_LOGGER.warning("Failed to stop timed-out Q10 goto task: %s", err)
|
|
118
|
+
if action is self._goto_action:
|
|
119
|
+
action.complete()
|
|
120
|
+
self.cancel_goto()
|
|
121
|
+
if self._goto_command_task is current_task:
|
|
122
|
+
self._goto_command_task = None
|
|
123
|
+
|
|
124
|
+
async def _async_timeout_goto(self, action: GotoAction) -> None:
|
|
125
|
+
"""Tell the active goto action when its safety timeout expires."""
|
|
126
|
+
try:
|
|
127
|
+
await asyncio.sleep(_GOTO_TIMEOUT)
|
|
128
|
+
except asyncio.CancelledError:
|
|
129
|
+
return
|
|
130
|
+
if action is self._goto_action:
|
|
131
|
+
action.timeout(self._goto_snapshot())
|
|
132
|
+
|
|
133
|
+
async def start_clean(self) -> None:
|
|
134
|
+
"""Start a whole-home clean.
|
|
135
|
+
|
|
136
|
+
The ``dpStartClean`` (201) command selects a task by code: ``1`` =
|
|
137
|
+
whole-home, ``2`` = segment/room (see :meth:`clean_segments`), ``3`` =
|
|
138
|
+
zone, ``4`` = build map, ``5`` = spot. Whole-home and spot accept the
|
|
139
|
+
bare integer code; segment cleaning needs a room selection (an object
|
|
140
|
+
payload) instead.
|
|
141
|
+
|
|
142
|
+
Verified live against ss07 hardware: ``{"dps": {"201": 1}}`` starts a
|
|
143
|
+
whole-home clean (clean_task_type -> 1).
|
|
144
|
+
"""
|
|
145
|
+
await self._command.send(command=B01_Q10_DP.START_CLEAN, params=1)
|
|
146
|
+
self.cancel_goto()
|
|
147
|
+
|
|
148
|
+
async def clean_segments(self, segment_ids: list[int]) -> None:
|
|
149
|
+
"""Start a room / segment clean for the given segment (room) ids.
|
|
150
|
+
|
|
151
|
+
The ids are the same room ids the device reports on its map (see the Q10
|
|
152
|
+
``MapContentTrait`` -- ``map.rooms``, each with an ``id``).
|
|
153
|
+
|
|
154
|
+
Unlike whole-home and spot, ``dpStartClean`` (201) carries the room
|
|
155
|
+
selection as an object: ``{"cmd": <task>, "clean_paramters": [<id>, ...]}``,
|
|
156
|
+
where ``cmd`` is the segment-clean task code.
|
|
157
|
+
|
|
158
|
+
Verified live against ss07 hardware: sending
|
|
159
|
+
``{"dps": {"201": {"cmd": 2, "clean_paramters": [9]}}}`` starts cleaning
|
|
160
|
+
room 9 (clean_task_type -> 2 / electoral).
|
|
161
|
+
"""
|
|
162
|
+
await self._command.send(
|
|
163
|
+
command=B01_Q10_DP.START_CLEAN,
|
|
164
|
+
# "clean_paramters" intentionally mirrors the device's misspelling of
|
|
165
|
+
# "parameters" -- the firmware only accepts that exact key.
|
|
166
|
+
params={"cmd": YXDeviceCleanTask.ELECTORAL.code, "clean_paramters": segment_ids},
|
|
167
|
+
)
|
|
168
|
+
self.cancel_goto()
|
|
169
|
+
|
|
170
|
+
async def clean_zone(
|
|
171
|
+
self,
|
|
172
|
+
first_corner: Q10RoborockPoint,
|
|
173
|
+
second_corner: Q10RoborockPoint,
|
|
174
|
+
*,
|
|
175
|
+
clean_count: int = 1,
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Clean one rectangular zone in the common Roborock coordinate space."""
|
|
178
|
+
encoded_zone = encode_clean_params(CleanParams(first_corner, second_corner, clean_count))
|
|
179
|
+
await self._command.send(
|
|
180
|
+
command=B01_Q10_DP.START_CLEAN,
|
|
181
|
+
params={
|
|
182
|
+
"cmd": YXDeviceCleanTask.DIVIDE_AREAS.code,
|
|
183
|
+
# "clean_paramters" is the spelling required by the firmware.
|
|
184
|
+
"clean_paramters": encoded_zone,
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
self.cancel_goto()
|
|
188
|
+
|
|
189
|
+
async def goto_position(self, target: Q10RoborockPoint) -> None:
|
|
190
|
+
"""Move to a coordinate using an owned 40 cm zone-clean task."""
|
|
191
|
+
target.to_vector()
|
|
192
|
+
snapshot = self._goto_snapshot()
|
|
193
|
+
if (position := snapshot.position) is not None and hypot(
|
|
194
|
+
position.x - target.x, position.y - target.y
|
|
195
|
+
) <= _GOTO_TOLERANCE:
|
|
196
|
+
if self._goto_action is not None and self._goto_action.owns(snapshot):
|
|
197
|
+
await self._command.send(command=B01_Q10_DP.PAUSE, params=0)
|
|
198
|
+
self.cancel_goto()
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
encoded_zone = encode_clean_params(
|
|
202
|
+
CleanParams(
|
|
203
|
+
Q10RoborockPoint(
|
|
204
|
+
target.x - _GOTO_HALF_ZONE_SIZE,
|
|
205
|
+
target.y - _GOTO_HALF_ZONE_SIZE,
|
|
206
|
+
),
|
|
207
|
+
Q10RoborockPoint(
|
|
208
|
+
target.x + _GOTO_HALF_ZONE_SIZE,
|
|
209
|
+
target.y + _GOTO_HALF_ZONE_SIZE,
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
await self._command.send(
|
|
214
|
+
command=B01_Q10_DP.START_CLEAN,
|
|
215
|
+
params={
|
|
216
|
+
"cmd": YXDeviceCleanTask.DIVIDE_AREAS.code,
|
|
217
|
+
"clean_paramters": encoded_zone,
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
self.cancel_goto()
|
|
221
|
+
action = GotoAction(
|
|
222
|
+
target,
|
|
223
|
+
snapshot.trace_sequence,
|
|
224
|
+
tolerance=_GOTO_TOLERANCE,
|
|
225
|
+
)
|
|
226
|
+
self._goto_action = action
|
|
227
|
+
self._goto_action_remove_listener = action.add_update_listener(
|
|
228
|
+
lambda command: self._goto_action_updated(action, command)
|
|
229
|
+
)
|
|
230
|
+
self._goto_timeout_task = asyncio.create_task(
|
|
231
|
+
self._async_timeout_goto(action),
|
|
232
|
+
name="roborock_q10_goto_timeout",
|
|
233
|
+
)
|
|
234
|
+
action.update(self._goto_snapshot())
|
|
235
|
+
|
|
236
|
+
async def spot_clean(self) -> None:
|
|
237
|
+
"""Start a spot / part clean around the robot's current position.
|
|
238
|
+
|
|
239
|
+
Verified live: ``{"dps": {"201": 5}}`` (clean_task_type -> 5).
|
|
240
|
+
"""
|
|
241
|
+
await self._command.send(command=B01_Q10_DP.START_CLEAN, params=5)
|
|
242
|
+
self.cancel_goto()
|
|
243
|
+
|
|
244
|
+
async def pause_clean(self) -> None:
|
|
245
|
+
"""Pause the current task. Verified live: ``{"dps": {"204": 0}}``."""
|
|
246
|
+
await self._command.send(command=B01_Q10_DP.PAUSE, params=0)
|
|
247
|
+
self.cancel_goto()
|
|
248
|
+
|
|
249
|
+
async def resume_clean(self) -> None:
|
|
250
|
+
"""Resume a paused task. Verified live: ``{"dps": {"205": 0}}``."""
|
|
251
|
+
await self._command.send(command=B01_Q10_DP.RESUME, params=0)
|
|
252
|
+
self.cancel_goto()
|
|
253
|
+
|
|
254
|
+
async def stop_clean(self) -> None:
|
|
255
|
+
"""Stop / cancel the current task. Verified live: ``{"dps": {"206": 0}}``."""
|
|
256
|
+
await self._command.send(command=B01_Q10_DP.STOP, params=0)
|
|
257
|
+
self.cancel_goto()
|
|
258
|
+
|
|
259
|
+
async def return_to_dock(self) -> None:
|
|
260
|
+
"""Send the robot back to the dock to charge.
|
|
261
|
+
|
|
262
|
+
Uses ``dpStartBack`` (202) with the back-dock task code ``5`` (charge),
|
|
263
|
+
matching the official app. Verified live: ``{"dps": {"202": 5}}`` puts the
|
|
264
|
+
robot into the returning state. (The other back-dock codes are ``1`` =
|
|
265
|
+
wash mop en route and ``4`` = collect dust en route.)
|
|
266
|
+
"""
|
|
267
|
+
await self._command.send(command=B01_Q10_DP.START_BACK, params=5)
|
|
268
|
+
self.cancel_goto()
|
|
269
|
+
|
|
270
|
+
async def empty_dustbin(self) -> None:
|
|
271
|
+
"""Empty the dustbin at the dock.
|
|
272
|
+
|
|
273
|
+
Verified live: ``{"dps": {"203": 2}}`` triggers dust collection
|
|
274
|
+
(status -> emptying_the_bin). This is a dock task (``dpStartDockTask``),
|
|
275
|
+
distinct from the en-route collect-dust back-dock code.
|
|
276
|
+
"""
|
|
277
|
+
await self._command.send(command=B01_Q10_DP.START_DOCK_TASK, params=2)
|
|
278
|
+
|
|
279
|
+
async def set_clean_mode(self, mode: YXCleanType) -> None:
|
|
280
|
+
"""Set the cleaning mode (vacuum, mop, or both)."""
|
|
281
|
+
await self._command.send(
|
|
282
|
+
command=B01_Q10_DP.CLEAN_MODE,
|
|
283
|
+
params=mode.code,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
async def set_fan_level(self, level: YXFanLevel) -> None:
|
|
287
|
+
"""Set the fan suction level."""
|
|
288
|
+
await self._command.send(
|
|
289
|
+
command=B01_Q10_DP.FAN_LEVEL,
|
|
290
|
+
params=level.code,
|
|
291
|
+
)
|
|
@@ -29,6 +29,7 @@ from vacuum_map_parser_base.config.color import ColorsPalette, SupportedColor
|
|
|
29
29
|
from vacuum_map_parser_base.config.image_config import ImageConfig
|
|
30
30
|
from vacuum_map_parser_base.map_data import ImageData, MapData, Point
|
|
31
31
|
|
|
32
|
+
from roborock.data.b01_q10.b01_q10_containers import Q10RoborockPoint
|
|
32
33
|
from roborock.data.containers import RoborockBase
|
|
33
34
|
from roborock.exceptions import RoborockException
|
|
34
35
|
|
|
@@ -222,11 +223,15 @@ class Q10MapPacket:
|
|
|
222
223
|
|
|
223
224
|
@dataclass
|
|
224
225
|
class Q10Point(RoborockBase):
|
|
225
|
-
"""A
|
|
226
|
+
"""A point in the Q10 firmware's dock-relative trace coordinate space."""
|
|
226
227
|
|
|
227
228
|
x: int
|
|
228
229
|
y: int
|
|
229
230
|
|
|
231
|
+
def to_roborock(self) -> Q10RoborockPoint:
|
|
232
|
+
"""Convert this trace point to common Roborock coordinates."""
|
|
233
|
+
return Q10RoborockPoint.from_trace(self.x, self.y)
|
|
234
|
+
|
|
230
235
|
|
|
231
236
|
@dataclass
|
|
232
237
|
class Q10TracePacket:
|