11"""Thin wrapper around the MQTT channel for Roborock B01 Q7 devices."""
22
3- from __future__ import annotations
4-
53import asyncio
64import json
75import logging
86from collections .abc import Callable
9- from typing import TypeAlias , TypeVar
7+ from typing import Protocol , TypeAlias , TypeVar
108
9+ from roborock .data import HomeDataDevice , HomeDataProduct
10+ from roborock .devices .transport .channel import Channel
1111from roborock .devices .transport .mqtt_channel import MqttChannel
1212from roborock .exceptions import RoborockException
1313from roborock .protocols .b01_q7_protocol import (
14+ B01_Q7_DPS ,
1415 B01_VERSION ,
16+ CommandType ,
1517 MapKey ,
18+ ParamsType ,
1619 Q7RequestMessage ,
20+ create_map_key ,
1721 decode_map_payload ,
1822 decode_rpc_response ,
1923 encode_mqtt_payload ,
2630DecodedB01Response : TypeAlias = dict [str , object ] | str
2731
2832
33+ class Q7RpcChannel (Protocol ):
34+ """Protocol for Q7 RPC channels."""
35+
36+ async def send_command (
37+ self ,
38+ command : CommandType ,
39+ params : ParamsType = None ,
40+ ) -> DecodedB01Response :
41+ """Send a command and get a decoded response."""
42+ ...
43+
44+
45+ class Q7MapRpcChannel (Protocol ):
46+ """Protocol for Q7 map RPC channels."""
47+
48+ async def send_map_command (
49+ self ,
50+ command : CommandType ,
51+ params : ParamsType = None ,
52+ ) -> bytes :
53+ """Send a map command and get decoded bytes."""
54+ ...
55+
56+
2957def _matches_map_response (response_message : RoborockMessage , * , version : bytes | None ) -> bytes | None :
3058 """Return raw map payload bytes for matching MAP_RESPONSE messages."""
3159 if (
@@ -120,39 +148,55 @@ def find_response(response_message: RoborockMessage) -> DecodedB01Response | Non
120148 raise RoborockException (f"B01 command timed out after { _TIMEOUT } s ({ request_message } )" ) from ex
121149 except RoborockException as ex :
122150 _LOGGER .warning (
123- "Error sending B01 decoded command (%ss ): %s" ,
151+ "Error sending B01 decoded command (%s ): %s" ,
124152 request_message ,
125153 ex ,
126154 )
127155 raise
128156 except Exception as ex :
129157 _LOGGER .exception (
130- "Error sending B01 decoded command (%ss ): %s" ,
158+ "Error sending B01 decoded command (%s ): %s" ,
131159 request_message ,
132160 ex ,
133161 )
134162 raise
135163
136164
137- class MapRpcChannel :
138- """RPC channel for map-related commands on B01/Q7 devices ."""
165+ class B01Q7Channel ( Channel , Q7RpcChannel , Q7MapRpcChannel ) :
166+ """Unified B01 Q7 channel wrapping MQTT transport ."""
139167
140168 def __init__ (self , mqtt_channel : MqttChannel , map_key : MapKey ) -> None :
141169 self ._mqtt_channel = mqtt_channel
142170 self ._map_key = map_key
143171
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- """
172+ @property
173+ def is_connected (self ) -> bool :
174+ return self ._mqtt_channel .is_connected
175+
176+ @property
177+ def is_local_connected (self ) -> bool :
178+ return False
179+
180+ async def subscribe (self , callback : Callable [[RoborockMessage ], None ]) -> Callable [[], None ]:
181+ return await self ._mqtt_channel .subscribe (callback )
182+
183+ async def send_command (
184+ self ,
185+ command : CommandType ,
186+ params : ParamsType = None ,
187+ ) -> DecodedB01Response :
188+ return await send_decoded_command (
189+ self ._mqtt_channel ,
190+ Q7RequestMessage (dps = B01_Q7_DPS , command = command , params = params ),
191+ )
155192
193+ async def send_map_command (
194+ self ,
195+ command : CommandType ,
196+ params : ParamsType = None ,
197+ ) -> bytes :
198+ """Send a map upload command and return decoded SCMap bytes."""
199+ request_message = Q7RequestMessage (dps = B01_Q7_DPS , command = command , params = params )
156200 try :
157201 raw_payload = await _send_command (
158202 self ._mqtt_channel ,
@@ -163,3 +207,17 @@ async def send_map_command(self, request_message: Q7RequestMessage) -> bytes:
163207 raise RoborockException (f"B01 map command timed out after { _TIMEOUT } s ({ request_message } )" ) from ex
164208
165209 return decode_map_payload (raw_payload , map_key = self ._map_key )
210+
211+
212+ def create_b01_q7_channel (
213+ device : HomeDataDevice ,
214+ product : HomeDataProduct ,
215+ mqtt_channel : MqttChannel ,
216+ ) -> B01Q7Channel :
217+ """Create a B01Q7Channel for the given device."""
218+ if device .sn is None or product .model is None :
219+ raise RoborockException (
220+ f"Device serial number and product model are required (sn: { device .sn } , model: { product .model } )"
221+ )
222+ map_key = create_map_key (serial = device .sn , model = product .model )
223+ return B01Q7Channel (mqtt_channel , map_key )
0 commit comments