Skip to content

Commit 0eb5720

Browse files
committed
🦎 q7: add B01 map decode/parse/render and map content trait
1 parent ac0c93b commit 0eb5720

File tree

9 files changed

+393
-7
lines changed

9 files changed

+393
-7
lines changed

roborock/devices/device_manager.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,12 @@ 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(
255+
channel,
256+
local_key=device.local_key,
257+
serial=device.sn,
258+
model=product.model,
259+
)
255260
else:
256261
raise UnsupportedDeviceError(f"Device {device.name} has unsupported B01 model: {product.model}")
257262
case _:

roborock/devices/rpc/b01_q7_channel.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
decode_rpc_response,
1515
encode_mqtt_payload,
1616
)
17-
from roborock.roborock_message import RoborockMessage
17+
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
1818

1919
_LOGGER = logging.getLogger(__name__)
2020
_TIMEOUT = 10.0
@@ -99,3 +99,52 @@ def find_response(response_message: RoborockMessage) -> None:
9999
raise
100100
finally:
101101
unsub()
102+
103+
104+
async def send_map_command(mqtt_channel: MqttChannel, request_message: Q7RequestMessage) -> bytes:
105+
"""Send map upload command and wait for MAP_RESPONSE payload bytes."""
106+
107+
roborock_message = encode_mqtt_payload(request_message)
108+
future: asyncio.Future[bytes] = asyncio.get_running_loop().create_future()
109+
110+
def find_response(response_message: RoborockMessage) -> None:
111+
if future.done():
112+
return
113+
114+
if response_message.protocol == RoborockMessageProtocol.MAP_RESPONSE and response_message.payload:
115+
future.set_result(response_message.payload)
116+
return
117+
118+
try:
119+
decoded_dps = decode_rpc_response(response_message)
120+
except RoborockException:
121+
return
122+
123+
for dps_value in decoded_dps.values():
124+
if not isinstance(dps_value, str):
125+
continue
126+
try:
127+
inner = json.loads(dps_value)
128+
except (json.JSONDecodeError, TypeError):
129+
continue
130+
if not isinstance(inner, dict) or inner.get("msgId") != str(request_message.msg_id):
131+
continue
132+
code = inner.get("code", 0)
133+
if code != 0:
134+
future.set_exception(RoborockException(f"B01 command failed with code {code} ({request_message})"))
135+
return
136+
data = inner.get("data")
137+
if isinstance(data, dict) and isinstance(data.get("payload"), str):
138+
try:
139+
future.set_result(bytes.fromhex(data["payload"]))
140+
except ValueError:
141+
pass
142+
143+
unsub = await mqtt_channel.subscribe(find_response)
144+
try:
145+
await mqtt_channel.publish(roborock_message)
146+
return await asyncio.wait_for(future, timeout=_TIMEOUT)
147+
except TimeoutError as ex:
148+
raise RoborockException(f"B01 map command timed out after {_TIMEOUT}s ({request_message})") from ex
149+
finally:
150+
unsub()

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
from roborock.roborock_typing import RoborockB01Q7Methods
2222

2323
from .clean_summary import CleanSummaryTrait
24+
from .map_content import Q7MapContentTrait
2425

2526
__all__ = [
2627
"Q7PropertiesApi",
2728
"CleanSummaryTrait",
29+
"Q7MapContentTrait",
2830
]
2931

3032

@@ -33,11 +35,13 @@ class Q7PropertiesApi(Trait):
3335

3436
clean_summary: CleanSummaryTrait
3537
"""Trait for clean records / clean summary (Q7 `service.get_record_list`)."""
38+
map_content: Q7MapContentTrait
3639

37-
def __init__(self, channel: MqttChannel) -> None:
40+
def __init__(self, channel: MqttChannel, *, local_key: str, serial: str, model: str) -> None:
3841
"""Initialize the B01Props API."""
3942
self._channel = channel
4043
self.clean_summary = CleanSummaryTrait(channel)
44+
self.map_content = Q7MapContentTrait(channel, local_key=local_key, serial=serial, model=model)
4145

4246
async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
4347
"""Query the device for the values of the given Q7 properties."""
@@ -142,6 +146,6 @@ async def send(self, command: CommandType, params: ParamsType) -> Any:
142146
)
143147

144148

145-
def create(channel: MqttChannel) -> Q7PropertiesApi:
146-
"""Create traits for B01 devices."""
147-
return Q7PropertiesApi(channel)
149+
def create(channel: MqttChannel, *, local_key: str, serial: str, model: str) -> Q7PropertiesApi:
150+
"""Create traits for B01 Q7 devices."""
151+
return Q7PropertiesApi(channel, local_key=local_key, serial=serial, model=model)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Map content trait for B01/Q7 devices."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
7+
from roborock.devices.rpc.b01_q7_channel import send_map_command
8+
from roborock.devices.transport.mqtt_channel import MqttChannel
9+
from roborock.devices.traits import Trait
10+
from roborock.devices.traits.v1.map_content import MapContent
11+
from roborock.map.b01_map_parser import decode_b01_map_payload, parse_scmap_payload, render_map_png
12+
from roborock.protocols.b01_q7_protocol import Q7RequestMessage
13+
from roborock.roborock_typing import RoborockB01Q7Methods
14+
15+
16+
@dataclass
17+
class B01MapContent(MapContent):
18+
"""B01 map content wrapper."""
19+
20+
21+
class Q7MapContentTrait(B01MapContent, Trait):
22+
"""Fetch and parse map content from B01/Q7 devices."""
23+
24+
def __init__(self, channel: MqttChannel, *, local_key: str, serial: str, model: str) -> None:
25+
super().__init__()
26+
self._channel = channel
27+
self._local_key = local_key
28+
self._serial = serial
29+
self._model = model
30+
31+
async def refresh(self) -> B01MapContent:
32+
raw_payload = await send_map_command(
33+
self._channel,
34+
Q7RequestMessage(dps=10000, command=RoborockB01Q7Methods.UPLOAD_BY_MAPTYPE, params={"maptype": 301}),
35+
)
36+
inflated = decode_b01_map_payload(
37+
raw_payload,
38+
local_key=self._local_key,
39+
serial=self._serial,
40+
model=self._model,
41+
)
42+
parsed = parse_scmap_payload(inflated)
43+
self.raw_api_response = raw_payload
44+
self.map_data = None
45+
self.image_content = render_map_png(parsed)
46+
return self

roborock/map/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
"""Module for Roborock map related data classes."""
22

3+
from .b01_map_parser import B01MapData, decode_b01_map_payload, parse_scmap_payload, render_map_png
34
from .map_parser import MapParserConfig, ParsedMapData
45

56
__all__ = [
7+
"B01MapData",
68
"MapParserConfig",
9+
"ParsedMapData",
10+
"decode_b01_map_payload",
11+
"parse_scmap_payload",
12+
"render_map_png",
713
]

0 commit comments

Comments
 (0)