Skip to content

Commit 0d807db

Browse files
committed
feat: add l01 to the new device format
1 parent df438f7 commit 0d807db

File tree

6 files changed

+109
-16
lines changed

6 files changed

+109
-16
lines changed

roborock/data/v1/v1_containers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@ class AppInitStatus(RoborockBase):
578578
new_feature_info_str: str
579579
new_feature_info_2: int | None = None
580580
carriage_type: int | None = None
581-
dsp_version: int | None = None
581+
dsp_version: str | None = None
582582

583583

584584
@dataclass

roborock/devices/local_channel.py

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77

88
from roborock.callbacks import CallbackList, decoder_callback
99
from roborock.exceptions import RoborockConnectionException, RoborockException
10-
from roborock.protocol import Decoder, Encoder, create_local_decoder, create_local_encoder
11-
from roborock.roborock_message import RoborockMessage
10+
from roborock.protocol import create_local_decoder, create_local_encoder
11+
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
1212

13+
from ..protocols.v1_protocol import LocalProtocolVersion
14+
from ..util import get_next_int
1315
from .channel import Channel
1416

1517
_LOGGER = logging.getLogger(__name__)
1618
_PORT = 58867
19+
_TIMEOUT = 10.0
1720

1821

1922
@dataclass
@@ -39,18 +42,83 @@ class LocalChannel(Channel):
3942
format most parsing to higher-level components.
4043
"""
4144

42-
def __init__(self, host: str, local_key: str):
45+
def __init__(self, host: str, local_key: str, local_protocol_version: LocalProtocolVersion | None = None):
4346
self._host = host
4447
self._transport: asyncio.Transport | None = None
4548
self._protocol: _LocalProtocol | None = None
4649
self._subscribers: CallbackList[RoborockMessage] = CallbackList(_LOGGER)
4750
self._is_connected = False
48-
49-
self._decoder: Decoder = create_local_decoder(local_key)
50-
self._encoder: Encoder = create_local_encoder(local_key)
51+
self._local_key = local_key
52+
self._local_protocol_version = local_protocol_version
53+
self._connect_nonce = get_next_int(10000, 32767)
54+
self._ack_nonce: int | None = None
55+
self._update_encoder_decoder()
56+
57+
def _update_encoder_decoder(self):
58+
self._encoder = create_local_encoder(
59+
local_key=self._local_key, connect_nonce=self._connect_nonce, ack_nonce=self._ack_nonce
60+
)
61+
self._decoder = create_local_decoder(
62+
local_key=self._local_key, connect_nonce=self._connect_nonce, ack_nonce=self._ack_nonce
63+
)
5164
# Callback to decode messages and dispatch to subscribers
5265
self._data_received: Callable[[bytes], None] = decoder_callback(self._decoder, self._subscribers, _LOGGER)
5366

67+
async def _do_hello(self, local_protocol_version: LocalProtocolVersion) -> bool:
68+
"""Perform the initial handshaking."""
69+
_LOGGER.debug(
70+
"Attempting to use the %s protocol for client %s...",
71+
local_protocol_version,
72+
self._host,
73+
)
74+
request = RoborockMessage(
75+
protocol=RoborockMessageProtocol.HELLO_REQUEST,
76+
version=local_protocol_version.encode(),
77+
random=self._connect_nonce,
78+
seq=1,
79+
)
80+
try:
81+
response = await self.send_message(
82+
roborock_message=request,
83+
request_id=request.seq,
84+
response_protocol=RoborockMessageProtocol.HELLO_RESPONSE,
85+
)
86+
self._ack_nonce = response.random
87+
self._local_protocol_version = local_protocol_version
88+
self._update_encoder_decoder()
89+
90+
_LOGGER.debug(
91+
"Client %s speaks the %s protocol.",
92+
self._host,
93+
local_protocol_version,
94+
)
95+
return True
96+
except RoborockException as e:
97+
_LOGGER.debug(
98+
"Client %s did not respond or does not speak the %s protocol. %s",
99+
self._host,
100+
local_protocol_version,
101+
e,
102+
)
103+
return False
104+
105+
async def hello(self):
106+
"""Send hello to the device to negotiate protocol."""
107+
if self._local_protocol_version:
108+
# version is forced - try it first, if it fails, try the opposite
109+
if not await self._do_hello(self._local_protocol_version):
110+
if not await self._do_hello(
111+
LocalProtocolVersion.V1
112+
if self._local_protocol_version is not LocalProtocolVersion.V1
113+
else LocalProtocolVersion.L01
114+
):
115+
raise RoborockException("Failed to connect to device with any known protocol")
116+
else:
117+
# try 1.0, then L01
118+
if not await self._do_hello(LocalProtocolVersion.V1):
119+
if not await self._do_hello(LocalProtocolVersion.L01):
120+
raise RoborockException("Failed to connect to device with any known protocol")
121+
54122
@property
55123
def is_connected(self) -> bool:
56124
"""Check if the channel is currently connected."""
@@ -113,6 +181,29 @@ async def publish(self, message: RoborockMessage) -> None:
113181
logging.exception("Uncaught error sending command")
114182
raise RoborockException(f"Failed to send message: {message}") from err
115183

184+
async def send_message(
185+
self,
186+
roborock_message: RoborockMessage,
187+
request_id: int,
188+
response_protocol: int,
189+
) -> RoborockMessage:
190+
"""Send a raw message and wait for a raw response."""
191+
future: asyncio.Future[RoborockMessage] = asyncio.Future()
192+
193+
def find_response(response_message: RoborockMessage) -> None:
194+
if response_message.protocol == response_protocol and response_message.seq == request_id:
195+
future.set_result(response_message)
196+
197+
unsub = await self.subscribe(find_response)
198+
try:
199+
await self.publish(roborock_message)
200+
return await asyncio.wait_for(future, timeout=_TIMEOUT)
201+
except TimeoutError as ex:
202+
future.cancel()
203+
raise RoborockException(f"Command timed out after {_TIMEOUT}s") from ex
204+
finally:
205+
unsub()
206+
116207

117208
# This module provides a factory function to create LocalChannel instances.
118209
#

roborock/devices/v1_channel.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ async def _local_connect(self, *, use_cache: bool = True) -> None:
212212
raise RoborockException(f"Error connecting to local device {self._device_uid}: {e}") from e
213213
# Wire up the new channel
214214
self._local_channel = local_channel
215+
await self._local_channel.hello()
215216
self._local_rpc_channel = create_local_rpc_channel(self._local_channel)
216217
self._local_unsub = await self._local_channel.subscribe(self._on_local_message)
217218
_LOGGER.info("Successfully connected to local device %s", self._device_uid)

roborock/mqtt/roborock_session.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ async def _run_task(self, start_future: asyncio.Future[None] | None) -> None:
106106
# Reset backoff once we've successfully connected
107107
self._backoff = MIN_BACKOFF_INTERVAL
108108
self._healthy = True
109+
_LOGGER.info("MQTT Session connected.")
109110
if start_future:
110111
start_future.set_result(None)
111112
start_future = None

roborock/protocols/v1_protocol.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import time
1212
from collections.abc import Callable
1313
from dataclasses import dataclass, field
14+
from enum import StrEnum
1415
from typing import Any
1516

1617
from roborock.data import RRiot
@@ -32,6 +33,13 @@
3233
ParamsType = list | dict | int | None
3334

3435

36+
class LocalProtocolVersion(StrEnum):
37+
"""Supported local protocol versions. Different from vacuum protocol versions."""
38+
39+
L01 = "L01"
40+
V1 = "1.0"
41+
42+
3543
@dataclass(frozen=True, kw_only=True)
3644
class SecurityData:
3745
"""Security data included in the request for some V1 commands."""

roborock/version_1_apis/roborock_local_client_v1.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,19 @@
33
from asyncio import Lock, TimerHandle, Transport, get_running_loop
44
from collections.abc import Callable
55
from dataclasses import dataclass
6-
from enum import StrEnum
76

87
from .. import CommandVacuumError, DeviceData, RoborockCommand
98
from ..api import RoborockClient
109
from ..exceptions import RoborockConnectionException, RoborockException, VacuumError
1110
from ..protocol import create_local_decoder, create_local_encoder
12-
from ..protocols.v1_protocol import RequestMessage
11+
from ..protocols.v1_protocol import LocalProtocolVersion, RequestMessage
1312
from ..roborock_message import RoborockMessage, RoborockMessageProtocol
1413
from ..util import RoborockLoggerAdapter, get_next_int
1514
from .roborock_client_v1 import CLOUD_REQUIRED, RoborockClientV1
1615

1716
_LOGGER = logging.getLogger(__name__)
1817

1918

20-
class LocalProtocolVersion(StrEnum):
21-
"""Supported local protocol versions. Different from vacuum protocol versions."""
22-
23-
L01 = "L01"
24-
V1 = "1.0"
25-
26-
2719
@dataclass
2820
class _LocalProtocol(asyncio.Protocol):
2921
"""Callbacks for the Roborock local client transport."""

0 commit comments

Comments
 (0)