Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion roborock/data/v1/v1_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ class AppInitStatus(RoborockBase):
new_feature_info_str: str
new_feature_info_2: int | None = None
carriage_type: int | None = None
dsp_version: int | None = None
Comment thread
Lash-L marked this conversation as resolved.
dsp_version: str | None = None


@dataclass
Expand Down
103 changes: 97 additions & 6 deletions roborock/devices/local_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@

from roborock.callbacks import CallbackList, decoder_callback
from roborock.exceptions import RoborockConnectionException, RoborockException
from roborock.protocol import Decoder, Encoder, create_local_decoder, create_local_encoder
from roborock.roborock_message import RoborockMessage
from roborock.protocol import create_local_decoder, create_local_encoder
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol

from ..protocols.v1_protocol import LocalProtocolVersion
from ..util import get_next_int
from .channel import Channel

_LOGGER = logging.getLogger(__name__)
_PORT = 58867
_TIMEOUT = 10.0
Comment thread
Lash-L marked this conversation as resolved.
Outdated


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

def __init__(self, host: str, local_key: str):
def __init__(self, host: str, local_key: str, local_protocol_version: LocalProtocolVersion | None = None):
Comment thread
Lash-L marked this conversation as resolved.
Outdated
self._host = host
self._transport: asyncio.Transport | None = None
self._protocol: _LocalProtocol | None = None
self._subscribers: CallbackList[RoborockMessage] = CallbackList(_LOGGER)
self._is_connected = False

self._decoder: Decoder = create_local_decoder(local_key)
self._encoder: Encoder = create_local_encoder(local_key)
self._local_key = local_key
Comment thread
Lash-L marked this conversation as resolved.
Outdated
self._local_protocol_version = local_protocol_version
self._connect_nonce = get_next_int(10000, 32767)
self._ack_nonce: int | None = None
self._update_encoder_decoder()

def _update_encoder_decoder(self):
self._encoder = create_local_encoder(
Comment thread
Lash-L marked this conversation as resolved.
local_key=self._local_key, connect_nonce=self._connect_nonce, ack_nonce=self._ack_nonce
)
self._decoder = create_local_decoder(
local_key=self._local_key, connect_nonce=self._connect_nonce, ack_nonce=self._ack_nonce
)
# Callback to decode messages and dispatch to subscribers
self._data_received: Callable[[bytes], None] = decoder_callback(self._decoder, self._subscribers, _LOGGER)

async def _do_hello(self, local_protocol_version: LocalProtocolVersion) -> bool:
Comment thread
Lash-L marked this conversation as resolved.
Outdated
"""Perform the initial handshaking."""
_LOGGER.debug(
"Attempting to use the %s protocol for client %s...",
local_protocol_version,
self._host,
)
request = RoborockMessage(
protocol=RoborockMessageProtocol.HELLO_REQUEST,
version=local_protocol_version.encode(),
random=self._connect_nonce,
seq=1,
)
try:
response = await self.send_message(
roborock_message=request,
request_id=request.seq,
response_protocol=RoborockMessageProtocol.HELLO_RESPONSE,
)
self._ack_nonce = response.random
self._local_protocol_version = local_protocol_version
self._update_encoder_decoder()

_LOGGER.debug(
"Client %s speaks the %s protocol.",
self._host,
local_protocol_version,
)
return True
except RoborockException as e:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a follow up: Is there a very specific specific error code or raised when the protocol is not supported? Would be nice if we could narrow down to a specific exception as right now the transport code is simply catching except Exception as err which is probably overly broad.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No response from the vac when protocol is not supported. So it just gives a timeout

_LOGGER.debug(
"Client %s did not respond or does not speak the %s protocol. %s",
self._host,
local_protocol_version,
e,
)
return False

async def hello(self):
Comment thread
Lash-L marked this conversation as resolved.
Outdated
"""Send hello to the device to negotiate protocol."""
if self._local_protocol_version:
# version is forced - try it first, if it fails, try the opposite
if not await self._do_hello(self._local_protocol_version):
if not await self._do_hello(
LocalProtocolVersion.V1
if self._local_protocol_version is not LocalProtocolVersion.V1
else LocalProtocolVersion.L01
):
raise RoborockException("Failed to connect to device with any known protocol")
else:
# try 1.0, then L01
if not await self._do_hello(LocalProtocolVersion.V1):
if not await self._do_hello(LocalProtocolVersion.L01):
raise RoborockException("Failed to connect to device with any known protocol")

@property
def is_connected(self) -> bool:
"""Check if the channel is currently connected."""
Expand Down Expand Up @@ -113,6 +181,29 @@ async def publish(self, message: RoborockMessage) -> None:
logging.exception("Uncaught error sending command")
raise RoborockException(f"Failed to send message: {message}") from err

async def send_message(
Comment thread
Lash-L marked this conversation as resolved.
Outdated
self,
roborock_message: RoborockMessage,
request_id: int,
response_protocol: int,
) -> RoborockMessage:
"""Send a raw message and wait for a raw response."""
future: asyncio.Future[RoborockMessage] = asyncio.Future()

def find_response(response_message: RoborockMessage) -> None:
if response_message.protocol == response_protocol and response_message.seq == request_id:
future.set_result(response_message)

unsub = await self.subscribe(find_response)
try:
await self.publish(roborock_message)
return await asyncio.wait_for(future, timeout=_TIMEOUT)
except TimeoutError as ex:
future.cancel()
raise RoborockException(f"Command timed out after {_TIMEOUT}s") from ex
finally:
unsub()


# This module provides a factory function to create LocalChannel instances.
#
Expand Down
1 change: 1 addition & 0 deletions roborock/devices/v1_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ async def _local_connect(self, *, use_cache: bool = True) -> None:
raise RoborockException(f"Error connecting to local device {self._device_uid}: {e}") from e
# Wire up the new channel
self._local_channel = local_channel
await self._local_channel.hello()
Comment thread
Lash-L marked this conversation as resolved.
Outdated
self._local_rpc_channel = create_local_rpc_channel(self._local_channel)
self._local_unsub = await self._local_channel.subscribe(self._on_local_message)
_LOGGER.info("Successfully connected to local device %s", self._device_uid)
Expand Down
1 change: 1 addition & 0 deletions roborock/mqtt/roborock_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ async def _run_task(self, start_future: asyncio.Future[None] | None) -> None:
# Reset backoff once we've successfully connected
self._backoff = MIN_BACKOFF_INTERVAL
self._healthy = True
_LOGGER.info("MQTT Session connected.")
Comment thread
Lash-L marked this conversation as resolved.
if start_future:
start_future.set_result(None)
start_future = None
Expand Down
8 changes: 8 additions & 0 deletions roborock/protocols/v1_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import time
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import StrEnum
from typing import Any

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


class LocalProtocolVersion(StrEnum):
"""Supported local protocol versions. Different from vacuum protocol versions."""

L01 = "L01"
V1 = "1.0"


@dataclass(frozen=True, kw_only=True)
class SecurityData:
"""Security data included in the request for some V1 commands."""
Expand Down
10 changes: 1 addition & 9 deletions roborock/version_1_apis/roborock_local_client_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,19 @@
from asyncio import Lock, TimerHandle, Transport, get_running_loop
from collections.abc import Callable
from dataclasses import dataclass
from enum import StrEnum

from .. import CommandVacuumError, DeviceData, RoborockCommand
from ..api import RoborockClient
from ..exceptions import RoborockConnectionException, RoborockException, VacuumError
from ..protocol import create_local_decoder, create_local_encoder
from ..protocols.v1_protocol import RequestMessage
from ..protocols.v1_protocol import LocalProtocolVersion, RequestMessage
from ..roborock_message import RoborockMessage, RoborockMessageProtocol
from ..util import RoborockLoggerAdapter, get_next_int
from .roborock_client_v1 import CLOUD_REQUIRED, RoborockClientV1

_LOGGER = logging.getLogger(__name__)


class LocalProtocolVersion(StrEnum):
"""Supported local protocol versions. Different from vacuum protocol versions."""

L01 = "L01"
V1 = "1.0"


@dataclass
class _LocalProtocol(asyncio.Protocol):
"""Callbacks for the Roborock local client transport."""
Expand Down
Loading