Skip to content

Commit 741fca6

Browse files
committed
refactor(q7): address maintainer review follow-ups
1 parent 13170e2 commit 741fca6

File tree

7 files changed

+104
-44
lines changed

7 files changed

+104
-44
lines changed

.pre-commit-config.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ repos:
3838
rev: v0.13.2
3939
hooks:
4040
- id: ruff-format
41-
exclude: ^roborock/map/proto/.*_pb2\.py$
41+
args:
42+
- --force-exclude
4243
- id: ruff
43-
exclude: ^roborock/map/proto/.*_pb2\.py$
4444
args:
45+
- --force-exclude
4546
- --fix
4647
- repo: https://github.com/pre-commit/mirrors-mypy
4748
rev: v1.7.1

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ major_tags= ["refactor"]
9898
lint.ignore = ["F403", "E741"]
9999
lint.select=["E", "F", "UP", "I"]
100100
line-length = 120
101+
extend-exclude = ["roborock/map/proto/*_pb2.py"]
101102

102103
[tool.ruff.lint.per-file-ignores]
103104
"*/__init__.py" = ["F401"]

roborock/devices/traits/b01/q7/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ def __init__(self, channel: MqttChannel, *, device: HomeDataDevice, product: Hom
5757
self._device = device
5858
self._product = product
5959

60+
if not device.sn or not product.model:
61+
raise ValueError("B01 Q7 map content requires device serial number and product model metadata")
62+
6063
self.clean_summary = CleanSummaryTrait(channel)
6164
self.map = MapTrait(channel)
6265
self.map_content = MapContentTrait(

roborock/devices/traits/b01/q7/map_content.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ def __init__(
5555
self,
5656
map_trait: MapTrait,
5757
*,
58-
serial: str | None,
59-
model: str | None,
58+
serial: str,
59+
model: str,
6060
map_parser_config: B01MapParserConfig | None = None,
6161
) -> None:
6262
super().__init__()
@@ -79,11 +79,6 @@ def parse_map_content(self, response: bytes) -> MapContent:
7979
This mirrors the v1 trait behavior so cached map payload bytes can be
8080
reparsed without going back to the device.
8181
"""
82-
if not self._serial or not self._model:
83-
raise RoborockException(
84-
"B01 map parsing requires device serial number and model metadata, but they were missing"
85-
)
86-
8782
try:
8883
parsed_data = self._map_parser.parse(
8984
response,

roborock/map/b01_map_parser.py

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
import io
1919
import zlib
2020
from dataclasses import dataclass, field
21-
from typing import Any
21+
from typing import Protocol, cast
2222

2323
from Crypto.Cipher import AES
2424
from Crypto.Util.Padding import pad, unpad
25-
from google.protobuf.message import DecodeError
25+
from google.protobuf.message import DecodeError, Message
2626
from PIL import Image
2727
from vacuum_map_parser_base.config.image_config import ImageConfig
2828
from vacuum_map_parser_base.map_data import ImageData, MapData
@@ -36,6 +36,72 @@
3636
_MAP_FILE_FORMAT = "PNG"
3737

3838

39+
class _ProtoMessage(Protocol):
40+
def HasField(self, field_name: str) -> bool: ...
41+
42+
43+
class _ScPointMessage(_ProtoMessage, Protocol):
44+
x: float
45+
y: float
46+
47+
48+
class _ScMapBoundaryInfoMessage(_ProtoMessage, Protocol):
49+
mapMd5: str
50+
vMinX: int
51+
vMaxX: int
52+
vMinY: int
53+
vMaxY: int
54+
55+
56+
class _ScMapExtInfoMessage(_ProtoMessage, Protocol):
57+
taskBeginDate: int
58+
mapUploadDate: int
59+
mapValid: int
60+
radian: int
61+
force: int
62+
cleanPath: int
63+
boudaryInfo: _ScMapBoundaryInfoMessage
64+
mapVersion: int
65+
mapValueType: int
66+
67+
68+
class _ScMapHeadMessage(_ProtoMessage, Protocol):
69+
mapHeadId: int
70+
sizeX: int
71+
sizeY: int
72+
minX: float
73+
minY: float
74+
maxX: float
75+
maxY: float
76+
resolution: float
77+
78+
79+
class _ScRoomDataMessage(_ProtoMessage, Protocol):
80+
roomId: int
81+
roomName: str
82+
roomTypeId: int
83+
meterialId: int
84+
cleanState: int
85+
roomClean: int
86+
roomCleanIndex: int
87+
roomNamePost: _ScPointMessage
88+
colorId: int
89+
floor_direction: int
90+
global_seq: int
91+
92+
93+
class _ScMapDataContainerMessage(_ProtoMessage, Protocol):
94+
mapData: bytes
95+
96+
97+
class _ScMapMessage(_ProtoMessage, Protocol):
98+
mapType: int
99+
mapExtInfo: _ScMapExtInfoMessage
100+
mapHead: _ScMapHeadMessage
101+
mapData: _ScMapDataContainerMessage
102+
roomDataInfo: list[_ScRoomDataMessage]
103+
104+
39105
@dataclass(frozen=True)
40106
class _ScPoint:
41107
x: float | None = None
@@ -185,7 +251,7 @@ def _decode_b01_map_payload(raw_payload: bytes, *, serial: str, model: str) -> b
185251
raise RoborockException("Failed to decode B01 map payload") from err
186252

187253

188-
def _parse_proto(blob: bytes, message: Any, *, context: str) -> None:
254+
def _parse_proto(blob: bytes, message: Message, *, context: str) -> None:
189255
try:
190256
message.ParseFromString(blob)
191257
except DecodeError as err:
@@ -199,14 +265,14 @@ def _decode_map_data_bytes(value: bytes) -> bytes:
199265
return value
200266

201267

202-
def _parse_sc_point(parsed: Any) -> _ScPoint:
268+
def _parse_sc_point(parsed: _ScPointMessage) -> _ScPoint:
203269
return _ScPoint(
204270
x=parsed.x if parsed.HasField("x") else None,
205271
y=parsed.y if parsed.HasField("y") else None,
206272
)
207273

208274

209-
def _parse_sc_map_boundary_info(parsed: Any) -> _ScMapBoundaryInfo:
275+
def _parse_sc_map_boundary_info(parsed: _ScMapBoundaryInfoMessage) -> _ScMapBoundaryInfo:
210276
return _ScMapBoundaryInfo(
211277
map_md5=parsed.mapMd5 if parsed.HasField("mapMd5") else None,
212278
v_min_x=parsed.vMinX if parsed.HasField("vMinX") else None,
@@ -216,7 +282,7 @@ def _parse_sc_map_boundary_info(parsed: Any) -> _ScMapBoundaryInfo:
216282
)
217283

218284

219-
def _parse_sc_map_ext_info(parsed: Any) -> _ScMapExtInfo:
285+
def _parse_sc_map_ext_info(parsed: _ScMapExtInfoMessage) -> _ScMapExtInfo:
220286
return _ScMapExtInfo(
221287
task_begin_date=parsed.taskBeginDate if parsed.HasField("taskBeginDate") else None,
222288
map_upload_date=parsed.mapUploadDate if parsed.HasField("mapUploadDate") else None,
@@ -230,7 +296,7 @@ def _parse_sc_map_ext_info(parsed: Any) -> _ScMapExtInfo:
230296
)
231297

232298

233-
def _parse_sc_map_head(parsed: Any) -> _ScMapHead:
299+
def _parse_sc_map_head(parsed: _ScMapHeadMessage) -> _ScMapHead:
234300
return _ScMapHead(
235301
map_head_id=parsed.mapHeadId if parsed.HasField("mapHeadId") else None,
236302
size_x=parsed.sizeX if parsed.HasField("sizeX") else None,
@@ -243,7 +309,7 @@ def _parse_sc_map_head(parsed: Any) -> _ScMapHead:
243309
)
244310

245311

246-
def _parse_sc_room_data(parsed: Any) -> _ScRoomData:
312+
def _parse_sc_room_data(parsed: _ScRoomDataMessage) -> _ScRoomData:
247313
return _ScRoomData(
248314
room_id=parsed.roomId if parsed.HasField("roomId") else None,
249315
room_name=parsed.roomName if parsed.HasField("roomName") else None,
@@ -261,8 +327,8 @@ def _parse_sc_room_data(parsed: Any) -> _ScRoomData:
261327

262328
def _parse_scmap_payload(payload: bytes) -> _ScMapPayload:
263329
"""Parse inflated SCMap bytes into typed map metadata."""
264-
parsed: Any = getattr(b01_scmap_pb2, "RobotMap")()
265-
_parse_proto(payload, parsed, context="B01 SCMap")
330+
parsed = cast(_ScMapMessage, getattr(b01_scmap_pb2, "RobotMap")())
331+
_parse_proto(payload, cast(Message, parsed), context="B01 SCMap")
266332

267333
map_data = None
268334
if parsed.HasField("mapData"):

roborock/map/proto/b01_scmap.proto

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
// Source of truth for the B01/Q7 SCMap schema.
2-
//
3-
// Regenerate the checked-in Python module after edits with:
4-
// python -m grpc_tools.protoc -I. --python_out=. roborock/map/proto/b01_scmap.proto
5-
//
1+
// Checked-in B01/Q7 SCMap schema for the generated runtime protobuf module.
62
// The generated file `b01_scmap_pb2.py` is checked in for runtime use and should
73
// not be edited by hand.
84
syntax = "proto2";

tests/devices/traits/b01/q7/test_map_content.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -57,25 +57,23 @@ def test_q7_map_content_preserves_specific_roborock_errors(q7_api: Q7PropertiesA
5757
q7_api.map_content.parse_map_content(b"raw")
5858

5959

60-
def test_q7_map_content_missing_metadata_fails_lazily(fake_channel: FakeChannel):
60+
def test_q7_map_content_requires_metadata_at_init(fake_channel: FakeChannel):
6161
from roborock.data import HomeDataDevice, HomeDataProduct, RoborockCategory
6262

63-
q7_api = Q7PropertiesApi(
64-
cast(MqttChannel, fake_channel),
65-
device=HomeDataDevice(
66-
duid="abc123",
67-
name="Q7",
68-
local_key="key123key123key1",
69-
product_id="product-id-q7",
70-
sn=None,
71-
),
72-
product=HomeDataProduct(
73-
id="product-id-q7",
74-
name="Roborock Q7",
75-
model="roborock.vacuum.sc05",
76-
category=RoborockCategory.VACUUM,
77-
),
78-
)
79-
80-
with pytest.raises(RoborockException, match="requires device serial number and model metadata"):
81-
q7_api.map_content.parse_map_content(b"raw")
63+
with pytest.raises(ValueError, match="requires device serial number and product model metadata"):
64+
Q7PropertiesApi(
65+
cast(MqttChannel, fake_channel),
66+
device=HomeDataDevice(
67+
duid="abc123",
68+
name="Q7",
69+
local_key="key123key123key1",
70+
product_id="product-id-q7",
71+
sn=None,
72+
),
73+
product=HomeDataProduct(
74+
id="product-id-q7",
75+
name="Roborock Q7",
76+
model="roborock.vacuum.sc05",
77+
category=RoborockCategory.VACUUM,
78+
),
79+
)

0 commit comments

Comments
 (0)