python-roborock 5.17.0__tar.gz → 5.18.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.17.0 → python_roborock-5.18.0}/PKG-INFO +1 -1
- {python_roborock-5.17.0 → python_roborock-5.18.0}/pyproject.toml +1 -1
- python_roborock-5.18.0/roborock/map/b01_grid_layers.py +253 -0
- python_roborock-5.18.0/roborock/map/b01_q10_overlays.py +99 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/.gitignore +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/LICENSE +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/README.md +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/callbacks.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/cli.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/const.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/code_mappings.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/containers.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/device_features.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/README.md +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/cache.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/device.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/device_manager.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/file_cache.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/rpc/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/rpc/a01_channel.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/rpc/v1_channel.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/q10/command.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/q10/remote.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/q10/status.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/q7/map.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/q7/map_content.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/common.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/common.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/transport/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/transport/channel.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/transport/local_channel.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/transport/mqtt_channel.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/diagnostics.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/exceptions.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/map/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/map/b01_map_parser.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/map/map_parser.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/map/proto/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/map/proto/b01_scmap.proto +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/map/proto/b01_scmap_pb2.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/protocol.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/protocols/__init__.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/protocols/b01_q10_protocol.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/protocols/b01_q7_protocol.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/py.typed +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/roborock_message.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/util.py +0 -0
- {python_roborock-5.17.0 → python_roborock-5.18.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.18.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.18.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"
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Device-agnostic decomposition of a B01 occupancy grid into map layers.
|
|
2
|
+
|
|
3
|
+
Both the Q10 (custom binary) and Q7 (SCMap protobuf) deliver their map as a
|
|
4
|
+
single-byte-per-cell occupancy grid where the cell *value* encodes a semantic
|
|
5
|
+
class (background / wall / per-room floor / ...). This module turns such a grid
|
|
6
|
+
into separable **layers** a frontend can stack, without knowing the device's
|
|
7
|
+
specific value encoding -- the caller supplies a ``classifier`` mapping a cell
|
|
8
|
+
value to a class name, plus the room metadata.
|
|
9
|
+
|
|
10
|
+
Coordinates here are **grid-pixel** space (origin top-left of the raw grid, before
|
|
11
|
+
any rendering flip/scale). Vector overlays in world/robot coordinates (path,
|
|
12
|
+
zones, ...) are placed into this same space by the device's calibration.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import io
|
|
16
|
+
from collections.abc import Callable, Iterable
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from math import ceil
|
|
19
|
+
|
|
20
|
+
from PIL import Image
|
|
21
|
+
|
|
22
|
+
# Canonical layer class names. Devices map their raw cell values onto these.
|
|
23
|
+
LAYER_BACKGROUND = "background"
|
|
24
|
+
LAYER_WALL = "wall"
|
|
25
|
+
LAYER_FLOOR = "floor"
|
|
26
|
+
LAYER_UNKNOWN = "unknown"
|
|
27
|
+
|
|
28
|
+
_PNG = "PNG"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class RoomLayer:
|
|
33
|
+
"""A single room (segment) and where its pixels sit in the grid."""
|
|
34
|
+
|
|
35
|
+
id: int
|
|
36
|
+
name: str
|
|
37
|
+
pixel_value: int
|
|
38
|
+
pixel_count: int
|
|
39
|
+
bbox: tuple[int, int, int, int]
|
|
40
|
+
"""Inclusive ``(min_x, min_y, max_x, max_y)`` bounding box in grid pixels."""
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def center(self) -> tuple[float, float]:
|
|
44
|
+
"""Center of the bounding box in grid-pixel space (for label placement)."""
|
|
45
|
+
min_x, min_y, max_x, max_y = self.bbox
|
|
46
|
+
return ((min_x + max_x) / 2, (min_y + max_y) / 2)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class GridLayers:
|
|
51
|
+
"""Separable layers decomposed from a single occupancy grid.
|
|
52
|
+
|
|
53
|
+
Holds a reference to the raw ``grid`` plus the classifier, and renders any
|
|
54
|
+
layer to a transparent RGBA PNG on demand (so we don't eagerly build a mask
|
|
55
|
+
per room). ``class_counts`` reports how many cells fall in each class.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
width: int
|
|
59
|
+
height: int
|
|
60
|
+
grid: bytes
|
|
61
|
+
rooms: list[RoomLayer]
|
|
62
|
+
classifier: Callable[[int], str]
|
|
63
|
+
class_counts: dict[str, int] = field(default_factory=dict)
|
|
64
|
+
flip: bool = True
|
|
65
|
+
"""Whether display rendering flips the grid top-to-bottom. Devices whose grid
|
|
66
|
+
is stored bottom-up (V1/Q7 convention) flip; the Q10 grid is stored top-down,
|
|
67
|
+
so it does not. Used as the default for the ``render_*`` methods so every
|
|
68
|
+
layer matches the device's composited map orientation."""
|
|
69
|
+
|
|
70
|
+
def cell_class(self, value: int) -> str:
|
|
71
|
+
"""Classify a single raw cell value into a canonical layer name."""
|
|
72
|
+
return self.classifier(value)
|
|
73
|
+
|
|
74
|
+
def render_mask(
|
|
75
|
+
self,
|
|
76
|
+
predicate: Callable[[int], bool],
|
|
77
|
+
color: tuple[int, int, int, int],
|
|
78
|
+
*,
|
|
79
|
+
scale: int = 1,
|
|
80
|
+
flip: bool | None = None,
|
|
81
|
+
) -> bytes:
|
|
82
|
+
"""Render cells matching ``predicate`` as ``color`` over transparency.
|
|
83
|
+
|
|
84
|
+
``flip`` applies the same top-to-bottom flip the composited map uses so
|
|
85
|
+
layers line up pixel-for-pixel (defaults to the device's :attr:`flip`);
|
|
86
|
+
``scale`` nearest-neighbour upsamples.
|
|
87
|
+
"""
|
|
88
|
+
if flip is None:
|
|
89
|
+
flip = self.flip
|
|
90
|
+
transparent = (0, 0, 0, 0)
|
|
91
|
+
px = bytearray()
|
|
92
|
+
for value in self.grid:
|
|
93
|
+
px.extend(color if predicate(value) else transparent)
|
|
94
|
+
img = Image.frombytes("RGBA", (self.width, self.height), bytes(px))
|
|
95
|
+
if flip:
|
|
96
|
+
img = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
|
|
97
|
+
if scale > 1:
|
|
98
|
+
img = img.resize((self.width * scale, self.height * scale), Image.Resampling.NEAREST)
|
|
99
|
+
buf = io.BytesIO()
|
|
100
|
+
img.save(buf, format=_PNG)
|
|
101
|
+
return buf.getvalue()
|
|
102
|
+
|
|
103
|
+
def render_class(
|
|
104
|
+
self, layer: str, color: tuple[int, int, int, int], *, scale: int = 1, flip: bool | None = None
|
|
105
|
+
) -> bytes:
|
|
106
|
+
"""Render a whole class layer (e.g. ``"wall"``) to an RGBA PNG."""
|
|
107
|
+
return self.render_mask(lambda v: self.classifier(v) == layer, color, scale=scale, flip=flip)
|
|
108
|
+
|
|
109
|
+
def render_room(
|
|
110
|
+
self, room_id: int, color: tuple[int, int, int, int], *, scale: int = 1, flip: bool | None = None
|
|
111
|
+
) -> bytes:
|
|
112
|
+
"""Render a single room's pixels to an RGBA PNG."""
|
|
113
|
+
room = next((r for r in self.rooms if r.id == room_id), None)
|
|
114
|
+
if room is None:
|
|
115
|
+
raise KeyError(f"No room with id {room_id}")
|
|
116
|
+
target = room.pixel_value
|
|
117
|
+
return self.render_mask(lambda v: v == target, color, scale=scale, flip=flip)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class GridCalibration:
|
|
122
|
+
"""Affine transform between device world coordinates and grid pixels.
|
|
123
|
+
|
|
124
|
+
``resolution`` is world-units per pixel. The Y axis is flipped between world
|
|
125
|
+
and grid space via ``y_sign`` (devices typically have world-Y increasing
|
|
126
|
+
upward while the grid's Y increases downward).
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
resolution: float
|
|
130
|
+
origin_x: float
|
|
131
|
+
origin_y: float
|
|
132
|
+
y_sign: int = 1
|
|
133
|
+
|
|
134
|
+
def world_to_pixel(self, x: float, y: float) -> tuple[float, float]:
|
|
135
|
+
"""Map a world ``(x, y)`` to grid-pixel ``(px, py)``."""
|
|
136
|
+
return (x / self.resolution + self.origin_x, self.origin_y - self.y_sign * y / self.resolution)
|
|
137
|
+
|
|
138
|
+
def pixel_to_world(self, px: float, py: float) -> tuple[float, float]:
|
|
139
|
+
"""Map a grid-pixel ``(px, py)`` back to world ``(x, y)``."""
|
|
140
|
+
return ((px - self.origin_x) * self.resolution, self.y_sign * (self.origin_y - py) * self.resolution)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def solve_calibration(
|
|
144
|
+
layers: GridLayers,
|
|
145
|
+
points: list[tuple[float, float]],
|
|
146
|
+
*,
|
|
147
|
+
resolutions: Iterable[float],
|
|
148
|
+
y_signs: Iterable[int] = (1, -1),
|
|
149
|
+
) -> GridCalibration | None:
|
|
150
|
+
"""Fit a :class:`GridCalibration` by overlaying ``points`` onto the floor.
|
|
151
|
+
|
|
152
|
+
A cleaning path must lie on floor (not walls/background). For each candidate
|
|
153
|
+
resolution and Y orientation we slide the path's pixel bounding box across
|
|
154
|
+
the map and keep the placement that maximises on-floor points while
|
|
155
|
+
penalising points landing on walls/background. Returns ``None`` if no
|
|
156
|
+
placement lands a clear majority of points on floor.
|
|
157
|
+
|
|
158
|
+
The search is bounded: the path bbox size fixes how far it can slide, so the
|
|
159
|
+
offset range is small. Needs a reasonably dense, shape-rich path (a long
|
|
160
|
+
clean) to be well-constrained.
|
|
161
|
+
"""
|
|
162
|
+
if not points:
|
|
163
|
+
return None
|
|
164
|
+
w, h = layers.width, layers.height
|
|
165
|
+
classify = layers.classifier
|
|
166
|
+
# 1 = floor, 2 = wall/background (blocked), 0 = other. Index by cell.
|
|
167
|
+
klass = bytes(
|
|
168
|
+
1 if (c := classify(v)) == LAYER_FLOOR else 2 if c in (LAYER_WALL, LAYER_BACKGROUND) else 0 for v in layers.grid
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
best: tuple[float, GridCalibration] | None = None
|
|
172
|
+
for resolution in resolutions:
|
|
173
|
+
if resolution <= 0:
|
|
174
|
+
continue
|
|
175
|
+
for y_sign in y_signs:
|
|
176
|
+
sx = [x / resolution for x, _ in points]
|
|
177
|
+
sy = [y_sign * y / resolution for _, y in points]
|
|
178
|
+
min_sx, max_sx, min_sy, max_sy = min(sx), max(sx), min(sy), max(sy)
|
|
179
|
+
if (max_sx - min_sx) >= w or (max_sy - min_sy) >= h:
|
|
180
|
+
continue # path wider/taller than the map at this resolution
|
|
181
|
+
pts = list(zip(sx, sy))
|
|
182
|
+
# Slide so every point stays in-bounds: px = px_f + ox in [0, w), py = oy - py_f in [0, h).
|
|
183
|
+
for ox in range(ceil(-min_sx), ceil(w - max_sx)):
|
|
184
|
+
for oy in range(ceil(max_sy), ceil(h + min_sy)):
|
|
185
|
+
on_floor = 0
|
|
186
|
+
blocked = 0
|
|
187
|
+
for px_f, py_f in pts:
|
|
188
|
+
cell = int(oy - py_f) * w + int(px_f + ox)
|
|
189
|
+
k = klass[cell]
|
|
190
|
+
if k == 1:
|
|
191
|
+
on_floor += 1
|
|
192
|
+
elif k == 2:
|
|
193
|
+
blocked += 1
|
|
194
|
+
score = on_floor - 1.5 * blocked
|
|
195
|
+
if best is None or score > best[0]:
|
|
196
|
+
best = (score, GridCalibration(float(resolution), float(ox), float(oy), y_sign))
|
|
197
|
+
|
|
198
|
+
if best is None or best[0] < len(points) * 0.5:
|
|
199
|
+
return None
|
|
200
|
+
return best[1]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def decompose_grid(
|
|
204
|
+
width: int,
|
|
205
|
+
height: int,
|
|
206
|
+
grid: bytes,
|
|
207
|
+
rooms: Iterable[tuple[int, str, int, int]],
|
|
208
|
+
classifier: Callable[[int], str],
|
|
209
|
+
*,
|
|
210
|
+
flip: bool = True,
|
|
211
|
+
) -> GridLayers:
|
|
212
|
+
"""Build :class:`GridLayers` from a grid + room records + a classifier.
|
|
213
|
+
|
|
214
|
+
``rooms`` items are ``(id, name, pixel_value, pixel_count)`` tuples. Per-room
|
|
215
|
+
bounding boxes are computed in one pass over the grid. ``flip`` records the
|
|
216
|
+
device's display orientation (see :attr:`GridLayers.flip`).
|
|
217
|
+
"""
|
|
218
|
+
room_meta = list(rooms)
|
|
219
|
+
bboxes: dict[int, list[int]] = {pv: [width, height, -1, -1] for (_, _, pv, _) in room_meta}
|
|
220
|
+
counts: dict[str, int] = {}
|
|
221
|
+
for index, value in enumerate(grid):
|
|
222
|
+
cls = classifier(value)
|
|
223
|
+
counts[cls] = counts.get(cls, 0) + 1
|
|
224
|
+
box = bboxes.get(value)
|
|
225
|
+
if box is not None:
|
|
226
|
+
x = index % width
|
|
227
|
+
y = index // width
|
|
228
|
+
if x < box[0]:
|
|
229
|
+
box[0] = x
|
|
230
|
+
if y < box[1]:
|
|
231
|
+
box[1] = y
|
|
232
|
+
if x > box[2]:
|
|
233
|
+
box[2] = x
|
|
234
|
+
if y > box[3]:
|
|
235
|
+
box[3] = y
|
|
236
|
+
|
|
237
|
+
room_layers: list[RoomLayer] = []
|
|
238
|
+
for room_id, name, pixel_value, pixel_count in room_meta:
|
|
239
|
+
box = bboxes[pixel_value]
|
|
240
|
+
bbox = (box[0], box[1], box[2], box[3]) if box[2] >= 0 else (0, 0, 0, 0)
|
|
241
|
+
room_layers.append(
|
|
242
|
+
RoomLayer(id=room_id, name=name, pixel_value=pixel_value, pixel_count=pixel_count, bbox=bbox)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return GridLayers(
|
|
246
|
+
width=width,
|
|
247
|
+
height=height,
|
|
248
|
+
grid=grid,
|
|
249
|
+
rooms=room_layers,
|
|
250
|
+
classifier=classifier,
|
|
251
|
+
class_counts=counts,
|
|
252
|
+
flip=flip,
|
|
253
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Decoders for Q10 (B01/ss07) map vector overlays.
|
|
2
|
+
|
|
3
|
+
No-go zones, no-mop zones, virtual walls and zoned-clean areas are not part of
|
|
4
|
+
the map raster; the device reports them as base64-encoded blobs in separate data
|
|
5
|
+
points (``dpRestrictedZoneUp`` 55, ``dpVirtualWallUp`` 57, ``dpZonedUp`` 59).
|
|
6
|
+
|
|
7
|
+
The blob format was reverse-engineered from a live ss07 (confirmed against 7
|
|
8
|
+
real no-go zones):
|
|
9
|
+
|
|
10
|
+
[version: u8][count: u8] then ``count`` fixed-size records, each:
|
|
11
|
+
[type: u8][vertex_count: u8] then vertex_count (x, y) int16-BE pairs,
|
|
12
|
+
zero-padded to the record size.
|
|
13
|
+
|
|
14
|
+
Coordinates are in the device's world units (the same space as the cleaning
|
|
15
|
+
path), so a :class:`~roborock.map.b01_grid_layers.GridCalibration` maps them to
|
|
16
|
+
map pixels. ``type`` distinguishes the restriction kind (2 = no-mop, 3 = door
|
|
17
|
+
threshold, anything else -- incl. 0 -- a no-go zone); it is preserved verbatim
|
|
18
|
+
so callers can route polygons to the right ``MapData`` layer.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import base64
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
|
|
24
|
+
_DEFAULT_RECORD_SIZE = 38 # 2-byte record header + up to 9 (x, y) int16 pairs
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Q10Zone:
|
|
29
|
+
"""A polygon overlay (no-go / no-mop / virtual wall) in world coordinates."""
|
|
30
|
+
|
|
31
|
+
type: int
|
|
32
|
+
vertices: list[tuple[int, int]] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _as_bytes(data: bytes | str | None) -> bytes:
|
|
36
|
+
if data is None:
|
|
37
|
+
return b""
|
|
38
|
+
if isinstance(data, bytes):
|
|
39
|
+
return data
|
|
40
|
+
try:
|
|
41
|
+
return base64.b64decode(data + "=" * (-len(data) % 4))
|
|
42
|
+
except (ValueError, base64.binascii.Error): # type: ignore[attr-defined]
|
|
43
|
+
return b""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_zone_blob(data: bytes | str | None) -> list[Q10Zone]:
|
|
47
|
+
"""Decode a Q10 zone/wall overlay blob into a list of :class:`Q10Zone`.
|
|
48
|
+
|
|
49
|
+
Accepts the raw bytes or the base64 string straight from the data point.
|
|
50
|
+
Returns ``[]`` for empty/absent/unparsable blobs (the device sends a single
|
|
51
|
+
``0x00`` byte when there are none).
|
|
52
|
+
"""
|
|
53
|
+
raw = _as_bytes(data)
|
|
54
|
+
if len(raw) < 2:
|
|
55
|
+
return []
|
|
56
|
+
count = raw[1]
|
|
57
|
+
if count <= 0:
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
body = raw[2:]
|
|
61
|
+
record_size = len(body) // count if count and len(body) % count == 0 else _DEFAULT_RECORD_SIZE
|
|
62
|
+
if record_size < 2:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
zones: list[Q10Zone] = []
|
|
66
|
+
for index in range(count):
|
|
67
|
+
record = body[index * record_size : (index + 1) * record_size]
|
|
68
|
+
if len(record) < 2:
|
|
69
|
+
break
|
|
70
|
+
zone_type = record[0]
|
|
71
|
+
vertex_count = record[1]
|
|
72
|
+
needed = 2 + vertex_count * 4
|
|
73
|
+
if needed > len(record):
|
|
74
|
+
continue # malformed record; skip rather than misread padding
|
|
75
|
+
vertices = [
|
|
76
|
+
(
|
|
77
|
+
int.from_bytes(record[2 + j * 4 : 4 + j * 4], "big", signed=True),
|
|
78
|
+
int.from_bytes(record[4 + j * 4 : 6 + j * 4], "big", signed=True),
|
|
79
|
+
)
|
|
80
|
+
for j in range(vertex_count)
|
|
81
|
+
]
|
|
82
|
+
zones.append(Q10Zone(type=zone_type, vertices=vertices))
|
|
83
|
+
return zones
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# Observed ``type`` values, confirmed against an ss07 Q10 (firmware 03.11.24) and
|
|
87
|
+
# cross-checked with the ioBroker roborock adapter: 2 = no-mop, 3 = door
|
|
88
|
+
# threshold, 1 = virtual wall. Any other value (including 0) is a no-go zone.
|
|
89
|
+
# In practice virtual walls arrive on a separate DP (VIRTUAL_WALL_UP 57), so this
|
|
90
|
+
# restricted-zone DP normally only carries 0 / 2 / 3. The raw value is also kept
|
|
91
|
+
# on ``Q10Zone.type`` for callers that recognise it.
|
|
92
|
+
#
|
|
93
|
+
# Corrected from an earlier reading that treated type 3 as no-mop -- 3 is the
|
|
94
|
+
# door-threshold rectangle; the no-mop area reads back as type 2. Reported and
|
|
95
|
+
# verified by @andrewlyeats (ss07 read-backs + the ioBroker Q10 parser).
|
|
96
|
+
ZONE_TYPE_NO_GO = 0
|
|
97
|
+
ZONE_TYPE_VIRTUAL_WALL = 1
|
|
98
|
+
ZONE_TYPE_NO_MOP = 2
|
|
99
|
+
ZONE_TYPE_THRESHOLD = 3
|
|
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.17.0 → python_roborock-5.18.0}/roborock/data/b01_q10/b01_q10_code_mappings.py
RENAMED
|
File without changes
|
{python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/data/b01_q10/b01_q10_containers.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-5.17.0 → python_roborock-5.18.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.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/q10/__init__.py
RENAMED
|
File without changes
|
{python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/q10/command.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/q7/__init__.py
RENAMED
|
File without changes
|
{python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/b01/q7/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-5.17.0 → python_roborock-5.18.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.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/device_features.py
RENAMED
|
File without changes
|
{python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/dust_collection_mode.py
RENAMED
|
File without changes
|
{python_roborock-5.17.0 → python_roborock-5.18.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.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/network_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.17.0 → python_roborock-5.18.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.17.0 → python_roborock-5.18.0}/roborock/devices/traits/v1/wash_towel_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.17.0 → python_roborock-5.18.0}/roborock/devices/transport/local_channel.py
RENAMED
|
File without changes
|
{python_roborock-5.17.0 → python_roborock-5.18.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
|