python-roborock 5.21.0__tar.gz → 5.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-5.21.0 → python_roborock-5.23.0}/PKG-INFO +1 -1
- {python_roborock-5.21.0 → python_roborock-5.23.0}/pyproject.toml +1 -1
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/cli.py +33 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +37 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/b01_q10/b01_q10_containers.py +50 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/__init__.py +7 -0
- python_roborock-5.23.0/roborock/devices/traits/b01/q10/clean_history.py +164 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/vacuum.py +27 -5
- {python_roborock-5.21.0 → python_roborock-5.23.0}/.gitignore +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/LICENSE +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/README.md +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/callbacks.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/const.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/code_mappings.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/containers.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/device_features.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/README.md +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/cache.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/device.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/device_manager.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/file_cache.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/rpc/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/rpc/a01_channel.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/rpc/v1_channel.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/button_light.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/child_lock.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/command.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/common.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/consumable.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/do_not_disturb.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/dust_collection.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/map.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/network_info.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/remote.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/status.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/volume.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q7/map.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q7/map_content.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/common.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/common.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/transport/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/transport/channel.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/transport/local_channel.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/transport/mqtt_channel.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/diagnostics.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/exceptions.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/map/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/map/b01_grid_layers.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/map/b01_map_parser.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/map/b01_q10_map_parser.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/map/b01_q10_overlays.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/map/map_parser.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/map/proto/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/map/proto/b01_scmap.proto +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/map/proto/b01_scmap_pb2.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/protocol.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/protocols/__init__.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/protocols/b01_q10_protocol.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/protocols/b01_q7_protocol.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/py.typed +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/roborock_message.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/util.py +0 -0
- {python_roborock-5.21.0 → python_roborock-5.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: 5.
|
|
3
|
+
Version: 5.23.0
|
|
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 = "5.
|
|
3
|
+
version = "5.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"
|
|
@@ -1482,6 +1482,39 @@ async def q10_vacuum_spot(ctx: click.Context, device_id: str) -> None:
|
|
|
1482
1482
|
click.echo(f"Error: {e}")
|
|
1483
1483
|
|
|
1484
1484
|
|
|
1485
|
+
@session.command()
|
|
1486
|
+
@click.option("--device_id", required=True, help="Device ID")
|
|
1487
|
+
@click.option(
|
|
1488
|
+
"--segments",
|
|
1489
|
+
required=True,
|
|
1490
|
+
help="Comma-separated room/segment ids to clean (see the `rooms` command), e.g. 9,2",
|
|
1491
|
+
)
|
|
1492
|
+
@click.pass_context
|
|
1493
|
+
@async_command
|
|
1494
|
+
async def q10_clean_segments(ctx: click.Context, device_id: str, segments: str) -> None:
|
|
1495
|
+
"""Start a room / segment clean on a Q10 device.
|
|
1496
|
+
|
|
1497
|
+
Room ids come from the `rooms` command (the device's map rooms).
|
|
1498
|
+
"""
|
|
1499
|
+
context: RoborockContext = ctx.obj
|
|
1500
|
+
try:
|
|
1501
|
+
segment_ids = [int(s) for s in segments.split(",") if s.strip()]
|
|
1502
|
+
except ValueError:
|
|
1503
|
+
click.echo("--segments must be comma-separated integers, e.g. 9,2")
|
|
1504
|
+
return
|
|
1505
|
+
if not segment_ids:
|
|
1506
|
+
click.echo("No segment ids provided")
|
|
1507
|
+
return
|
|
1508
|
+
try:
|
|
1509
|
+
trait = await _q10_vacuum_trait(context, device_id)
|
|
1510
|
+
await trait.clean_segments(segment_ids)
|
|
1511
|
+
click.echo(f"Starting room clean of segments {segment_ids}...")
|
|
1512
|
+
except RoborockUnsupportedFeature:
|
|
1513
|
+
click.echo("Device does not support B01 Q10 protocol. Is it a Q10?")
|
|
1514
|
+
except RoborockException as e:
|
|
1515
|
+
click.echo(f"Error: {e}")
|
|
1516
|
+
|
|
1517
|
+
|
|
1485
1518
|
async def _q10_set(ctx: click.Context, device_id: str, apply: Callable[[Any], Any], message: str) -> None:
|
|
1486
1519
|
"""Run a Q10 settings write and report the result."""
|
|
1487
1520
|
context: RoborockContext = ctx.obj
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/b01_q10/b01_q10_code_mappings.py
RENAMED
|
@@ -226,6 +226,43 @@ class YXDeviceCleanTask(RoborockModeEnum):
|
|
|
226
226
|
PART = "part", 5
|
|
227
227
|
|
|
228
228
|
|
|
229
|
+
class YXCleanScope(RoborockModeEnum):
|
|
230
|
+
"""Clean scope/type as stored in a *clean record* (``dpCleanRecord``, field 7).
|
|
231
|
+
|
|
232
|
+
This is the same conceptual axis as the live :class:`YXDeviceCleanTask`, but the
|
|
233
|
+
persisted record uses a different integer encoding -- e.g. a full clean records
|
|
234
|
+
``0`` here vs ``1`` (``smart``) live, and a select-rooms clean records ``1`` here
|
|
235
|
+
vs ``2`` (``electoral``) live. Ground-truthed against the app's History labels;
|
|
236
|
+
code ``2`` was never observed on ss07 and is intentionally unmapped (so it
|
|
237
|
+
resolves to ``None`` rather than a guessed label).
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
UNKNOWN = "unknown", -1
|
|
241
|
+
FULL = "full", 0
|
|
242
|
+
SELECTIVE_ROOM = "selective_room", 1
|
|
243
|
+
ZONE = "zone", 3
|
|
244
|
+
SPOT = "spot", 4
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class YXCleaningResult(RoborockModeEnum):
|
|
248
|
+
"""How a clean ended, as stored in a clean record (``dpCleanRecord``, field 9)."""
|
|
249
|
+
|
|
250
|
+
UNKNOWN = "unknown", -1
|
|
251
|
+
INTERRUPTED = "interrupted", 0 # ended on a fault
|
|
252
|
+
COMPLETED = "completed", 1
|
|
253
|
+
STOPPED = "stopped", 2 # ended early without a fault
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class YXStartMethod(RoborockModeEnum):
|
|
257
|
+
"""What initiated a clean, as stored in a clean record (``dpCleanRecord``, field 10)."""
|
|
258
|
+
|
|
259
|
+
UNKNOWN = "unknown", -1
|
|
260
|
+
REMOTE = "remote", 0
|
|
261
|
+
APP = "app", 1
|
|
262
|
+
TIMER = "timer", 2 # schedule / timer
|
|
263
|
+
BUTTON = "button", 3 # device button
|
|
264
|
+
|
|
265
|
+
|
|
229
266
|
class YXDeviceDustCollectionFrequency(RoborockModeEnum):
|
|
230
267
|
# The app exposes "regular" (code 0) vs "frequent", where "frequent" selects
|
|
231
268
|
# one of the every-N-cleans intervals below.
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/b01_q10/b01_q10_containers.py
RENAMED
|
@@ -6,6 +6,7 @@ dataclass fields to device Data Points (DPS). This metadata is utilized by the
|
|
|
6
6
|
automatically update objects from raw device responses.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import datetime
|
|
9
10
|
from dataclasses import dataclass, field
|
|
10
11
|
|
|
11
12
|
from ..containers import RoborockBase
|
|
@@ -14,12 +15,15 @@ from .b01_q10_code_mappings import (
|
|
|
14
15
|
YXAreaUnit,
|
|
15
16
|
YXBackType,
|
|
16
17
|
YXCarpetCleanType,
|
|
18
|
+
YXCleaningResult,
|
|
17
19
|
YXCleanLine,
|
|
20
|
+
YXCleanScope,
|
|
18
21
|
YXCleanType,
|
|
19
22
|
YXDeviceCleanTask,
|
|
20
23
|
YXDeviceDustCollectionFrequency,
|
|
21
24
|
YXDeviceState,
|
|
22
25
|
YXFanLevel,
|
|
26
|
+
YXStartMethod,
|
|
23
27
|
YXWaterLevel,
|
|
24
28
|
)
|
|
25
29
|
|
|
@@ -32,6 +36,52 @@ class dpCleanRecord(RoborockBase):
|
|
|
32
36
|
data: list
|
|
33
37
|
|
|
34
38
|
|
|
39
|
+
@dataclass
|
|
40
|
+
class Q10CleanRecord(RoborockBase):
|
|
41
|
+
"""A single Q10 (ss07) clean record decoded from a ``dpCleanRecord`` (DP 52) entry.
|
|
42
|
+
|
|
43
|
+
The device returns each record as a 12-field underscore-delimited string in the
|
|
44
|
+
``data`` list of a ``{"op": "list"}`` query (or the ``id`` of an ``{"op": "notify"}``
|
|
45
|
+
push). The ``*_len`` values are internal blob-length metrics whose units aren't
|
|
46
|
+
confirmed; the original ``raw`` string is always retained. The enum fields resolve
|
|
47
|
+
an unmapped/unset code to ``None`` rather than guessing.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
raw: str
|
|
51
|
+
record_id: str | None = None
|
|
52
|
+
start_time: int | None = None
|
|
53
|
+
"""Clean start time, Unix seconds."""
|
|
54
|
+
clean_time: int | None = None
|
|
55
|
+
"""Cleaning time, minutes."""
|
|
56
|
+
clean_area: int | None = None
|
|
57
|
+
"""Cleaned area in square meters."""
|
|
58
|
+
map_len: int | None = None
|
|
59
|
+
"""Length of the saved map blob for this record (0 = none stored)."""
|
|
60
|
+
path_len: int | None = None
|
|
61
|
+
"""Length of the saved path blob for this record (0 = none stored)."""
|
|
62
|
+
virtual_len: int | None = None
|
|
63
|
+
"""Length of the saved virtual-restriction blob for this record (0 = none stored)."""
|
|
64
|
+
clean_mode: YXCleanScope | None = None
|
|
65
|
+
"""Clean scope/type (full / selective-room / zone / spot). Same axis as the live
|
|
66
|
+
:class:`YXDeviceCleanTask` but a different record encoding -- see :class:`YXCleanScope`."""
|
|
67
|
+
work_mode: YXCleanType | None = None
|
|
68
|
+
"""Actual work performed (vac+mop / vacuum / mop) -- the same enum :class:`Q10Status`
|
|
69
|
+
uses for the live clean-mode DP. Records only ever carry 1/2/3 here."""
|
|
70
|
+
cleaning_result: YXCleaningResult | None = None
|
|
71
|
+
"""How the clean ended: 0 interrupted (fault), 1 completed, 2 stopped (no fault)."""
|
|
72
|
+
start_method: YXStartMethod | None = None
|
|
73
|
+
"""What initiated the clean: 0 remote, 1 app, 2 timer, 3 button."""
|
|
74
|
+
collect_dust_count: int | None = None
|
|
75
|
+
"""Number of dock auto-empties during the clean."""
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def start_datetime(self) -> datetime.datetime | None:
|
|
79
|
+
"""The start time as a timezone-aware (UTC) datetime."""
|
|
80
|
+
if self.start_time is not None:
|
|
81
|
+
return datetime.datetime.fromtimestamp(self.start_time).astimezone(datetime.UTC)
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
35
85
|
@dataclass
|
|
36
86
|
class dpMultiMap(RoborockBase):
|
|
37
87
|
op: str
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/__init__.py
RENAMED
|
@@ -12,6 +12,7 @@ from roborock.protocols.b01_q10_protocol import Q10DpsUpdate, Q10Message
|
|
|
12
12
|
|
|
13
13
|
from .button_light import ButtonLightTrait
|
|
14
14
|
from .child_lock import ChildLockTrait
|
|
15
|
+
from .clean_history import CleanHistoryTrait
|
|
15
16
|
from .command import CommandTrait
|
|
16
17
|
from .consumable import ConsumableTrait
|
|
17
18
|
from .do_not_disturb import DoNotDisturbTrait
|
|
@@ -27,6 +28,7 @@ __all__ = [
|
|
|
27
28
|
"Q10PropertiesApi",
|
|
28
29
|
"ButtonLightTrait",
|
|
29
30
|
"ChildLockTrait",
|
|
31
|
+
"CleanHistoryTrait",
|
|
30
32
|
"ConsumableTrait",
|
|
31
33
|
"DoNotDisturbTrait",
|
|
32
34
|
"DustCollectionTrait",
|
|
@@ -78,6 +80,9 @@ class Q10PropertiesApi(Trait):
|
|
|
78
80
|
map: MapContentTrait
|
|
79
81
|
"""Trait for fetching the current parsed map (image + rooms)."""
|
|
80
82
|
|
|
83
|
+
clean_history: CleanHistoryTrait
|
|
84
|
+
"""Trait for fetching the device clean-record history (``dpCleanRecord``)."""
|
|
85
|
+
|
|
81
86
|
def __init__(self, channel: MqttChannel) -> None:
|
|
82
87
|
"""Initialize the B01Props API."""
|
|
83
88
|
self._channel = channel
|
|
@@ -93,6 +98,7 @@ class Q10PropertiesApi(Trait):
|
|
|
93
98
|
self.network_info = NetworkInfoTrait()
|
|
94
99
|
self.consumable = ConsumableTrait()
|
|
95
100
|
self.map = MapContentTrait()
|
|
101
|
+
self.clean_history = CleanHistoryTrait(self.command)
|
|
96
102
|
# Read-model traits updated from the device's DPS push stream.
|
|
97
103
|
self._updatable_traits = [
|
|
98
104
|
self.status,
|
|
@@ -102,6 +108,7 @@ class Q10PropertiesApi(Trait):
|
|
|
102
108
|
self.dust_collection,
|
|
103
109
|
self.network_info,
|
|
104
110
|
self.consumable,
|
|
111
|
+
self.clean_history,
|
|
105
112
|
]
|
|
106
113
|
self._subscribe_task: asyncio.Task[None] | None = None
|
|
107
114
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Clean history trait for Q10 B01 devices.
|
|
2
|
+
|
|
3
|
+
Unlike the Q7 (which exposes a synchronous ``service.get_record_list`` RPC), the
|
|
4
|
+
Q10 is push-driven: :meth:`CleanHistoryTrait.refresh` sends an ``{"op": "list"}``
|
|
5
|
+
query for ``dpCleanRecord`` (DP 52) over the ``dpCommon`` channel, and the device
|
|
6
|
+
publishes its clean-record list back on the subscribe stream, which
|
|
7
|
+
:meth:`CleanHistoryTrait.update_from_dps` then decodes.
|
|
8
|
+
|
|
9
|
+
Wire parsing is separated from state management: :class:`CleanRecordConverter` turns
|
|
10
|
+
a ``dpCleanRecord`` envelope into a :class:`CleanRecordPush`, and the trait applies it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import (
|
|
18
|
+
B01_Q10_DP,
|
|
19
|
+
YXCleaningResult,
|
|
20
|
+
YXCleanScope,
|
|
21
|
+
YXCleanType,
|
|
22
|
+
YXStartMethod,
|
|
23
|
+
)
|
|
24
|
+
from roborock.data.b01_q10.b01_q10_containers import Q10CleanRecord
|
|
25
|
+
|
|
26
|
+
from .command import CommandTrait
|
|
27
|
+
from .common import UpdatableTrait
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"CleanHistoryTrait",
|
|
31
|
+
"CleanRecordConverter",
|
|
32
|
+
"CleanRecordPush",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
_LOGGER = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
_RECORD_FIELD_COUNT = 12
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class CleanRecordPush:
|
|
42
|
+
"""A parsed ``dpCleanRecord`` push: the records it carries and how to apply them.
|
|
43
|
+
|
|
44
|
+
``replace`` is ``True`` for an ``{"op": "list"}`` reply (the full history, which
|
|
45
|
+
replaces the current state) and ``False`` for an ``{"op": "notify"}`` push (a
|
|
46
|
+
single just-finished record, which is upserted).
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
records: list[Q10CleanRecord] = field(default_factory=list)
|
|
50
|
+
replace: bool = False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CleanRecordConverter:
|
|
54
|
+
"""Converts a raw ``dpCleanRecord`` (DP 52) envelope into a :class:`CleanRecordPush`.
|
|
55
|
+
|
|
56
|
+
Mirrors the converter-per-object pattern used by the other Q10 traits: parsing the
|
|
57
|
+
wire payload lives here, separate from the trait that manages the record list.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def parse(self, envelope: dict[str, Any]) -> CleanRecordPush | None:
|
|
61
|
+
"""Parse a decoded ``dpCleanRecord`` envelope into a :class:`CleanRecordPush`.
|
|
62
|
+
|
|
63
|
+
Returns ``None`` for an envelope that carries nothing usable (so the trait can
|
|
64
|
+
ignore it without changing state). Malformed individual records are skipped.
|
|
65
|
+
"""
|
|
66
|
+
if envelope.get("op") == "notify":
|
|
67
|
+
record = CleanRecordConverter.parse_record(envelope.get("id"))
|
|
68
|
+
return CleanRecordPush([record], replace=False) if record is not None else None
|
|
69
|
+
data = envelope.get("data")
|
|
70
|
+
if not isinstance(data, list):
|
|
71
|
+
return None
|
|
72
|
+
records = [record for item in data if (record := CleanRecordConverter.parse_record(item)) is not None]
|
|
73
|
+
return CleanRecordPush(records, replace=True)
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def parse_record(raw: Any | None) -> Q10CleanRecord | None:
|
|
77
|
+
"""Decode one underscore-delimited clean-record string into a :class:`Q10CleanRecord`.
|
|
78
|
+
|
|
79
|
+
The device joins 12 values with ``_``: recordId, startTime (Unix s), cleanTime
|
|
80
|
+
(min), cleanArea (m2), mapLen, pathLen, virtualLen, cleanMode, workMode,
|
|
81
|
+
cleaningResult, startMethod, collectDustCount. Returns ``None`` for anything but
|
|
82
|
+
a well-formed 12-field string; an unmapped enum code resolves to ``None`` on its
|
|
83
|
+
field (``raw`` keeps the original).
|
|
84
|
+
"""
|
|
85
|
+
if not isinstance(raw, str):
|
|
86
|
+
return None
|
|
87
|
+
parts = raw.split("_")
|
|
88
|
+
if len(parts) != _RECORD_FIELD_COUNT:
|
|
89
|
+
return None
|
|
90
|
+
try:
|
|
91
|
+
return Q10CleanRecord(
|
|
92
|
+
raw=raw,
|
|
93
|
+
record_id=parts[0],
|
|
94
|
+
start_time=int(parts[1]),
|
|
95
|
+
clean_time=int(parts[2]),
|
|
96
|
+
clean_area=int(parts[3]),
|
|
97
|
+
map_len=int(parts[4]),
|
|
98
|
+
path_len=int(parts[5]),
|
|
99
|
+
virtual_len=int(parts[6]),
|
|
100
|
+
clean_mode=YXCleanScope.from_code_optional(int(parts[7])),
|
|
101
|
+
work_mode=YXCleanType.from_code_optional(int(parts[8])),
|
|
102
|
+
cleaning_result=YXCleaningResult.from_code_optional(int(parts[9])),
|
|
103
|
+
start_method=YXStartMethod.from_code_optional(int(parts[10])),
|
|
104
|
+
collect_dust_count=int(parts[11]),
|
|
105
|
+
)
|
|
106
|
+
except ValueError:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class CleanHistoryTrait(UpdatableTrait):
|
|
111
|
+
"""Access to the Q10 clean-record history (``dpCleanRecord``, DP 52).
|
|
112
|
+
|
|
113
|
+
A read-model trait updated from the DPS stream like the others, but it overrides
|
|
114
|
+
:meth:`update_from_dps` because the payload is a structured push (a record list,
|
|
115
|
+
or a single ``op:"notify"`` record) rather than a flat data-point-to-field map.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, command: CommandTrait) -> None:
|
|
119
|
+
"""Initialize the clean history trait."""
|
|
120
|
+
UpdatableTrait.__init__(self, command, _LOGGER)
|
|
121
|
+
self._converter = CleanRecordConverter()
|
|
122
|
+
self.records: list[Q10CleanRecord] = []
|
|
123
|
+
"""Decoded clean records, most recent first."""
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def last_record(self) -> Q10CleanRecord | None:
|
|
127
|
+
"""The most recent clean record, or ``None`` if there are none."""
|
|
128
|
+
return self.records[0] if self.records else None
|
|
129
|
+
|
|
130
|
+
async def refresh(self) -> None:
|
|
131
|
+
"""Request the clean-record list from the device.
|
|
132
|
+
|
|
133
|
+
This sends the query and returns immediately; the records arrive
|
|
134
|
+
asynchronously on the device stream and populate :attr:`records` once
|
|
135
|
+
:meth:`update_from_dps` processes the ``dpCleanRecord`` push.
|
|
136
|
+
"""
|
|
137
|
+
if self._command is None:
|
|
138
|
+
raise ValueError("Trait is read-only; no command channel was provided")
|
|
139
|
+
await self._command.send(
|
|
140
|
+
B01_Q10_DP.COMMON,
|
|
141
|
+
params={str(B01_Q10_DP.CLEAN_RECORD.code): {"op": "list"}},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def update_from_dps(self, decoded_dps: dict[B01_Q10_DP, Any]) -> None:
|
|
145
|
+
"""Apply a ``dpCleanRecord`` push (a full list reply or a single notify)."""
|
|
146
|
+
envelope = decoded_dps.get(B01_Q10_DP.CLEAN_RECORD)
|
|
147
|
+
if not isinstance(envelope, dict):
|
|
148
|
+
return
|
|
149
|
+
push = self._converter.parse(envelope)
|
|
150
|
+
if push is None:
|
|
151
|
+
return
|
|
152
|
+
self._apply(push)
|
|
153
|
+
|
|
154
|
+
def _apply(self, push: CleanRecordPush) -> None:
|
|
155
|
+
"""Merge or replace the records from ``push``, then sort newest-first and notify."""
|
|
156
|
+
if push.replace:
|
|
157
|
+
records = list(push.records)
|
|
158
|
+
else:
|
|
159
|
+
updated_ids = {record.record_id for record in push.records}
|
|
160
|
+
records = [record for record in self.records if record.record_id not in updated_ids]
|
|
161
|
+
records.extend(push.records)
|
|
162
|
+
records.sort(key=lambda record: record.start_time or 0, reverse=True)
|
|
163
|
+
self.records = records
|
|
164
|
+
self._notify_update()
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from roborock.data.b01_q10.b01_q10_code_mappings import (
|
|
4
4
|
B01_Q10_DP,
|
|
5
5
|
YXCleanType,
|
|
6
|
+
YXDeviceCleanTask,
|
|
6
7
|
YXFanLevel,
|
|
7
8
|
)
|
|
8
9
|
|
|
@@ -23,17 +24,38 @@ class VacuumTrait:
|
|
|
23
24
|
async def start_clean(self) -> None:
|
|
24
25
|
"""Start a whole-home clean.
|
|
25
26
|
|
|
26
|
-
The ``dpStartClean`` (201) command
|
|
27
|
-
|
|
28
|
-
``5`` = spot. Whole-home and spot
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
The ``dpStartClean`` (201) command selects a task by code: ``1`` =
|
|
28
|
+
whole-home, ``2`` = segment/room (see :meth:`clean_segments`), ``3`` =
|
|
29
|
+
zone, ``4`` = build map, ``5`` = spot. Whole-home and spot accept the
|
|
30
|
+
bare integer code; segment cleaning needs a room selection (an object
|
|
31
|
+
payload) instead.
|
|
31
32
|
|
|
32
33
|
Verified live against ss07 hardware: ``{"dps": {"201": 1}}`` starts a
|
|
33
34
|
whole-home clean (clean_task_type -> 1).
|
|
34
35
|
"""
|
|
35
36
|
await self._command.send(command=B01_Q10_DP.START_CLEAN, params=1)
|
|
36
37
|
|
|
38
|
+
async def clean_segments(self, segment_ids: list[int]) -> None:
|
|
39
|
+
"""Start a room / segment clean for the given segment (room) ids.
|
|
40
|
+
|
|
41
|
+
The ids are the same room ids the device reports on its map (see the Q10
|
|
42
|
+
``MapContentTrait`` -- ``map.rooms``, each with an ``id``).
|
|
43
|
+
|
|
44
|
+
Unlike whole-home and spot, ``dpStartClean`` (201) carries the room
|
|
45
|
+
selection as an object: ``{"cmd": <task>, "clean_paramters": [<id>, ...]}``,
|
|
46
|
+
where ``cmd`` is the segment-clean task code.
|
|
47
|
+
|
|
48
|
+
Verified live against ss07 hardware: sending
|
|
49
|
+
``{"dps": {"201": {"cmd": 2, "clean_paramters": [9]}}}`` starts cleaning
|
|
50
|
+
room 9 (clean_task_type -> 2 / electoral).
|
|
51
|
+
"""
|
|
52
|
+
await self._command.send(
|
|
53
|
+
command=B01_Q10_DP.START_CLEAN,
|
|
54
|
+
# "clean_paramters" intentionally mirrors the device's misspelling of
|
|
55
|
+
# "parameters" -- the firmware only accepts that exact key.
|
|
56
|
+
params={"cmd": YXDeviceCleanTask.ELECTORAL.code, "clean_paramters": segment_ids},
|
|
57
|
+
)
|
|
58
|
+
|
|
37
59
|
async def spot_clean(self) -> None:
|
|
38
60
|
"""Start a spot / part clean around the robot's current position.
|
|
39
61
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/data/b01_q7/b01_q7_code_mappings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/button_light.py
RENAMED
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/child_lock.py
RENAMED
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/command.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/consumable.py
RENAMED
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/dust_collection.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q10/network_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q7/__init__.py
RENAMED
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q7/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/b01/q7/map_content.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/device_features.py
RENAMED
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/dust_collection_mode.py
RENAMED
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/flow_led_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/network_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/smart_wash_params.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/traits/v1/wash_towel_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/transport/local_channel.py
RENAMED
|
File without changes
|
{python_roborock-5.21.0 → python_roborock-5.23.0}/roborock/devices/transport/mqtt_channel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|