python-roborock 7.13.0__tar.gz → 7.14.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-7.13.0 → python_roborock-7.14.0}/PKG-INFO +1 -1
- {python_roborock-7.13.0 → python_roborock-7.14.0}/pyproject.toml +1 -1
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/cli.py +30 -2
- python_roborock-7.14.0/roborock/map/dynamic_blocks.py +220 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/testing/v1_simulator.py +95 -1
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/web_api.py +25 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/.gitignore +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/LICENSE +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/README.md +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/callbacks.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/const.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/code_mappings.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/containers.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/mower/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/mower/mower_code_mappings.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/mower/mower_containers.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/device_features.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/README.md +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/cache.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/device.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/device_manager.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/file_cache.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/rpc/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/rpc/a01_channel.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/rpc/v1_channel.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/a01/device_feature.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/button_light.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/child_lock.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/clean_history.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/command.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/common.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/consumable.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/do_not_disturb.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/dust_collection.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/goto.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/map.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/maps.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/network_info.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/remote.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/status.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/volume.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q7/map.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q7/map_content.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/common.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/common.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/mop_dryer.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/obstacle_photos.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/transport/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/transport/channel.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/transport/local_channel.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/transport/mqtt_channel.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/diagnostics.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/exceptions.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/map/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/map/b01_grid_layers.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/map/b01_map_parser.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/map/b01_q10_map_parser.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/map/b01_q10_overlays.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/map/b01_q10_render.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/map/map_parser.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/map/proto/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/map/proto/b01_scmap.proto +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/map/proto/b01_scmap_pb2.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/map/room_colors.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/protocol.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/protocols/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/protocols/b01_q10_protocol.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/protocols/b01_q7_protocol.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/py.typed +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/roborock_message.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/testing/__init__.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/testing/a01_simulator.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/testing/b01_q10_simulator.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/testing/channel.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/testing/cloud.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/testing/simulator.py +0 -0
- {python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.14.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 = "7.
|
|
3
|
+
version = "7.14.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"
|
|
@@ -34,7 +34,7 @@ import threading
|
|
|
34
34
|
from collections.abc import Callable
|
|
35
35
|
from dataclasses import asdict, dataclass
|
|
36
36
|
from pathlib import Path
|
|
37
|
-
from typing import Any, cast
|
|
37
|
+
from typing import Any, TextIO, cast
|
|
38
38
|
|
|
39
39
|
try:
|
|
40
40
|
import click
|
|
@@ -50,7 +50,7 @@ except ImportError as err:
|
|
|
50
50
|
) from err
|
|
51
51
|
|
|
52
52
|
from roborock import RoborockCommand
|
|
53
|
-
from roborock.data import RoborockBase, UserData
|
|
53
|
+
from roborock.data import HomeDataSchedule, RoborockBase, UserData
|
|
54
54
|
from roborock.data.b01_q10.b01_q10_code_mappings import (
|
|
55
55
|
B01_Q10_DP,
|
|
56
56
|
YXCleanType,
|
|
@@ -428,6 +428,33 @@ async def execute_scene(ctx, scene_id):
|
|
|
428
428
|
await client.execute_scene(connection_cache.user_data, scene_id)
|
|
429
429
|
|
|
430
430
|
|
|
431
|
+
@click.command()
|
|
432
|
+
@click.option("--device_id", required=True)
|
|
433
|
+
@click.option(
|
|
434
|
+
"--job-file",
|
|
435
|
+
required=True,
|
|
436
|
+
type=click.File("r", encoding="utf-8"),
|
|
437
|
+
help="JSON file containing the schedule to update.",
|
|
438
|
+
)
|
|
439
|
+
@click.pass_context
|
|
440
|
+
@async_command
|
|
441
|
+
async def update_job(ctx: click.Context, device_id: str, job_file: TextIO) -> None:
|
|
442
|
+
"""Replace a cloud job using a complete schedule JSON file.
|
|
443
|
+
|
|
444
|
+
Include all existing settings in the file, including unchanged cleaning parameters.
|
|
445
|
+
"""
|
|
446
|
+
context: RoborockContext = ctx.obj
|
|
447
|
+
try:
|
|
448
|
+
job = HomeDataSchedule.from_dict(json.load(job_file))
|
|
449
|
+
connection_cache = context.connection_cache()
|
|
450
|
+
client = RoborockApiClient(connection_cache.email)
|
|
451
|
+
await client.update_job(connection_cache.user_data, device_id, job)
|
|
452
|
+
except Exception as err:
|
|
453
|
+
click.echo(f"Error: {err}", err=True)
|
|
454
|
+
raise SystemExit(1) from err
|
|
455
|
+
click.echo(f"Updated job {job.id} for device {device_id}")
|
|
456
|
+
|
|
457
|
+
|
|
431
458
|
async def _v1_trait(context: RoborockContext, device_id: str, display_func: Callable[[], V1TraitMixin]) -> Trait:
|
|
432
459
|
device_manager = await context.get_device_manager()
|
|
433
460
|
device = await device_manager.get_device(device_id)
|
|
@@ -1366,6 +1393,7 @@ cli.add_command(discover)
|
|
|
1366
1393
|
cli.add_command(list_devices)
|
|
1367
1394
|
cli.add_command(list_scenes)
|
|
1368
1395
|
cli.add_command(execute_scene)
|
|
1396
|
+
cli.add_command(update_job)
|
|
1369
1397
|
cli.add_command(status)
|
|
1370
1398
|
cli.add_command(command)
|
|
1371
1399
|
cli.add_command(parser)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Byte-level container helpers for Roborock V1 map blocks.
|
|
2
|
+
|
|
3
|
+
A V1 map file and the ``data`` / ``robot_data`` values returned by the dynamic
|
|
4
|
+
map diff commands share one block framing. Every block begins with an
|
|
5
|
+
eight-byte common header::
|
|
6
|
+
|
|
7
|
+
offset 0, u16 block type
|
|
8
|
+
offset 2, u16 header length H (including these eight bytes)
|
|
9
|
+
offset 4, u32 payload length P
|
|
10
|
+
offset 8 ..H type-specific header fields
|
|
11
|
+
offset H ..H+P payload
|
|
12
|
+
|
|
13
|
+
The next block starts exactly ``H + P`` bytes after the current one. All
|
|
14
|
+
multibyte fields are little-endian and unsigned.
|
|
15
|
+
|
|
16
|
+
This module only understands that framing. It can split a map file or a
|
|
17
|
+
dynamic value into blocks, rebuild a map file from blocks, and decode the
|
|
18
|
+
block 34 nonce table that identifies the base map and its dynamic elements.
|
|
19
|
+
It deliberately knows nothing about the meaning of any block payload; that
|
|
20
|
+
stays with the map parser (full maps) and the dynamic element merge logic.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import struct
|
|
24
|
+
from collections.abc import Iterable, Iterator
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Self
|
|
27
|
+
|
|
28
|
+
from roborock.exceptions import RoborockException
|
|
29
|
+
|
|
30
|
+
# Marker at the start of a full V1 map file.
|
|
31
|
+
MAP_FILE_MAGIC = b"rr"
|
|
32
|
+
|
|
33
|
+
# Size of the block header fields shared by every block type.
|
|
34
|
+
COMMON_HEADER_LEN = 8
|
|
35
|
+
|
|
36
|
+
# A dynamic ``data`` / ``robot_data`` value holds at most this many blocks.
|
|
37
|
+
MAX_DYNAMIC_VALUE_BLOCKS = 4
|
|
38
|
+
|
|
39
|
+
# Block type of the nonce table carried inside a full map.
|
|
40
|
+
NONCE_TABLE_BLOCK_TYPE = 34
|
|
41
|
+
|
|
42
|
+
# Entry identifier inside the nonce table that supplies the map nonce.
|
|
43
|
+
MAP_NONCE_ENTRY_ID = 35
|
|
44
|
+
|
|
45
|
+
# Trailing digest block of a full map. New blocks are inserted before it.
|
|
46
|
+
DIGEST_BLOCK_TYPE = 1024
|
|
47
|
+
|
|
48
|
+
_COMMON_HEADER = struct.Struct("<HHI")
|
|
49
|
+
_NONCE_TABLE_NUM = struct.Struct("<I")
|
|
50
|
+
_NONCE_ENTRY = struct.Struct("<BI")
|
|
51
|
+
_NONCE_TABLE_MIN_HEADER_LEN = COMMON_HEADER_LEN + _NONCE_TABLE_NUM.size
|
|
52
|
+
_MAP_FILE_HEADER_LEN = struct.Struct("<H")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class MapBlock:
|
|
57
|
+
"""One framed block: its type, type-specific header fields, and payload.
|
|
58
|
+
|
|
59
|
+
``extra_header`` holds the bytes between the common eight-byte header and
|
|
60
|
+
the payload, so the common header is always regenerated from the lengths
|
|
61
|
+
and can never disagree with the data it describes.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
block_type: int
|
|
65
|
+
payload: bytes
|
|
66
|
+
extra_header: bytes = b""
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def header_length(self) -> int:
|
|
70
|
+
"""Total header length ``H`` written to the common header."""
|
|
71
|
+
return COMMON_HEADER_LEN + len(self.extra_header)
|
|
72
|
+
|
|
73
|
+
def with_payload(self, payload: bytes) -> Self:
|
|
74
|
+
"""Return a copy carrying ``payload`` and the same type and header fields."""
|
|
75
|
+
return type(self)(self.block_type, payload, self.extra_header)
|
|
76
|
+
|
|
77
|
+
def encode(self) -> bytes:
|
|
78
|
+
"""Serialize the block with a regenerated common header."""
|
|
79
|
+
common = _COMMON_HEADER.pack(self.block_type, self.header_length, len(self.payload))
|
|
80
|
+
return common + self.extra_header + self.payload
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def iter_blocks(data: bytes, start: int = 0) -> Iterator[MapBlock]:
|
|
84
|
+
"""Yield consecutive blocks from ``data`` beginning at byte ``start``.
|
|
85
|
+
|
|
86
|
+
The sequence must consume ``data`` exactly. A header shorter than eight
|
|
87
|
+
bytes, a header or payload that runs past the end of ``data``, or a block
|
|
88
|
+
that does not fit a complete common header is rejected.
|
|
89
|
+
"""
|
|
90
|
+
position = start
|
|
91
|
+
end = len(data)
|
|
92
|
+
while position < end:
|
|
93
|
+
if end - position < COMMON_HEADER_LEN:
|
|
94
|
+
raise RoborockException(f"Map block at offset {position} is shorter than its common header")
|
|
95
|
+
block_type, header_length, payload_length = _COMMON_HEADER.unpack_from(data, position)
|
|
96
|
+
if header_length < COMMON_HEADER_LEN:
|
|
97
|
+
raise RoborockException(f"Map block at offset {position} declares header length {header_length} < 8")
|
|
98
|
+
block_end = position + header_length + payload_length
|
|
99
|
+
if block_end > end:
|
|
100
|
+
raise RoborockException(f"Map block at offset {position} extends past the end of the data")
|
|
101
|
+
header_end = position + header_length
|
|
102
|
+
yield MapBlock(
|
|
103
|
+
block_type=block_type,
|
|
104
|
+
payload=data[header_end:block_end],
|
|
105
|
+
extra_header=data[position + COMMON_HEADER_LEN : header_end],
|
|
106
|
+
)
|
|
107
|
+
position = block_end
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def parse_dynamic_value(data: bytes) -> list[MapBlock]:
|
|
111
|
+
"""Split a decoded dynamic ``data`` / ``robot_data`` value into blocks.
|
|
112
|
+
|
|
113
|
+
A dynamic value is a bare block sequence: there is no ``rr`` file header
|
|
114
|
+
and no digest. An empty value yields no blocks. A value that reaches a
|
|
115
|
+
fifth block is rejected, so a non-empty value holds one to four blocks.
|
|
116
|
+
"""
|
|
117
|
+
blocks: list[MapBlock] = []
|
|
118
|
+
for block in iter_blocks(data):
|
|
119
|
+
if len(blocks) == MAX_DYNAMIC_VALUE_BLOCKS:
|
|
120
|
+
raise RoborockException(f"Dynamic map value holds more than {MAX_DYNAMIC_VALUE_BLOCKS} blocks")
|
|
121
|
+
blocks.append(block)
|
|
122
|
+
return blocks
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def encode_blocks(blocks: Iterable[MapBlock]) -> bytes:
|
|
126
|
+
"""Serialize a block sequence without any file header."""
|
|
127
|
+
return b"".join(block.encode() for block in blocks)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class MapFile:
|
|
132
|
+
"""A full V1 map file split into its file header and framed blocks."""
|
|
133
|
+
|
|
134
|
+
header: bytes
|
|
135
|
+
"""The ``rr`` file header, kept verbatim so it round-trips unchanged."""
|
|
136
|
+
|
|
137
|
+
blocks: list[MapBlock]
|
|
138
|
+
|
|
139
|
+
def encode(self) -> bytes:
|
|
140
|
+
"""Serialize the file header followed by every block."""
|
|
141
|
+
return self.header + encode_blocks(self.blocks)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def split_map_file(map_bytes: bytes) -> MapFile:
|
|
145
|
+
"""Split a decompressed full map into its file header and blocks."""
|
|
146
|
+
if not map_bytes.startswith(MAP_FILE_MAGIC):
|
|
147
|
+
raise RoborockException("Map file does not start with the 'rr' marker")
|
|
148
|
+
if len(map_bytes) < _MAP_FILE_HEADER_LEN.size + 2:
|
|
149
|
+
raise RoborockException("Map file is shorter than its header length field")
|
|
150
|
+
(header_length,) = _MAP_FILE_HEADER_LEN.unpack_from(map_bytes, 2)
|
|
151
|
+
if header_length < 4 or header_length > len(map_bytes):
|
|
152
|
+
raise RoborockException(f"Map file declares an invalid header length {header_length}")
|
|
153
|
+
return MapFile(header=map_bytes[:header_length], blocks=list(iter_blocks(map_bytes, header_length)))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def find_block(blocks: Iterable[MapBlock], block_type: int) -> MapBlock | None:
|
|
157
|
+
"""Return the first block of ``block_type``, or ``None`` when absent."""
|
|
158
|
+
for block in blocks:
|
|
159
|
+
if block.block_type == block_type:
|
|
160
|
+
return block
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def replace_block(blocks: list[MapBlock], new_block: MapBlock) -> list[MapBlock]:
|
|
165
|
+
"""Return a new block list with ``new_block`` substituted for its type.
|
|
166
|
+
|
|
167
|
+
The first existing block of the same type is replaced in place, so the
|
|
168
|
+
relative order the map parser depends on (for example mop flags after
|
|
169
|
+
the path they describe) is preserved. When the type is not present the
|
|
170
|
+
block is inserted before the trailing digest, or appended when there is
|
|
171
|
+
no digest.
|
|
172
|
+
"""
|
|
173
|
+
for index, block in enumerate(blocks):
|
|
174
|
+
if block.block_type == new_block.block_type:
|
|
175
|
+
return [*blocks[:index], new_block, *blocks[index + 1 :]]
|
|
176
|
+
for index, block in enumerate(blocks):
|
|
177
|
+
if block.block_type == DIGEST_BLOCK_TYPE:
|
|
178
|
+
return [*blocks[:index], new_block, *blocks[index:]]
|
|
179
|
+
return [*blocks, new_block]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True)
|
|
183
|
+
class NonceTable:
|
|
184
|
+
"""Decoded block 34: identities of the base map and its dynamic elements.
|
|
185
|
+
|
|
186
|
+
Nonces are opaque 32-bit identities. Only equality is meaningful. They are
|
|
187
|
+
decoded unsigned to preserve all 32 bits. JSON values may use a signed
|
|
188
|
+
representation, as the app's binary reader does. Callers must normalize
|
|
189
|
+
JSON nonces with ``value & 0xFFFFFFFF`` before comparing them to this table.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
map_nonce: int | None
|
|
193
|
+
"""Nonce of the base map (entry 35), or ``None`` when the table lacks it."""
|
|
194
|
+
|
|
195
|
+
element_nonces: dict[int, int]
|
|
196
|
+
"""Element nonce keyed by element id, excluding the map nonce entry."""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def decode_nonce_table(block: MapBlock) -> NonceTable:
|
|
200
|
+
"""Decode a block 34 nonce table.
|
|
201
|
+
|
|
202
|
+
The header carries ``num`` as a u32 at offset 8. The payload holds ``num``
|
|
203
|
+
five-byte entries, each a one-byte identifier followed by a u32 nonce.
|
|
204
|
+
"""
|
|
205
|
+
if block.block_type != NONCE_TABLE_BLOCK_TYPE:
|
|
206
|
+
raise RoborockException(f"Block type {block.block_type} is not a nonce table")
|
|
207
|
+
if block.header_length < _NONCE_TABLE_MIN_HEADER_LEN:
|
|
208
|
+
raise RoborockException("Nonce table header is shorter than 12 bytes")
|
|
209
|
+
(num,) = _NONCE_TABLE_NUM.unpack_from(block.extra_header, 0)
|
|
210
|
+
if len(block.payload) < num * _NONCE_ENTRY.size:
|
|
211
|
+
raise RoborockException(f"Nonce table declares {num} entries but the payload is too short")
|
|
212
|
+
map_nonce: int | None = None
|
|
213
|
+
element_nonces: dict[int, int] = {}
|
|
214
|
+
for index in range(num):
|
|
215
|
+
entry_id, nonce = _NONCE_ENTRY.unpack_from(block.payload, index * _NONCE_ENTRY.size)
|
|
216
|
+
if entry_id == MAP_NONCE_ENTRY_ID:
|
|
217
|
+
map_nonce = nonce
|
|
218
|
+
else:
|
|
219
|
+
element_nonces[entry_id] = nonce
|
|
220
|
+
return NonceTable(map_nonce=map_nonce, element_nonces=element_nonces)
|
|
@@ -4,8 +4,11 @@ This module provides `V1VacuumSimulator` which simulates the firmware state
|
|
|
4
4
|
machine and JSON RPC commands for V1 vacuum cleaners.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import gzip
|
|
8
|
+
import hashlib
|
|
7
9
|
import json
|
|
8
10
|
import logging
|
|
11
|
+
import struct
|
|
9
12
|
import time
|
|
10
13
|
from collections.abc import Callable
|
|
11
14
|
from dataclasses import asdict, replace
|
|
@@ -37,6 +40,7 @@ from roborock.data.v1.v1_containers import (
|
|
|
37
40
|
)
|
|
38
41
|
from roborock.devices.cache import DeviceCache, InMemoryCache
|
|
39
42
|
from roborock.devices.rpc.v1_channel import V1Channel
|
|
43
|
+
from roborock.protocol import Utils
|
|
40
44
|
from roborock.protocols.v1_protocol import SecurityData
|
|
41
45
|
from roborock.roborock_message import RoborockDataProtocol, RoborockMessage, RoborockMessageProtocol
|
|
42
46
|
from roborock.testing.channel import FakeChannel
|
|
@@ -165,6 +169,33 @@ DEFAULT_LAST_CLEAN_RECORD = CleanRecord(
|
|
|
165
169
|
)
|
|
166
170
|
|
|
167
171
|
|
|
172
|
+
# Eight character endpoint, matching the width of the map response header field.
|
|
173
|
+
MAP_ENDPOINT = "fakeendp"
|
|
174
|
+
|
|
175
|
+
# Minimum side length in cells of the simulated map image.
|
|
176
|
+
MAP_IMAGE_SIZE = 32
|
|
177
|
+
|
|
178
|
+
_MAP_FILE_HEADER_LEN = 20
|
|
179
|
+
_MAP_CELL_SIZE_MM = 50
|
|
180
|
+
_MAP_IMAGE_PADDING = 2
|
|
181
|
+
_IMAGE_BLOCK_TYPE = 2
|
|
182
|
+
_PATH_BLOCK_TYPE = 3
|
|
183
|
+
_POSE_BLOCK_TYPE = 8
|
|
184
|
+
_MOP_BLOCK_TYPE = 18
|
|
185
|
+
_DIGEST_BLOCK_TYPE = 1024
|
|
186
|
+
_MAP_INSIDE = 0xFF
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _path_point(index: int) -> bytes:
|
|
190
|
+
"""A deterministic path point, so tests can assert on exact bytes."""
|
|
191
|
+
return struct.pack("<HH", 20000 + index * 10, 21000 + index * 7)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _block(block_type: int, payload: bytes, extra_header: bytes = b"") -> bytes:
|
|
195
|
+
"""Frame one map block: type, header length, payload length, header, payload."""
|
|
196
|
+
return struct.pack("<HHI", block_type, 8 + len(extra_header), len(payload)) + extra_header + payload
|
|
197
|
+
|
|
198
|
+
|
|
168
199
|
class V1VacuumSimulator(RoborockDeviceSimulator):
|
|
169
200
|
"""Firmware simulator for a V1/L01 vacuum device.
|
|
170
201
|
|
|
@@ -219,10 +250,18 @@ class V1VacuumSimulator(RoborockDeviceSimulator):
|
|
|
219
250
|
"reset_consumable": self._handle_reset_consumable,
|
|
220
251
|
"app_get_init_status": self._handle_app_get_init_status,
|
|
221
252
|
"get_network_info": self._handle_get_network_info,
|
|
253
|
+
"get_map_v1": lambda params: self.build_map_bytes(),
|
|
222
254
|
}
|
|
223
255
|
|
|
256
|
+
# The simulated robot's own view of its map. Tests extend the path
|
|
257
|
+
# with advance_path() and move the robot via robot_position.
|
|
258
|
+
self.path_length = 0
|
|
259
|
+
self.robot_position = (25000, 24000, 90)
|
|
260
|
+
|
|
224
261
|
self.device_cache = DeviceCache(self.duid, InMemoryCache())
|
|
225
|
-
|
|
262
|
+
# The endpoint must fit the eight byte map response header field and the
|
|
263
|
+
# nonce must be a valid sixteen byte AES key for map responses to decode.
|
|
264
|
+
self.security_data = SecurityData(endpoint=MAP_ENDPOINT, nonce=b"fake_nonce_16byt")
|
|
226
265
|
local_session = Mock(return_value=self.local_channel)
|
|
227
266
|
|
|
228
267
|
self._v1_channel = V1Channel(
|
|
@@ -333,6 +372,56 @@ class V1VacuumSimulator(RoborockDeviceSimulator):
|
|
|
333
372
|
def _handle_get_network_info(self, params: Any) -> dict[str, Any]:
|
|
334
373
|
return _serialize_dataclass(self.network_info)
|
|
335
374
|
|
|
375
|
+
# --- Simulated map -------------------------------------------------
|
|
376
|
+
|
|
377
|
+
def advance_path(self, count: int) -> None:
|
|
378
|
+
"""Extend the current path, as the robot does while it cleans."""
|
|
379
|
+
self.path_length += count
|
|
380
|
+
|
|
381
|
+
def path_payload(self, start: int, count: int) -> bytes:
|
|
382
|
+
"""The point records for a range of the current path."""
|
|
383
|
+
return b"".join(_path_point(i) for i in range(start, start + count))
|
|
384
|
+
|
|
385
|
+
def mop_payload(self, start: int, count: int) -> bytes:
|
|
386
|
+
"""The mop flags for a range of the current path, alternating per point."""
|
|
387
|
+
return bytes(i % 2 for i in range(start, start + count))
|
|
388
|
+
|
|
389
|
+
def _pose_block(self) -> bytes:
|
|
390
|
+
return _block(_POSE_BLOCK_TYPE, struct.pack("<iii", *self.robot_position))
|
|
391
|
+
|
|
392
|
+
def _path_blocks(self, start: int, count: int) -> bytes:
|
|
393
|
+
"""Blocks 3 and 18 for a range of the path, as both a map and a page carry them."""
|
|
394
|
+
return _block(_PATH_BLOCK_TYPE, self.path_payload(start, count), struct.pack("<III", count, 4, 0)) + _block(
|
|
395
|
+
_MOP_BLOCK_TYPE, self.mop_payload(start, count)
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def build_map_bytes(self) -> bytes:
|
|
399
|
+
"""Build a decompressed map file reflecting the current robot state."""
|
|
400
|
+
# Full-map image offsets are cells; path and robot coordinates are millimetres.
|
|
401
|
+
points = [self.robot_position[:2], *struct.iter_unpack("<HH", self.path_payload(0, self.path_length))]
|
|
402
|
+
xs = [x // _MAP_CELL_SIZE_MM for x, _ in points]
|
|
403
|
+
ys = [y // _MAP_CELL_SIZE_MM for _, y in points]
|
|
404
|
+
left = max(0, min(xs) - _MAP_IMAGE_PADDING)
|
|
405
|
+
top = max(0, min(ys) - _MAP_IMAGE_PADDING)
|
|
406
|
+
width = max(MAP_IMAGE_SIZE, max(xs) - left + _MAP_IMAGE_PADDING + 1)
|
|
407
|
+
height = max(MAP_IMAGE_SIZE, max(ys) - top + _MAP_IMAGE_PADDING + 1)
|
|
408
|
+
image = _block(
|
|
409
|
+
_IMAGE_BLOCK_TYPE,
|
|
410
|
+
bytes([_MAP_INSIDE]) * (width * height),
|
|
411
|
+
struct.pack("<Iiiii", 0, top, left, height, width),
|
|
412
|
+
)
|
|
413
|
+
body = image + self._path_blocks(0, self.path_length) + self._pose_block()
|
|
414
|
+
digest_header = struct.pack("<HHI", _DIGEST_BLOCK_TYPE, 8, 20)
|
|
415
|
+
header = struct.pack("<2sHIHHII", b"rr", _MAP_FILE_HEADER_LEN, len(body) + len(digest_header) + 20, 1, 0, 0, 0)
|
|
416
|
+
data = header + body + digest_header
|
|
417
|
+
return data + hashlib.sha1(data, usedforsecurity=False).digest()
|
|
418
|
+
|
|
419
|
+
def _map_response_message(self, request_id: int, map_bytes: bytes) -> RoborockMessage:
|
|
420
|
+
"""Frame the map as the encrypted binary response the device sends."""
|
|
421
|
+
body = Utils.encrypt_cbc(gzip.compress(map_bytes), self.security_data.nonce)
|
|
422
|
+
header = struct.pack("<8s8sH6s", self.security_data.endpoint.encode(), bytes(8), request_id & 0xFFFF, bytes(6))
|
|
423
|
+
return RoborockMessage(protocol=RoborockMessageProtocol.MAP_RESPONSE, payload=header + body)
|
|
424
|
+
|
|
336
425
|
async def _handle_publish(self, message: RoborockMessage, channel: FakeChannel) -> None:
|
|
337
426
|
if not message.payload:
|
|
338
427
|
return
|
|
@@ -364,6 +453,11 @@ class V1VacuumSimulator(RoborockDeviceSimulator):
|
|
|
364
453
|
else:
|
|
365
454
|
result = "ok"
|
|
366
455
|
|
|
456
|
+
if method == "get_map_v1" and error is None and isinstance(result, bytes):
|
|
457
|
+
# Custom handlers return decompressed map bytes, just like the default.
|
|
458
|
+
channel.notify_subscribers(self._map_response_message(msg_id, result))
|
|
459
|
+
return
|
|
460
|
+
|
|
367
461
|
response_data = {
|
|
368
462
|
"dps": {"102": json.dumps({"id": msg_id, "result": result, "error": error})},
|
|
369
463
|
"t": int(time.time()),
|
|
@@ -768,6 +768,31 @@ class RoborockApiClient:
|
|
|
768
768
|
raise RoborockException(response)
|
|
769
769
|
return response
|
|
770
770
|
|
|
771
|
+
async def update_job(self, user_data: UserData, device_id: str, job: HomeDataSchedule) -> None:
|
|
772
|
+
"""Replace an existing cloud job with the full supplied schedule.
|
|
773
|
+
|
|
774
|
+
Pass a schedule returned by :meth:`get_schedules` with the desired changes.
|
|
775
|
+
The job ID identifies the URL; all other fields are sent as the replacement body.
|
|
776
|
+
"""
|
|
777
|
+
rriot = user_data.rriot
|
|
778
|
+
if rriot is None:
|
|
779
|
+
raise RoborockException("rriot is none")
|
|
780
|
+
if rriot.r.a is None:
|
|
781
|
+
raise RoborockException("Missing field 'a' in rriot reference")
|
|
782
|
+
path = f"/user/devices/{device_id}/jobs/{job.id}"
|
|
783
|
+
body = job.as_dict(exclude={"id"})
|
|
784
|
+
job_request = PreparedRequest(
|
|
785
|
+
rriot.r.a,
|
|
786
|
+
self.session,
|
|
787
|
+
{
|
|
788
|
+
"Authorization": _get_hawk_authentication(rriot, path, body=body),
|
|
789
|
+
"Content-Type": "application/json",
|
|
790
|
+
},
|
|
791
|
+
)
|
|
792
|
+
response = await job_request.request("put", path, data=_compact_json(body).encode())
|
|
793
|
+
if not response or not response.get("success"):
|
|
794
|
+
raise RoborockException(response)
|
|
795
|
+
|
|
771
796
|
async def get_products(self, user_data: UserData) -> ProductResponse:
|
|
772
797
|
"""Gets all products and their schemas, good for determining status codes and model numbers."""
|
|
773
798
|
base_url = await self.base_url
|
|
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-7.13.0 → python_roborock-7.14.0}/roborock/data/b01_q10/b01_q10_code_mappings.py
RENAMED
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/b01_q10/b01_q10_containers.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.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
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/data/mower/mower_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
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/a01/device_feature.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/__init__.py
RENAMED
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/button_light.py
RENAMED
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/child_lock.py
RENAMED
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/clean_history.py
RENAMED
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/command.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/consumable.py
RENAMED
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/dust_collection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q10/network_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q7/__init__.py
RENAMED
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/b01/q7/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.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-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/device_features.py
RENAMED
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/dust_collection_mode.py
RENAMED
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.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
|
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/network_info.py
RENAMED
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/obstacle_photos.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/smart_wash_params.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/traits/v1/wash_towel_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.0}/roborock/devices/transport/local_channel.py
RENAMED
|
File without changes
|
{python_roborock-7.13.0 → python_roborock-7.14.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|