python-roborock 5.18.0__tar.gz → 5.20.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_roborock-5.18.0 → python_roborock-5.20.0}/PKG-INFO +1 -1
- {python_roborock-5.18.0 → python_roborock-5.20.0}/pyproject.toml +1 -1
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/cli.py +103 -4
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/rpc/b01_q10_channel.py +16 -10
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/b01/q10/__init__.py +28 -9
- python_roborock-5.20.0/roborock/devices/traits/b01/q10/map.py +110 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/b01/q7/__init__.py +23 -0
- python_roborock-5.20.0/roborock/map/b01_q10_map_parser.py +399 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/protocols/b01_q10_protocol.py +43 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/.gitignore +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/LICENSE +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/README.md +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/callbacks.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/const.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/code_mappings.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/containers.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/device_features.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/README.md +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/cache.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/device.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/device_manager.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/file_cache.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/rpc/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/rpc/a01_channel.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/rpc/v1_channel.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/b01/q10/command.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/b01/q10/remote.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/b01/q10/status.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/b01/q7/map.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/b01/q7/map_content.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/common.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/common.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/transport/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/transport/channel.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/transport/local_channel.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/transport/mqtt_channel.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/diagnostics.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/exceptions.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/map/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/map/b01_grid_layers.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/map/b01_map_parser.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/map/b01_q10_overlays.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/map/map_parser.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/map/proto/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/map/proto/b01_scmap.proto +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/map/proto/b01_scmap_pb2.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/protocol.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/protocols/__init__.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/protocols/b01_q7_protocol.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/py.typed +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/roborock_message.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/util.py +0 -0
- {python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.20.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.20.0"
|
|
4
4
|
description = "A package to control Roborock vacuums."
|
|
5
5
|
authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
|
|
6
6
|
requires-python = ">=3.11, <4"
|
|
@@ -56,6 +56,7 @@ from roborock.devices.cache import Cache, CacheData
|
|
|
56
56
|
from roborock.devices.device import RoborockDevice
|
|
57
57
|
from roborock.devices.device_manager import DeviceManager, UserParams, create_device_manager
|
|
58
58
|
from roborock.devices.traits import Trait
|
|
59
|
+
from roborock.devices.traits.b01.q10 import Q10PropertiesApi
|
|
59
60
|
from roborock.devices.traits.b01.q10.vacuum import VacuumTrait
|
|
60
61
|
from roborock.devices.traits.v1 import V1TraitMixin
|
|
61
62
|
from roborock.devices.traits.v1.consumeable import ConsumableAttribute
|
|
@@ -527,6 +528,45 @@ async def maps(ctx, device_id: str):
|
|
|
527
528
|
await _display_v1_trait(context, device_id, lambda v1: v1.maps)
|
|
528
529
|
|
|
529
530
|
|
|
531
|
+
# The Q10 pushes its map ~9s after a dpRequestDps; firmware throttles pushes to
|
|
532
|
+
# ~once per 60-70s, so a single request is answered quickly but rapid re-requests
|
|
533
|
+
# may not be. This bounds how long a one-shot CLI command waits for that push.
|
|
534
|
+
_Q10_MAP_PUSH_TIMEOUT = 30.0
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
async def _await_q10_map_push(
|
|
538
|
+
properties: Q10PropertiesApi,
|
|
539
|
+
predicate: Callable[[], bool],
|
|
540
|
+
*,
|
|
541
|
+
timeout: float = _Q10_MAP_PUSH_TIMEOUT,
|
|
542
|
+
allow_cached_on_timeout: bool = False,
|
|
543
|
+
) -> bool:
|
|
544
|
+
"""Nudge a Q10 to push its map/trace and wait for a fresh update.
|
|
545
|
+
|
|
546
|
+
The Q10 map API is entirely push-driven: there is no synchronous get-map
|
|
547
|
+
request. A ``dpRequestDps`` causes the device to publish a ``MAP_RESPONSE``,
|
|
548
|
+
which the device's subscribe loop feeds into the map trait. Here we register
|
|
549
|
+
an update listener, send the request, and wait for a newly pushed update to
|
|
550
|
+
satisfy ``predicate``. Returns whether it did within ``timeout``.
|
|
551
|
+
"""
|
|
552
|
+
loop = asyncio.get_running_loop()
|
|
553
|
+
updated: asyncio.Future[None] = loop.create_future()
|
|
554
|
+
|
|
555
|
+
def on_update() -> None:
|
|
556
|
+
if predicate() and not updated.done():
|
|
557
|
+
updated.set_result(None)
|
|
558
|
+
|
|
559
|
+
unsub = properties.map.add_update_listener(on_update)
|
|
560
|
+
try:
|
|
561
|
+
await properties.refresh()
|
|
562
|
+
await asyncio.wait_for(updated, timeout=timeout)
|
|
563
|
+
return True
|
|
564
|
+
except TimeoutError:
|
|
565
|
+
return allow_cached_on_timeout and predicate()
|
|
566
|
+
finally:
|
|
567
|
+
unsub()
|
|
568
|
+
|
|
569
|
+
|
|
530
570
|
@session.command()
|
|
531
571
|
@click.option("--device_id", required=True)
|
|
532
572
|
@click.option("--output-file", required=True, help="Path to save the map image.")
|
|
@@ -535,10 +575,22 @@ async def maps(ctx, device_id: str):
|
|
|
535
575
|
async def map_image(ctx, device_id: str, output_file: str):
|
|
536
576
|
"""Get device map image and save it to a file."""
|
|
537
577
|
context: RoborockContext = ctx.obj
|
|
538
|
-
|
|
539
|
-
|
|
578
|
+
device_manager = await context.get_device_manager()
|
|
579
|
+
device = await device_manager.get_device(device_id)
|
|
580
|
+
if device.b01_q10_properties is not None:
|
|
581
|
+
properties = device.b01_q10_properties
|
|
582
|
+
await _await_q10_map_push(
|
|
583
|
+
properties,
|
|
584
|
+
lambda: properties.map.image_content is not None,
|
|
585
|
+
allow_cached_on_timeout=True,
|
|
586
|
+
)
|
|
587
|
+
image_content = properties.map.image_content
|
|
588
|
+
else:
|
|
589
|
+
v1_trait: MapContentTrait = await _v1_trait(context, device_id, lambda v1: v1.map_content)
|
|
590
|
+
image_content = v1_trait.image_content
|
|
591
|
+
if image_content:
|
|
540
592
|
with open(output_file, "wb") as f:
|
|
541
|
-
f.write(
|
|
593
|
+
f.write(image_content)
|
|
542
594
|
click.echo(f"Map image saved to {output_file}")
|
|
543
595
|
else:
|
|
544
596
|
click.echo("No map image content available.")
|
|
@@ -570,6 +622,39 @@ async def map_data(ctx, device_id: str, include_path: bool):
|
|
|
570
622
|
click.echo(dump_json(data_summary))
|
|
571
623
|
|
|
572
624
|
|
|
625
|
+
@session.command()
|
|
626
|
+
@click.option("--device_id", required=True)
|
|
627
|
+
@click.option("--include_path", is_flag=True, default=False, help="Include all path points in the output.")
|
|
628
|
+
@click.pass_context
|
|
629
|
+
@async_command
|
|
630
|
+
async def q10_position(ctx, device_id: str, include_path: bool):
|
|
631
|
+
"""Get the current Q10 robot position and live cleaning path.
|
|
632
|
+
|
|
633
|
+
The Q10 only streams its position/path while it is actively cleaning, so this
|
|
634
|
+
will report that no live trace is available for an idle/docked robot.
|
|
635
|
+
"""
|
|
636
|
+
context: RoborockContext = ctx.obj
|
|
637
|
+
device_manager = await context.get_device_manager()
|
|
638
|
+
device = await device_manager.get_device(device_id)
|
|
639
|
+
if device.b01_q10_properties is None:
|
|
640
|
+
click.echo("Feature not supported by device")
|
|
641
|
+
return
|
|
642
|
+
properties = device.b01_q10_properties
|
|
643
|
+
got_trace = await _await_q10_map_push(properties, lambda: bool(properties.map.path))
|
|
644
|
+
if not got_trace:
|
|
645
|
+
click.echo("No live trace available (the robot only reports position while cleaning).")
|
|
646
|
+
return
|
|
647
|
+
map_trait = properties.map
|
|
648
|
+
position = map_trait.robot_position
|
|
649
|
+
summary: dict[str, Any] = {
|
|
650
|
+
"robot_position": {"x": position.x, "y": position.y} if position else None,
|
|
651
|
+
"path_points": len(map_trait.path),
|
|
652
|
+
}
|
|
653
|
+
if include_path:
|
|
654
|
+
summary["path"] = [[p.x, p.y] for p in map_trait.path]
|
|
655
|
+
click.echo(dump_json(summary))
|
|
656
|
+
|
|
657
|
+
|
|
573
658
|
@session.command()
|
|
574
659
|
@click.option("--device_id", required=True)
|
|
575
660
|
@click.pass_context
|
|
@@ -711,7 +796,20 @@ async def set_child_lock(ctx, device_id: str, enabled: bool):
|
|
|
711
796
|
async def rooms(ctx, device_id: str):
|
|
712
797
|
"""Get device room mapping info."""
|
|
713
798
|
context: RoborockContext = ctx.obj
|
|
714
|
-
await
|
|
799
|
+
device_manager = await context.get_device_manager()
|
|
800
|
+
device = await device_manager.get_device(device_id)
|
|
801
|
+
if device.b01_q10_properties is not None:
|
|
802
|
+
properties = device.b01_q10_properties
|
|
803
|
+
# A valid map may have no room records, so wait on the map arriving
|
|
804
|
+
# (image_content) rather than on rooms being non-empty.
|
|
805
|
+
await _await_q10_map_push(
|
|
806
|
+
properties,
|
|
807
|
+
lambda: properties.map.image_content is not None,
|
|
808
|
+
allow_cached_on_timeout=True,
|
|
809
|
+
)
|
|
810
|
+
click.echo(dump_json({room.id: room.name for room in properties.map.rooms}))
|
|
811
|
+
else:
|
|
812
|
+
await _display_v1_trait(context, device_id, lambda v1: v1.rooms)
|
|
715
813
|
|
|
716
814
|
|
|
717
815
|
@session.command()
|
|
@@ -1200,6 +1298,7 @@ cli.add_command(set_volume)
|
|
|
1200
1298
|
cli.add_command(maps)
|
|
1201
1299
|
cli.add_command(map_image)
|
|
1202
1300
|
cli.add_command(map_data)
|
|
1301
|
+
cli.add_command(q10_position)
|
|
1203
1302
|
cli.add_command(consumables)
|
|
1204
1303
|
cli.add_command(reset_consumable)
|
|
1205
1304
|
cli.add_command(rooms)
|
|
@@ -2,36 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from collections.abc import AsyncGenerator
|
|
5
|
-
from typing import Any
|
|
6
5
|
|
|
7
6
|
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
8
7
|
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
9
8
|
from roborock.exceptions import RoborockException
|
|
10
9
|
from roborock.protocols.b01_q10_protocol import (
|
|
11
10
|
ParamsType,
|
|
12
|
-
|
|
11
|
+
Q10Message,
|
|
12
|
+
decode_message,
|
|
13
13
|
encode_mqtt_payload,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
_LOGGER = logging.getLogger(__name__)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
async def
|
|
19
|
+
async def stream_decoded_messages(
|
|
20
20
|
mqtt_channel: MqttChannel,
|
|
21
|
-
) -> AsyncGenerator[
|
|
22
|
-
"""Stream decoded
|
|
21
|
+
) -> AsyncGenerator[Q10Message, None]:
|
|
22
|
+
"""Stream decoded Q10 messages received via MQTT.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Each pushed ``RoborockMessage`` is decoded into a typed :data:`Q10Message`
|
|
25
|
+
(a DPS status update, a map packet, or a trace packet). Messages that fail
|
|
26
|
+
to decode or carry an unrecognized payload are skipped.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
async for message in mqtt_channel.subscribe_stream():
|
|
25
30
|
try:
|
|
26
|
-
|
|
31
|
+
decoded = decode_message(message)
|
|
27
32
|
except RoborockException as ex:
|
|
28
33
|
_LOGGER.debug(
|
|
29
|
-
"Failed to decode B01 Q10
|
|
30
|
-
|
|
34
|
+
"Failed to decode B01 Q10 message: %s: %s",
|
|
35
|
+
message,
|
|
31
36
|
ex,
|
|
32
37
|
)
|
|
33
38
|
continue
|
|
34
|
-
|
|
39
|
+
if decoded is not None:
|
|
40
|
+
yield decoded
|
|
35
41
|
|
|
36
42
|
|
|
37
43
|
async def send_command(
|
{python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/b01/q10/__init__.py
RENAMED
|
@@ -4,17 +4,21 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
|
|
6
6
|
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
7
|
-
from roborock.devices.rpc.b01_q10_channel import
|
|
7
|
+
from roborock.devices.rpc.b01_q10_channel import stream_decoded_messages
|
|
8
8
|
from roborock.devices.traits import Trait
|
|
9
9
|
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
10
|
+
from roborock.map.b01_q10_map_parser import Q10MapPacket, Q10TracePacket
|
|
11
|
+
from roborock.protocols.b01_q10_protocol import Q10DpsUpdate, Q10Message
|
|
10
12
|
|
|
11
13
|
from .command import CommandTrait
|
|
14
|
+
from .map import MapContentTrait
|
|
12
15
|
from .remote import RemoteTrait
|
|
13
16
|
from .status import StatusTrait
|
|
14
17
|
from .vacuum import VacuumTrait
|
|
15
18
|
|
|
16
19
|
__all__ = [
|
|
17
20
|
"Q10PropertiesApi",
|
|
21
|
+
"MapContentTrait",
|
|
18
22
|
]
|
|
19
23
|
|
|
20
24
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -35,6 +39,9 @@ class Q10PropertiesApi(Trait):
|
|
|
35
39
|
remote: RemoteTrait
|
|
36
40
|
"""Trait for sending remote control related commands to Q10 devices."""
|
|
37
41
|
|
|
42
|
+
map: MapContentTrait
|
|
43
|
+
"""Trait for fetching the current parsed map (image + rooms)."""
|
|
44
|
+
|
|
38
45
|
def __init__(self, channel: MqttChannel) -> None:
|
|
39
46
|
"""Initialize the B01Props API."""
|
|
40
47
|
self._channel = channel
|
|
@@ -42,6 +49,7 @@ class Q10PropertiesApi(Trait):
|
|
|
42
49
|
self.vacuum = VacuumTrait(self.command)
|
|
43
50
|
self.remote = RemoteTrait(self.command)
|
|
44
51
|
self.status = StatusTrait()
|
|
52
|
+
self.map = MapContentTrait()
|
|
45
53
|
self._subscribe_task: asyncio.Task[None] | None = None
|
|
46
54
|
|
|
47
55
|
async def start(self) -> None:
|
|
@@ -65,14 +73,25 @@ class Q10PropertiesApi(Trait):
|
|
|
65
73
|
await self.command.send(B01_Q10_DP.REQUEST_DPS, params={})
|
|
66
74
|
|
|
67
75
|
async def _subscribe_loop(self) -> None:
|
|
68
|
-
"""Persistent loop to
|
|
69
|
-
async for
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
"""Persistent loop dispatching decoded messages to the read-model traits."""
|
|
77
|
+
async for message in stream_decoded_messages(self._channel):
|
|
78
|
+
self._handle_message(message)
|
|
79
|
+
|
|
80
|
+
def _handle_message(self, message: Q10Message) -> None:
|
|
81
|
+
"""Route a single decoded message to the trait responsible for it.
|
|
82
|
+
|
|
83
|
+
Map and trace packets arrive as protocol-301 ``MAP_RESPONSE`` pushes (the
|
|
84
|
+
Q10 is entirely push-driven: there is no synchronous get-map request, a
|
|
85
|
+
``dpRequestDps`` just nudges the device to publish its current map). DPS
|
|
86
|
+
updates feed the status trait. More traits can be dispatched here below.
|
|
87
|
+
"""
|
|
88
|
+
if isinstance(message, Q10MapPacket):
|
|
89
|
+
self.map.update_from_map_packet(message)
|
|
90
|
+
elif isinstance(message, Q10TracePacket):
|
|
91
|
+
self.map.update_from_trace_packet(message)
|
|
92
|
+
elif isinstance(message, Q10DpsUpdate):
|
|
93
|
+
_LOGGER.debug("Received Q10 status update: %s", message.dps)
|
|
94
|
+
self.status.update_from_dps(message.dps)
|
|
76
95
|
|
|
77
96
|
|
|
78
97
|
def create(channel: MqttChannel) -> Q10PropertiesApi:
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Map content trait for B01 Q10 devices.
|
|
2
|
+
|
|
3
|
+
Unlike the v1 / Q7 maps, the Q10 has no synchronous "get map" command, so this
|
|
4
|
+
trait is purely push-driven and mirrors the Q10 ``StatusTrait`` contract:
|
|
5
|
+
|
|
6
|
+
- The device pushes its current map/path as protocol-301 ``MAP_RESPONSE``
|
|
7
|
+
messages (a ``dpRequestDps`` nudges it to do so). The protocol layer decodes
|
|
8
|
+
those into :class:`Q10MapPacket` / :class:`Q10TracePacket` objects and the
|
|
9
|
+
``Q10PropertiesApi`` subscribe loop routes them to
|
|
10
|
+
:meth:`MapContentTrait.update_from_map_packet` /
|
|
11
|
+
:meth:`MapContentTrait.update_from_trace_packet`.
|
|
12
|
+
- Those methods render/cache the content and notify update listeners (register
|
|
13
|
+
via :meth:`add_update_listener`).
|
|
14
|
+
- ``image_content``, ``map_data``, ``rooms``, ``path`` and ``robot_position``
|
|
15
|
+
are readable and reflect the most recently pushed map.
|
|
16
|
+
|
|
17
|
+
Unlike the Q7, the Q10 map payload is unencrypted, so no map key is required.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
|
|
23
|
+
from vacuum_map_parser_base.map_data import MapData
|
|
24
|
+
|
|
25
|
+
from roborock.data import RoborockBase
|
|
26
|
+
from roborock.devices.traits.common import TraitUpdateListener
|
|
27
|
+
from roborock.map.b01_q10_map_parser import (
|
|
28
|
+
B01Q10MapParser,
|
|
29
|
+
B01Q10MapParserConfig,
|
|
30
|
+
Q10MapPacket,
|
|
31
|
+
Q10Point,
|
|
32
|
+
Q10Room,
|
|
33
|
+
Q10TracePacket,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_LOGGER = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
_TRUNCATE_LENGTH = 20
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class MapContent(RoborockBase):
|
|
43
|
+
"""Dataclass representing Q10 map content."""
|
|
44
|
+
|
|
45
|
+
image_content: bytes | None = None
|
|
46
|
+
"""The rendered image of the map in PNG format."""
|
|
47
|
+
|
|
48
|
+
map_data: MapData | None = None
|
|
49
|
+
"""Parsed map data (image metadata + room names)."""
|
|
50
|
+
|
|
51
|
+
rooms: list[Q10Room] = field(default_factory=list)
|
|
52
|
+
"""Rooms (segments) reported by the device, with ids and names."""
|
|
53
|
+
|
|
54
|
+
path: list[Q10Point] = field(default_factory=list)
|
|
55
|
+
"""Full path of the current cleaning session (oldest point first).
|
|
56
|
+
|
|
57
|
+
The robot accumulates this server-side and serves the whole trajectory so
|
|
58
|
+
far in one packet, so it is complete even if we connect mid-session. Only
|
|
59
|
+
populated while a cleaning session is active."""
|
|
60
|
+
|
|
61
|
+
robot_position: Q10Point | None = None
|
|
62
|
+
"""Current robot position (the most recent path point), if known."""
|
|
63
|
+
|
|
64
|
+
def __repr__(self) -> str:
|
|
65
|
+
img = self.image_content
|
|
66
|
+
if img and len(img) > _TRUNCATE_LENGTH:
|
|
67
|
+
img = img[: _TRUNCATE_LENGTH - 3] + b"..."
|
|
68
|
+
return f"MapContent(image_content={img!r}, rooms={self.rooms!r})"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MapContentTrait(MapContent, TraitUpdateListener):
|
|
72
|
+
"""Trait holding the most recently pushed parsed map content for Q10 devices.
|
|
73
|
+
|
|
74
|
+
The Q10 has no synchronous get-map request; the device pushes map and trace
|
|
75
|
+
packets, which the protocol layer decodes and the ``Q10PropertiesApi``
|
|
76
|
+
subscribe loop feeds into :meth:`update_from_map_packet` /
|
|
77
|
+
:meth:`update_from_trace_packet`. Consumers read the cached fields and/or
|
|
78
|
+
register a callback with :meth:`add_update_listener` to be notified when new
|
|
79
|
+
map content arrives.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
map_parser_config: B01Q10MapParserConfig | None = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
super().__init__()
|
|
88
|
+
TraitUpdateListener.__init__(self, logger=_LOGGER)
|
|
89
|
+
self._map_parser = B01Q10MapParser(map_parser_config)
|
|
90
|
+
|
|
91
|
+
def update_from_map_packet(self, packet: Q10MapPacket) -> None:
|
|
92
|
+
"""Render a pushed full-map packet into the cached image/rooms.
|
|
93
|
+
|
|
94
|
+
Rendering failures are logged and skipped (listeners are not notified) so
|
|
95
|
+
a single bad push cannot tear down the subscribe loop.
|
|
96
|
+
"""
|
|
97
|
+
parsed = self._map_parser.parse_packet(packet)
|
|
98
|
+
if parsed.image_content is None:
|
|
99
|
+
_LOGGER.debug("Failed to render Q10 map image")
|
|
100
|
+
return
|
|
101
|
+
self.image_content = parsed.image_content
|
|
102
|
+
self.map_data = parsed.map_data
|
|
103
|
+
self.rooms = packet.rooms
|
|
104
|
+
self._notify_update()
|
|
105
|
+
|
|
106
|
+
def update_from_trace_packet(self, packet: Q10TracePacket) -> None:
|
|
107
|
+
"""Cache the path/robot position from a pushed trace packet."""
|
|
108
|
+
self.path = packet.points
|
|
109
|
+
self.robot_position = packet.robot_position
|
|
110
|
+
self._notify_update()
|
{python_roborock-5.18.0 → python_roborock-5.20.0}/roborock/devices/traits/b01/q7/__init__.py
RENAMED
|
@@ -116,6 +116,29 @@ class Q7PropertiesApi(Trait):
|
|
|
116
116
|
"""Enable or disable the child lock."""
|
|
117
117
|
await self.set_prop(RoborockB01Props.CHILD_LOCK, int(enabled))
|
|
118
118
|
|
|
119
|
+
async def set_do_not_disturb(self, enabled: bool, begin_time: int, end_time: int) -> None:
|
|
120
|
+
"""Configure do-not-disturb.
|
|
121
|
+
|
|
122
|
+
The device expects all three values together via ``service.set_quiet_time``
|
|
123
|
+
(individual ``prop.set`` calls are ignored). ``begin_time``/``end_time`` are
|
|
124
|
+
minutes since midnight and must be in the range 0-1439 (inclusive).
|
|
125
|
+
|
|
126
|
+
Ranges that cross midnight are supported by passing a ``begin_time`` that is
|
|
127
|
+
greater than ``end_time`` (e.g. 22:00-07:00 is ``begin_time=1320``,
|
|
128
|
+
``end_time=420``).
|
|
129
|
+
"""
|
|
130
|
+
for name, value in (("begin_time", begin_time), ("end_time", end_time)):
|
|
131
|
+
if not 0 <= value <= 1439:
|
|
132
|
+
raise ValueError(f"{name} must be between 0 and 1439 minutes since midnight, got {value}")
|
|
133
|
+
await self.send(
|
|
134
|
+
RoborockB01Q7Methods.SET_QUIET_TIME,
|
|
135
|
+
{
|
|
136
|
+
"is_open": int(enabled),
|
|
137
|
+
"quiet_begin_time": begin_time,
|
|
138
|
+
"quiet_end_time": end_time,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
119
142
|
async def start_clean(self) -> None:
|
|
120
143
|
"""Start cleaning."""
|
|
121
144
|
await self.send(
|