-
Notifications
You must be signed in to change notification settings - Fork 51
feat: Implement L01 protocol #487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
da21a0f
95308ed
99d3252
67f9ed6
d7ce9ba
8d8ff89
f926359
89a0eed
b4b6730
3a5874e
3362aac
8af494c
fbaf426
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
import asyncio | ||
import json | ||
import logging | ||
import math | ||
import time | ||
from asyncio import Lock, TimerHandle, Transport, get_running_loop | ||
from collections.abc import Callable | ||
from dataclasses import dataclass | ||
|
@@ -12,25 +15,12 @@ | |
from ..protocol import Decoder, Encoder, create_local_decoder, create_local_encoder | ||
from ..protocols.v1_protocol import RequestMessage | ||
from ..roborock_message import RoborockMessage, RoborockMessageProtocol | ||
from ..util import RoborockLoggerAdapter | ||
from ..util import RoborockLoggerAdapter, get_next_int | ||
from .roborock_client_v1 import CLOUD_REQUIRED, RoborockClientV1 | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
_HELLO_REQUEST_MESSAGE = RoborockMessage( | ||
protocol=RoborockMessageProtocol.HELLO_REQUEST, | ||
seq=1, | ||
random=22, | ||
) | ||
|
||
_PING_REQUEST_MESSAGE = RoborockMessage( | ||
protocol=RoborockMessageProtocol.PING_REQUEST, | ||
seq=2, | ||
random=23, | ||
) | ||
|
||
|
||
@dataclass | ||
class _LocalProtocol(asyncio.Protocol): | ||
"""Callbacks for the Roborock local client transport.""" | ||
|
@@ -50,7 +40,7 @@ def connection_lost(self, exc: Exception | None) -> None: | |
class RoborockLocalClientV1(RoborockClientV1, RoborockClient): | ||
"""Roborock local client for v1 devices.""" | ||
|
||
def __init__(self, device_data: DeviceData, queue_timeout: int = 4): | ||
def __init__(self, device_data: DeviceData, queue_timeout: int = 4, version: str | None = None): | ||
Lash-L marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
"""Initialize the Roborock local client.""" | ||
if device_data.host is None: | ||
raise RoborockException("Host is required") | ||
|
@@ -63,8 +53,14 @@ def __init__(self, device_data: DeviceData, queue_timeout: int = 4): | |
RoborockClientV1.__init__(self, device_data, security_data=None) | ||
RoborockClient.__init__(self, device_data) | ||
self._local_protocol = _LocalProtocol(self._data_received, self._connection_lost) | ||
self._encoder: Encoder = create_local_encoder(device_data.device.local_key) | ||
self._decoder: Decoder = create_local_decoder(device_data.device.local_key) | ||
self._version = version | ||
self._connect_nonce: int | None = None | ||
self._ack_nonce: int | None = None | ||
if version == "L01": | ||
self._set_l01_encoder_decoder() | ||
else: | ||
self._encoder: Encoder = create_local_encoder(device_data.device.local_key) | ||
self._decoder: Decoder = create_local_decoder(device_data.device.local_key) | ||
self.queue_timeout = queue_timeout | ||
self._logger = RoborockLoggerAdapter(device_data.device.name, _LOGGER) | ||
|
||
|
@@ -121,20 +117,58 @@ async def async_disconnect(self) -> None: | |
async with self._mutex: | ||
self._sync_disconnect() | ||
|
||
async def hello(self): | ||
def _set_l01_encoder_decoder(self): | ||
Lash-L marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
"""Tell the system to use the L01 encoder/decoder.""" | ||
self._encoder = create_local_encoder(self.device_info.device.local_key, self._connect_nonce, self._ack_nonce) | ||
self._decoder = create_local_decoder(self.device_info.device.local_key, self._connect_nonce, self._ack_nonce) | ||
|
||
async def _do_hello(self, version: str) -> bool: | ||
"""Perform the initial handshaking.""" | ||
self._logger.debug(f"Attempting to use the {version} protocol for client {self.device_info.device.duid}...") | ||
Lash-L marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
self._connect_nonce = get_next_int(10000, 32767) | ||
request = RoborockMessage( | ||
protocol=RoborockMessageProtocol.HELLO_REQUEST, | ||
version=version.encode(), | ||
random=self._connect_nonce, | ||
seq=1, | ||
) | ||
try: | ||
return await self._send_message( | ||
roborock_message=_HELLO_REQUEST_MESSAGE, | ||
request_id=_HELLO_REQUEST_MESSAGE.seq, | ||
response = await self._send_message( | ||
roborock_message=request, | ||
request_id=request.seq, | ||
response_protocol=RoborockMessageProtocol.HELLO_RESPONSE, | ||
) | ||
except Exception as e: | ||
self._logger.error(e) | ||
if response.version.decode() == "L01": | ||
self._ack_nonce = response.random | ||
self._set_l01_encoder_decoder() | ||
|
||
self._version = version | ||
self._logger.debug(f"Client {self.device_info.device.duid} speaks the {version} protocol.") | ||
return True | ||
except RoborockException as e: | ||
self._logger.debug( | ||
f"Client {self.device_info.device.duid} did not respond or does not speak the {version} protocol. {e}" | ||
) | ||
return False | ||
|
||
async def hello(self): | ||
"""Send hello to the device to negotiate protocol.""" | ||
if self._version: | ||
# version is forced | ||
if not await self._do_hello(self._version): | ||
raise RoborockException(f"Failed to connect to device with protocol {self._version}") | ||
else: | ||
# try 1.0, then L01 | ||
if not await self._do_hello("1.0"): | ||
if not await self._do_hello("L01"): | ||
raise RoborockException("Failed to connect to device with any known protocol") | ||
|
||
async def ping(self) -> None: | ||
ping_message = RoborockMessage( | ||
protocol=RoborockMessageProtocol.PING_REQUEST, | ||
) | ||
await self._send_message( | ||
roborock_message=_PING_REQUEST_MESSAGE, | ||
request_id=_PING_REQUEST_MESSAGE.seq, | ||
roborock_message=ping_message, | ||
request_id=ping_message.seq, | ||
response_protocol=RoborockMessageProtocol.PING_RESPONSE, | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need to set ping request statically like we did. I know we talked about this before but I wasn't fully sure. This works |
||
|
||
|
@@ -153,6 +187,33 @@ async def _send_command( | |
): | ||
if method in CLOUD_REQUIRED: | ||
raise RoborockException(f"Method {method} is not supported over local connection") | ||
if self._version == "L01": | ||
|
||
request_id = get_next_int(10000, 999999) | ||
dps_payload = { | ||
"id": request_id, | ||
"method": method, | ||
"params": params, | ||
} | ||
ts = math.floor(time.time()) | ||
payload = { | ||
"dps": {str(RoborockMessageProtocol.RPC_REQUEST.value): json.dumps(dps_payload, separators=(",", ":"))}, | ||
"t": ts, | ||
} | ||
roborock_message = RoborockMessage( | ||
protocol=RoborockMessageProtocol.GENERAL_REQUEST, | ||
payload=json.dumps(payload, separators=(",", ":")).encode("utf-8"), | ||
version=self._version.encode(), | ||
timestamp=ts, | ||
) | ||
self._logger.debug("Building message id %s for method %s", request_id, method) | ||
return await self._send_message( | ||
roborock_message, | ||
request_id=request_id, | ||
response_protocol=RoborockMessageProtocol.GENERAL_REQUEST, | ||
method=method, | ||
params=params, | ||
) | ||
|
||
request_message = RequestMessage(method=method, params=params) | ||
roborock_message = request_message.encode_message(RoborockMessageProtocol.GENERAL_REQUEST) | ||
self._logger.debug("Building message id %s for method %s", request_message.request_id, method) | ||
|
Uh oh!
There was an error while loading. Please reload this page.