Skip to content

Commit 0b8f6fe

Browse files
committed
🦎 q7: align map trait with refresh-based pattern
1 parent a20ae01 commit 0b8f6fe

3 files changed

Lines changed: 36 additions & 61 deletions

File tree

roborock/devices/rpc/b01_q7_channel.py

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,7 @@ def find_response(response_message: RoborockMessage) -> None:
102102

103103

104104
async def send_map_command(mqtt_channel: MqttChannel, request_message: Q7RequestMessage) -> bytes:
105-
"""Send map upload command and wait for the map payload bytes.
106-
107-
On a real Q7 B01 device, map uploads arrive as a dedicated
108-
``MAP_RESPONSE`` message with raw payload bytes.
109-
110-
We still decode RPC responses so we can fail fast on non-zero ``code``
111-
values for the initiating request (matched by msgId).
112-
"""
105+
"""Send map upload command and wait for MAP_RESPONSE payload bytes."""
113106

114107
roborock_message = encode_mqtt_payload(request_message)
115108
future: asyncio.Future[bytes] = asyncio.get_running_loop().create_future()
@@ -124,29 +117,6 @@ def find_response(response_message: RoborockMessage) -> None:
124117
and response_message.version == roborock_message.version
125118
):
126119
future.set_result(response_message.payload)
127-
return
128-
129-
# Optional: look for an error response correlated by msgId.
130-
try:
131-
decoded_dps = decode_rpc_response(response_message)
132-
except RoborockException:
133-
return
134-
135-
dps_value = decoded_dps.get(request_message.dps)
136-
if not isinstance(dps_value, str):
137-
return
138-
139-
try:
140-
inner = json.loads(dps_value)
141-
except (json.JSONDecodeError, TypeError):
142-
return
143-
144-
if not isinstance(inner, dict) or inner.get("msgId") != str(request_message.msg_id):
145-
return
146-
147-
code = inner.get("code", 0)
148-
if code != 0:
149-
future.set_exception(RoborockException(f"B01 command failed with code {code} ({request_message})"))
150120

151121
unsub = await mqtt_channel.subscribe(find_response)
152122
try:

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

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,22 @@ def __init__(self, channel: MqttChannel) -> None:
3636
self._channel = channel
3737
# Map uploads are serialized per-device to avoid response cross-wiring.
3838
self._map_command_lock = asyncio.Lock()
39-
self._map_list_cache: Q7MapList | None = None
39+
self._map_list: Q7MapList | None = None
4040

41-
async def refresh_map_list(self) -> Q7MapList:
42-
"""Fetch and cache map list metadata from the robot."""
41+
@property
42+
def map_list(self) -> Q7MapList | None:
43+
"""Latest cached map list metadata, populated by ``refresh()``."""
44+
return self._map_list
45+
46+
@property
47+
def current_map_id(self) -> int | None:
48+
"""Current map id derived from cached map list metadata."""
49+
if self._map_list is None:
50+
return None
51+
return self._extract_current_map_id(self._map_list)
52+
53+
async def refresh(self) -> None:
54+
"""Refresh cached map list metadata from the device."""
4355
response = await send_decoded_command(
4456
self._channel,
4557
Q7RequestMessage(dps=_Q7_DPS, command=RoborockB01Q7Methods.GET_MAP_LIST, params={}),
@@ -51,24 +63,9 @@ async def refresh_map_list(self) -> Q7MapList:
5163
if parsed is None:
5264
raise TypeError(f"Failed to decode map list response: {response!r}")
5365

54-
self._map_list_cache = parsed
55-
return parsed
56-
57-
async def get_map_list(self, *, refresh: bool = False) -> Q7MapList:
58-
"""Return cached map list metadata, refreshing when requested or absent."""
59-
if refresh or self._map_list_cache is None:
60-
return await self.refresh_map_list()
61-
return self._map_list_cache
66+
self._map_list = parsed
6267

63-
async def get_current_map_id(self, *, refresh: bool = False) -> int:
64-
"""Resolve and return the currently active map id from map list metadata."""
65-
map_list = await self.get_map_list(refresh=refresh)
66-
map_id = self._extract_current_map_id(map_list)
67-
if map_id is None:
68-
raise RoborockException(f"Unable to determine map_id from map list response: {map_list!r}")
69-
return map_id
70-
71-
async def get_map_payload(self, *, map_id: int) -> bytes:
68+
async def _get_map_payload(self, *, map_id: int) -> bytes:
7269
"""Fetch raw map payload bytes for the given map id."""
7370
request = Q7RequestMessage(
7471
dps=_Q7_DPS,
@@ -78,10 +75,15 @@ async def get_map_payload(self, *, map_id: int) -> bytes:
7875
async with self._map_command_lock:
7976
return await send_map_command(self._channel, request)
8077

81-
async def get_current_map_payload(self, *, refresh_map_list: bool = False) -> bytes:
78+
async def get_current_map_payload(self) -> bytes:
8279
"""Fetch raw map payload bytes for the currently selected map."""
83-
map_id = await self.get_current_map_id(refresh=refresh_map_list)
84-
return await self.get_map_payload(map_id=map_id)
80+
if self._map_list is None:
81+
await self.refresh()
82+
83+
map_id = self.current_map_id
84+
if map_id is None:
85+
raise RoborockException(f"Unable to determine map_id from map list response: {self._map_list!r}")
86+
return await self._get_map_payload(map_id=map_id)
8587

8688
@staticmethod
8789
def _extract_current_map_id(map_list_response: Q7MapList) -> int | None:

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -309,21 +309,24 @@ async def test_q7_api_get_current_map_payload(
309309
assert second_payload["dps"]["10000"]["params"] == {"map_id": 1772093512}
310310

311311

312-
async def test_q7_api_map_trait_caches_map_list(
312+
async def test_q7_api_map_trait_refresh_populates_cached_values(
313313
q7_api: Q7PropertiesApi,
314314
fake_channel: FakeChannel,
315315
message_builder: B01MessageBuilder,
316316
):
317-
"""Map list is represented as dataclasses and cached on the map trait."""
317+
"""Map trait follows refresh + cached-value access pattern."""
318318
fake_channel.response_queue.append(message_builder.build({"map_list": [{"id": 101, "cur": True}]}))
319319

320-
first = await q7_api.map.get_map_list()
321-
second = await q7_api.map.get_map_list()
320+
assert q7_api.map.map_list is None
321+
assert q7_api.map.current_map_id is None
322+
323+
await q7_api.map.refresh()
322324

323325
assert len(fake_channel.published_messages) == 1
324-
assert first is second
325-
assert first.map_list[0].id == 101
326-
assert first.map_list[0].cur is True
326+
assert q7_api.map.map_list is not None
327+
assert q7_api.map.map_list.map_list[0].id == 101
328+
assert q7_api.map.map_list.map_list[0].cur is True
329+
assert q7_api.map.current_map_id == 101
327330

328331

329332
async def test_q7_api_get_current_map_payload_falls_back_to_first_map(

0 commit comments

Comments
 (0)