22
33import asyncio
44import logging
5+ from typing import Any
56
67from roborock .data .b01_q10 .b01_q10_code_mappings import B01_Q10_DP
78from roborock .devices .rpc .b01_q10_channel import B01Q10Channel
89from roborock .devices .traits import Trait
10+ from roborock .exceptions import RoborockException
911from roborock .map .b01_q10_map_parser import Q10MapPacket , Q10TracePacket
1012from roborock .protocols .b01_q10_protocol import Q10DpsUpdate , Q10Message
1113
3840]
3941
4042_LOGGER = logging .getLogger (__name__ )
43+ _MAP_LIST_REQUEST_TIMEOUT = 30.0
44+
45+
46+ def _map_id_from_list_response (response : Any ) -> int | str | None :
47+ """Return the first usable map ID from a ``dpMultiMap`` list response."""
48+ if not isinstance (response , dict ) or response .get ("op" ) != "list" :
49+ return None
50+ data = response .get ("data" )
51+ if not isinstance (data , list ):
52+ return None
53+ for map_info in data :
54+ if not isinstance (map_info , dict ):
55+ continue
56+ map_id = map_info .get ("id" )
57+ if isinstance (map_id , int ) and not isinstance (map_id , bool ):
58+ return map_id
59+ if isinstance (map_id , str ) and map_id :
60+ return map_id
61+ return None
4162
4263
4364class Q10PropertiesApi (Trait ):
@@ -115,6 +136,9 @@ def __init__(self, channel: B01Q10Channel) -> None:
115136 self ._map_dps ,
116137 ]
117138 self ._subscribe_task : asyncio .Task [None ] | None = None
139+ self ._map_request_lock = asyncio .Lock ()
140+ self ._map_list_request_token : object | None = None
141+ self ._map_list_requested_at : float | None = None
118142
119143 async def start (self ) -> None :
120144 """Start any necessary subscriptions for the trait."""
@@ -132,22 +156,55 @@ async def close(self) -> None:
132156
133157 async def refresh (self ) -> None :
134158 """Refresh all traits."""
135- # Sending the REQUEST_DPS will cause the device to send all DPS values
136- # to the device. Updates will be received by the subscribe loop below.
159+ # Status and map retrieval use separate Q10 requests. A bare REQUEST_DPS
160+ # reliably refreshes status but does not reliably make every firmware
161+ # publish its map.
137162 await self .command .send (B01_Q10_DP .REQUEST_DPS , params = {})
163+ await self .request_map ()
164+
165+ async def request_map (self ) -> None :
166+ """Request the current saved map through the Q10 multi-map protocol.
167+
168+ The list response arrives asynchronously on the subscribe stream.
169+ ``_handle_message`` extracts its first map ID and follows up with the
170+ matching ``get`` command; the resulting protocol-301 map packet is
171+ routed to :attr:`map`.
172+ """
173+ async with self ._map_request_lock :
174+ now = asyncio .get_running_loop ().time ()
175+ if (
176+ self ._map_list_request_token is not None
177+ and self ._map_list_requested_at is not None
178+ and now - self ._map_list_requested_at < _MAP_LIST_REQUEST_TIMEOUT
179+ ):
180+ return
181+ token = object ()
182+ self ._map_list_request_token = token
183+ self ._map_list_requested_at = now
184+ sent = False
185+ try :
186+ await self .command .send (
187+ B01_Q10_DP .COMMON ,
188+ {str (B01_Q10_DP .MULTI_MAP .code ): {"op" : "list" }},
189+ )
190+ sent = True
191+ finally :
192+ if not sent and self ._map_list_request_token is token :
193+ self ._map_list_request_token = None
194+ self ._map_list_requested_at = None
138195
139196 async def _subscribe_loop (self ) -> None :
140197 """Persistent loop dispatching decoded messages to the read-model traits."""
141198 async for message in self ._channel .subscribe_stream ():
142- self ._handle_message (message )
199+ await self ._handle_message (message )
143200
144- def _handle_message (self , message : Q10Message ) -> None :
201+ async def _handle_message (self , message : Q10Message ) -> None :
145202 """Route a single decoded message to the trait responsible for it.
146203
147- Map and trace packets arrive as protocol-301 ``MAP_RESPONSE`` pushes (the
148- Q10 is entirely push-driven: there is no synchronous get-map request, a
149- ``dpRequestDps`` just nudges the device to publish its current map). DPS
150- updates feed the read-model traits. More traits can be dispatched here below .
204+ Map and trace packets arrive as protocol-301 ``MAP_RESPONSE`` pushes.
205+ A ``dpMultiMap`` list response completes the asynchronous request flow
206+ started by :meth:`request_map`; other DPS updates feed the read-model
207+ traits.
151208 """
152209 if isinstance (message , Q10MapPacket ):
153210 self .map .update_from_map_packet (message )
@@ -159,6 +216,39 @@ def _handle_message(self, message: Q10Message) -> None:
159216 # only updates the fields that it is responsible for.
160217 for trait in self ._updatable_traits :
161218 trait .update_from_dps (message .dps )
219+ await self ._request_map_from_list_response (message .dps )
220+
221+ async def _request_map_from_list_response (self , decoded_dps : dict [B01_Q10_DP , Any ]) -> None :
222+ """Request map content after receiving our pending map-list response."""
223+ response = decoded_dps .get (B01_Q10_DP .MULTI_MAP )
224+ if self ._map_list_request_token is None or not isinstance (response , dict ) or response .get ("op" ) != "list" :
225+ return
226+
227+ token = self ._map_list_request_token
228+ if (map_id := _map_id_from_list_response (response )) is None :
229+ if self ._map_list_request_token is token :
230+ self ._map_list_request_token = None
231+ self ._map_list_requested_at = None
232+ _LOGGER .debug ("Q10 map list response did not contain a usable map ID" )
233+ return
234+
235+ try :
236+ await self .command .send (
237+ B01_Q10_DP .COMMON ,
238+ {
239+ str (B01_Q10_DP .MULTI_MAP .code ): {
240+ "op" : "get" ,
241+ "id" : map_id ,
242+ }
243+ },
244+ )
245+ except RoborockException as ex :
246+ # A failed follow-up must not kill the persistent subscribe loop.
247+ _LOGGER .debug ("Failed to request Q10 map content: %s" , ex )
248+ finally :
249+ if self ._map_list_request_token is token :
250+ self ._map_list_request_token = None
251+ self ._map_list_requested_at = None
162252
163253
164254def create (channel : B01Q10Channel ) -> Q10PropertiesApi :
0 commit comments