66import json
77import logging
88from collections .abc import Callable
9- from typing import TypeAlias , TypeVar
9+ from typing import Protocol , TypeAlias , TypeVar
1010
11+ from roborock .data import HomeDataDevice , HomeDataProduct
12+ from roborock .devices .transport .channel import Channel
1113from roborock .devices .transport .mqtt_channel import MqttChannel
1214from roborock .exceptions import RoborockException
1315from roborock .protocols .b01_q7_protocol import (
16+ B01_Q7_DPS ,
1417 B01_VERSION ,
18+ CommandType ,
1519 MapKey ,
20+ ParamsType ,
1621 Q7RequestMessage ,
22+ create_map_key ,
1723 decode_map_payload ,
1824 decode_rpc_response ,
1925 encode_mqtt_payload ,
2632DecodedB01Response : TypeAlias = dict [str , object ] | str
2733
2834
35+ class Q7RpcChannel (Protocol ):
36+ """Protocol for Q7 RPC channels."""
37+
38+ async def send_command (
39+ self ,
40+ command : CommandType ,
41+ params : ParamsType = None ,
42+ ) -> DecodedB01Response :
43+ """Send a command and get a decoded response."""
44+ ...
45+
46+
47+ class Q7MapRpcChannel (Protocol ):
48+ """Protocol for Q7 map RPC channels."""
49+
50+ async def send_map_command (
51+ self ,
52+ command : CommandType ,
53+ params : ParamsType = None ,
54+ ) -> bytes :
55+ """Send a map command and get decoded bytes."""
56+ ...
57+
58+
2959def _matches_map_response (response_message : RoborockMessage , * , version : bytes | None ) -> bytes | None :
3060 """Return raw map payload bytes for matching MAP_RESPONSE messages."""
3161 if (
@@ -120,39 +150,55 @@ def find_response(response_message: RoborockMessage) -> DecodedB01Response | Non
120150 raise RoborockException (f"B01 command timed out after { _TIMEOUT } s ({ request_message } )" ) from ex
121151 except RoborockException as ex :
122152 _LOGGER .warning (
123- "Error sending B01 decoded command (%ss ): %s" ,
153+ "Error sending B01 decoded command (%s ): %s" ,
124154 request_message ,
125155 ex ,
126156 )
127157 raise
128158 except Exception as ex :
129159 _LOGGER .exception (
130- "Error sending B01 decoded command (%ss ): %s" ,
160+ "Error sending B01 decoded command (%s ): %s" ,
131161 request_message ,
132162 ex ,
133163 )
134164 raise
135165
136166
137- class MapRpcChannel :
138- """RPC channel for map-related commands on B01/Q7 devices ."""
167+ class B01Q7Channel ( Channel , Q7RpcChannel , Q7MapRpcChannel ) :
168+ """Unified B01 Q7 channel wrapping MQTT transport ."""
139169
140170 def __init__ (self , mqtt_channel : MqttChannel , map_key : MapKey ) -> None :
141171 self ._mqtt_channel = mqtt_channel
142172 self ._map_key = map_key
143173
144- async def send_map_command (self , request_message : Q7RequestMessage ) -> bytes :
145- """Send a map upload command and return decoded SCMap bytes.
146-
147- This publishes the request and waits for a matching ``MAP_RESPONSE`` message
148- with the correct protocol version. The raw ``MAP_RESPONSE`` payload bytes are
149- then decoded/inflated via :func:`decode_map_payload` using this channel's
150- ``map_key``, and the resulting SCMap bytes are returned.
151-
152- The returned value is the decoded map data bytes suitable for passing to the
153- map parser library, not the raw MQTT ``MAP_RESPONSE`` payload bytes.
154- """
174+ @property
175+ def is_connected (self ) -> bool :
176+ return self ._mqtt_channel .is_connected
177+
178+ @property
179+ def is_local_connected (self ) -> bool :
180+ return False
181+
182+ async def subscribe (self , callback : Callable [[RoborockMessage ], None ]) -> Callable [[], None ]:
183+ return await self ._mqtt_channel .subscribe (callback )
184+
185+ async def send_command (
186+ self ,
187+ command : CommandType ,
188+ params : ParamsType = None ,
189+ ) -> DecodedB01Response :
190+ return await send_decoded_command (
191+ self ._mqtt_channel ,
192+ Q7RequestMessage (dps = B01_Q7_DPS , command = command , params = params ),
193+ )
155194
195+ async def send_map_command (
196+ self ,
197+ command : CommandType ,
198+ params : ParamsType = None ,
199+ ) -> bytes :
200+ """Send a map upload command and return decoded SCMap bytes."""
201+ request_message = Q7RequestMessage (dps = B01_Q7_DPS , command = command , params = params )
156202 try :
157203 raw_payload = await _send_command (
158204 self ._mqtt_channel ,
@@ -163,3 +209,17 @@ async def send_map_command(self, request_message: Q7RequestMessage) -> bytes:
163209 raise RoborockException (f"B01 map command timed out after { _TIMEOUT } s ({ request_message } )" ) from ex
164210
165211 return decode_map_payload (raw_payload , map_key = self ._map_key )
212+
213+
214+ def create_b01_q7_channel (
215+ device : HomeDataDevice ,
216+ product : HomeDataProduct ,
217+ mqtt_channel : MqttChannel ,
218+ ) -> B01Q7Channel :
219+ """Create a B01Q7Channel for the given device."""
220+ if device .sn is None or product .model is None :
221+ raise RoborockException (
222+ f"Device serial number and product model are required (sn: { device .sn } , model: { product .model } )"
223+ )
224+ map_key = create_map_key (serial = device .sn , model = product .model )
225+ return B01Q7Channel (mqtt_channel , map_key )
0 commit comments