Skip to content

Commit dea0d4d

Browse files
committed
🦎 q7: add b01 map content trait
1 parent 6f9251f commit dea0d4d

File tree

8 files changed

+606
-11
lines changed

8 files changed

+606
-11
lines changed

roborock/devices/device_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat
251251
trait = b01.q10.create(channel)
252252
elif "sc" in model_part:
253253
# Q7 devices start with 'sc' in their model naming.
254-
trait = b01.q7.create(channel)
254+
trait = b01.q7.create(product, device, channel)
255255
else:
256256
raise UnsupportedDeviceError(f"Device {device.name} has unsupported B01 model: {product.model}")
257257
case _:

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

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
"""Traits for Q7 B01 devices.
2-
Potentially other devices may fall into this category in the future."""
2+
3+
Potentially other devices may fall into this category in the future.
4+
"""
5+
6+
from __future__ import annotations
37

48
from typing import Any
59

610
from roborock import B01Props
7-
from roborock.data import Q7MapList, Q7MapListEntry
11+
from roborock.data import HomeDataDevice, HomeDataProduct, Q7MapList, Q7MapListEntry
812
from roborock.data.b01_q7.b01_q7_code_mappings import (
913
CleanPathPreferenceMapping,
1014
CleanRepeatMapping,
@@ -17,36 +21,56 @@
1721
from roborock.devices.rpc.b01_q7_channel import send_decoded_command
1822
from roborock.devices.traits import Trait
1923
from roborock.devices.transport.mqtt_channel import MqttChannel
24+
from roborock.exceptions import RoborockException
2025
from roborock.protocols.b01_q7_protocol import B01_Q7_DPS, CommandType, ParamsType, Q7RequestMessage
2126
from roborock.roborock_message import RoborockB01Props
2227
from roborock.roborock_typing import RoborockB01Q7Methods
2328

2429
from .clean_summary import CleanSummaryTrait
2530
from .map import MapTrait
31+
from .map_content import MapContentTrait
2632

2733
__all__ = [
2834
"Q7PropertiesApi",
2935
"CleanSummaryTrait",
3036
"MapTrait",
37+
"MapContentTrait",
3138
"Q7MapList",
3239
"Q7MapListEntry",
3340
]
3441

3542

3643
class Q7PropertiesApi(Trait):
37-
"""API for interacting with B01 devices."""
44+
"""API for interacting with B01 Q7 devices."""
3845

3946
clean_summary: CleanSummaryTrait
4047
"""Trait for clean records / clean summary (Q7 `service.get_record_list`)."""
4148

4249
map: MapTrait
4350
"""Trait for map list metadata + raw map payload retrieval."""
4451

45-
def __init__(self, channel: MqttChannel) -> None:
46-
"""Initialize the B01Props API."""
52+
map_content: MapContentTrait
53+
"""Trait for fetching parsed current map content."""
54+
55+
def __init__(self, channel: MqttChannel, *, device: HomeDataDevice, product: HomeDataProduct) -> None:
56+
"""Initialize the Q7 API."""
4757
self._channel = channel
58+
self._device = device
59+
self._product = product
60+
61+
if not device.sn:
62+
raise RoborockException(
63+
"B01/Q7 map parsing requires the device serial number (HomeDataDevice.sn), but it was missing"
64+
)
65+
4866
self.clean_summary = CleanSummaryTrait(channel)
4967
self.map = MapTrait(channel)
68+
self.map_content = MapContentTrait(
69+
self.map,
70+
local_key=device.local_key,
71+
serial=device.sn,
72+
model=product.model,
73+
)
5074

5175
async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
5276
"""Query the device for the values of the given Q7 properties."""
@@ -151,6 +175,6 @@ async def send(self, command: CommandType, params: ParamsType) -> Any:
151175
)
152176

153177

154-
def create(channel: MqttChannel) -> Q7PropertiesApi:
155-
"""Create traits for B01 devices."""
156-
return Q7PropertiesApi(channel)
178+
def create(product: HomeDataProduct, device: HomeDataDevice, channel: MqttChannel) -> Q7PropertiesApi:
179+
"""Create traits for B01 Q7 devices."""
180+
return Q7PropertiesApi(channel, device=device, product=product)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Trait for fetching parsed map content from B01/Q7 devices.
2+
3+
This follows the same basic pattern as the v1 `MapContentTrait`:
4+
- `refresh()` performs I/O and populates cached fields
5+
- fields `image_content`, `map_data`, and `raw_api_response` are then readable
6+
7+
For B01/Q7 devices, the underlying raw map payload is retrieved via `MapTrait`.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from dataclasses import dataclass
13+
14+
from vacuum_map_parser_base.map_data import MapData
15+
16+
from roborock.data import RoborockBase
17+
from roborock.devices.traits import Trait
18+
from roborock.exceptions import RoborockException
19+
from roborock.map.b01_map_parser import B01MapParser, B01MapParserConfig
20+
21+
from .map import MapTrait
22+
23+
_TRUNCATE_LENGTH = 20
24+
25+
26+
@dataclass
27+
class MapContent(RoborockBase):
28+
"""Dataclass representing map content."""
29+
30+
image_content: bytes | None = None
31+
"""The rendered image of the map in PNG format."""
32+
33+
map_data: MapData | None = None
34+
"""Parsed map data (metadata for points on the map)."""
35+
36+
raw_api_response: bytes | None = None
37+
"""Raw bytes of the map payload from the device.
38+
39+
This should be treated as an opaque blob used only internally by this
40+
library to re-parse the map data when needed.
41+
"""
42+
43+
def __repr__(self) -> str:
44+
img = self.image_content
45+
if img and len(img) > _TRUNCATE_LENGTH:
46+
img = img[: _TRUNCATE_LENGTH - 3] + b"..."
47+
return f"MapContent(image_content={img!r}, map_data={self.map_data!r})"
48+
49+
50+
class MapContentTrait(MapContent, Trait):
51+
"""Trait for fetching parsed map content for Q7 devices."""
52+
53+
def __init__(
54+
self,
55+
map_trait: MapTrait,
56+
*,
57+
local_key: str,
58+
serial: str,
59+
model: str,
60+
map_parser_config: B01MapParserConfig | None = None,
61+
) -> None:
62+
super().__init__()
63+
self._map_trait = map_trait
64+
self._local_key = local_key
65+
self._serial = serial
66+
self._model = model
67+
self._map_parser = B01MapParser(map_parser_config)
68+
69+
async def refresh(self) -> None:
70+
"""Fetch, decode, and parse the current map payload."""
71+
raw_payload = await self._map_trait.get_current_map_payload()
72+
parsed = self.parse_map_content(raw_payload)
73+
self.image_content = parsed.image_content
74+
self.map_data = parsed.map_data
75+
self.raw_api_response = parsed.raw_api_response
76+
77+
def parse_map_content(self, response: bytes) -> MapContent:
78+
"""Parse map content from raw bytes.
79+
80+
Exposed so callers can re-parse cached map payload bytes without
81+
performing I/O.
82+
"""
83+
try:
84+
parsed_data = self._map_parser.parse(
85+
response,
86+
local_key=self._local_key,
87+
serial=self._serial,
88+
model=self._model,
89+
)
90+
except Exception as ex:
91+
raise RoborockException("Failed to parse B01 map data") from ex
92+
93+
if parsed_data.image_content is None:
94+
raise RoborockException("Failed to render B01 map image")
95+
96+
return MapContent(
97+
image_content=parsed_data.image_content,
98+
map_data=parsed_data.map_data,
99+
raw_api_response=response,
100+
)

0 commit comments

Comments
 (0)