Skip to content

Commit 632b160

Browse files
committed
🦎 q7: harden map command decode + cover RPC payload tests
1 parent 861b338 commit 632b160

2 files changed

Lines changed: 54 additions & 7 deletions

File tree

roborock/devices/rpc/b01_q7_channel.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,23 @@ async def send_map_command(mqtt_channel: MqttChannel, request_message: Q7Request
113113

114114
roborock_message = encode_mqtt_payload(request_message)
115115
future: asyncio.Future[bytes] = asyncio.get_running_loop().create_future()
116+
publish_started = asyncio.Event()
116117

117118
def find_response(response_message: RoborockMessage) -> None:
118119
if future.done():
119120
return
120121

121-
if response_message.protocol == RoborockMessageProtocol.MAP_RESPONSE and response_message.payload:
122-
if not future.done():
123-
future.set_result(response_message.payload)
122+
# Avoid accepting queued/stale MAP_RESPONSE messages before we actually
123+
# publish this request.
124+
if not publish_started.is_set():
125+
return
126+
127+
if (
128+
response_message.protocol == RoborockMessageProtocol.MAP_RESPONSE
129+
and response_message.payload
130+
and response_message.version == roborock_message.version
131+
):
132+
future.set_result(response_message.payload)
124133
return
125134

126135
try:
@@ -145,13 +154,25 @@ def find_response(response_message: RoborockMessage) -> None:
145154
data = inner.get("data")
146155
if isinstance(data, dict) and isinstance(data.get("payload"), str):
147156
try:
148-
if not future.done():
149-
future.set_result(bytes.fromhex(data["payload"]))
150-
except ValueError:
151-
pass
157+
future.set_result(bytes.fromhex(data["payload"]))
158+
except ValueError as ex:
159+
future.set_exception(
160+
RoborockException(
161+
f"Invalid hex payload in B01 map response: {data.get('payload')} ({request_message})"
162+
)
163+
)
164+
_LOGGER.debug(
165+
"Invalid hex payload in B01 map response (msgId=%s): %s (%s): %s",
166+
inner.get("msgId"),
167+
data.get("payload"),
168+
request_message,
169+
ex,
170+
)
171+
return
152172

153173
unsub = await mqtt_channel.subscribe(find_response)
154174
try:
175+
publish_started.set()
155176
await mqtt_channel.publish(roborock_message)
156177
return await asyncio.wait_for(future, timeout=_TIMEOUT)
157178
except TimeoutError as ex:

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,32 @@ 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_get_current_map_payload_rpc_wrapped_hex_payload(
313+
q7_api: Q7PropertiesApi,
314+
fake_channel: FakeChannel,
315+
message_builder: B01MessageBuilder,
316+
):
317+
"""Fetch current map via RPC-wrapped hex payload response."""
318+
fake_channel.response_queue.append(message_builder.build({"map_list": [{"id": 1772093512, "cur": True}]}))
319+
fake_channel.response_queue.append(message_builder.build({"payload": "68656c6c6f"}))
320+
321+
raw_payload = await q7_api.get_current_map_payload()
322+
assert raw_payload == b"hello"
323+
324+
325+
async def test_q7_api_get_current_map_payload_rpc_wrapped_invalid_hex_errors(
326+
q7_api: Q7PropertiesApi,
327+
fake_channel: FakeChannel,
328+
message_builder: B01MessageBuilder,
329+
):
330+
"""Invalid hex payload should fail fast (not time out)."""
331+
fake_channel.response_queue.append(message_builder.build({"map_list": [{"id": 1772093512, "cur": True}]}))
332+
fake_channel.response_queue.append(message_builder.build({"payload": "not-hex"}))
333+
334+
with pytest.raises(RoborockException, match="Invalid hex payload"):
335+
await q7_api.get_current_map_payload()
336+
337+
312338
async def test_q7_api_get_current_map_payload_falls_back_to_first_map(
313339
q7_api: Q7PropertiesApi,
314340
fake_channel: FakeChannel,

0 commit comments

Comments
 (0)