Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
6 changes: 3 additions & 3 deletions roborock/devices/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ For each V1 command:
| **RPC Abstraction** | `RpcChannel` with strategies | Helper functions |
| **Strategy Pattern** | ✅ Multi-strategy (Local → MQTT) | ❌ Direct MQTT only |
| **Health Manager** | ✅ Tracks local/MQTT health | ❌ Not needed |
| **Code Location** | `v1_channel.py` | `a01_channel.py`, `b01_channel.py` |
| **Code Location** | `v1_channel.py` | `a01_channel.py`, `b01_q7_channel.py` |

#### Health Management (V1 Only)

Expand Down Expand Up @@ -572,7 +572,7 @@ roborock/
│ ├── local_channel.py # Local TCP channel implementation
│ ├── v1_channel.py # V1 protocol channel with RPC strategies
│ ├── a01_channel.py # A01 protocol helpers
│ ├── b01_channel.py # B01 protocol helpers
│ ├── b01_q7_channel.py # B01 Q7 protocol helpers
│ └── traits/ # Device-specific command traits
│ └── v1/ # V1 device traits
│ ├── __init__.py # Trait initialization
Expand All @@ -585,7 +585,7 @@ roborock/
├── protocols/ # Protocol encoders/decoders
│ ├── v1_protocol.py # V1 JSON RPC protocol
│ ├── a01_protocol.py # A01 protocol
│ ├── b01_protocol.py # B01 protocol
│ ├── b01_q7_protocol.py # B01 Q7 protocol
│ └── ...
└── data/ # Data containers and mappings
├── containers.py # Status, HomeData, etc.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@
from typing import Any

from roborock.exceptions import RoborockException
from roborock.protocols.b01_protocol import (
CommandType,
ParamsType,
from roborock.protocols.b01_q7_protocol import (
Q7RequestMessage,
decode_rpc_response,
encode_mqtt_payload,
)
from roborock.roborock_message import RoborockMessage
from roborock.util import get_next_int

from .mqtt_channel import MqttChannel

Expand All @@ -25,20 +23,11 @@

async def send_decoded_command(
mqtt_channel: MqttChannel,
dps: int,
command: CommandType,
params: ParamsType,
request_message: Q7RequestMessage,
) -> dict[str, Any] | None:
"""Send a command on the MQTT channel and get a decoded response."""
msg_id = str(get_next_int(100000000000, 999999999999))
_LOGGER.debug(
"Sending B01 MQTT command: dps=%s method=%s msg_id=%s params=%s",
dps,
command,
msg_id,
params,
)
roborock_message = encode_mqtt_payload(dps, command, params, msg_id)
_LOGGER.debug("Sending B01 MQTT command: %s", request_message)
roborock_message = encode_mqtt_payload(request_message)
future: asyncio.Future[Any] = asyncio.get_running_loop().create_future()

def find_response(response_message: RoborockMessage) -> None:
Expand All @@ -48,13 +37,12 @@ def find_response(response_message: RoborockMessage) -> None:
except RoborockException as ex:
_LOGGER.debug(
"Failed to decode B01 RPC response (expecting method=%s msg_id=%s): %s: %s",
command,
msg_id,
request_message.command,
request_message.msg_id,
response_message,
ex,
)
return

for dps_value in decoded_dps.values():
# valid responses are JSON strings wrapped in the dps value
if not isinstance(dps_value, str):
Expand All @@ -66,29 +54,22 @@ def find_response(response_message: RoborockMessage) -> None:
except (json.JSONDecodeError, TypeError):
_LOGGER.debug("Received unexpected response: %s", dps_value)
continue

if isinstance(inner, dict) and inner.get("msgId") == msg_id:
if isinstance(inner, dict) and inner.get("msgId") == str(request_message.msg_id):
_LOGGER.debug("Received query response: %s", inner)
# Check for error code (0 = success, non-zero = error)
code = inner.get("code", 0)
if code != 0:
error_msg = (
f"B01 command failed with code {code} "
f"(method={command}, msg_id={msg_id}, dps={dps}, params={params})"
)
error_msg = f"B01 command failed with code {code} ({request_message})"
_LOGGER.debug("B01 error response: %s", error_msg)
if not future.done():
future.set_exception(RoborockException(error_msg))
return
data = inner.get("data")
# All get commands should be dicts
if command.endswith(".get") and not isinstance(data, dict):
if request_message.command.endswith(".get") and not isinstance(data, dict):
if not future.done():
future.set_exception(
RoborockException(
f"Unexpected data type for response "
f"(method={command}, msg_id={msg_id}, dps={dps}, params={params})"
)
RoborockException(f"Unexpected data type for response {data} ({request_message})")
)
return
if not future.done():
Expand All @@ -101,27 +82,19 @@ def find_response(response_message: RoborockMessage) -> None:
await mqtt_channel.publish(roborock_message)
return await asyncio.wait_for(future, timeout=_TIMEOUT)
except TimeoutError as ex:
raise RoborockException(
f"B01 command timed out after {_TIMEOUT}s (method={command}, msg_id={msg_id}, dps={dps}, params={params})"
) from ex
raise RoborockException(f"B01 command timed out after {_TIMEOUT}s ({request_message})") from ex
except RoborockException as ex:
_LOGGER.warning(
"Error sending B01 decoded command (method=%s msg_id=%s dps=%s params=%s): %s",
command,
msg_id,
dps,
params,
"Error sending B01 decoded command (%ss): %s",
request_message,
ex,
)
raise

except Exception as ex:
_LOGGER.exception(
"Error sending B01 decoded command (method=%s msg_id=%s dps=%s params=%s): %s",
command,
msg_id,
dps,
params,
"Error sending B01 decoded command (%ss): %s",
request_message,
ex,
)
raise
Expand Down
7 changes: 3 additions & 4 deletions roborock/devices/traits/b01/q7/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
SCWindMapping,
WaterLevelMapping,
)
from roborock.devices.b01_channel import CommandType, ParamsType, send_decoded_command
from roborock.devices.b01_q7_channel import send_decoded_command
from roborock.devices.mqtt_channel import MqttChannel
from roborock.devices.traits import Trait
from roborock.protocols.b01_q7_protocol import CommandType, ParamsType, Q7RequestMessage
from roborock.roborock_message import RoborockB01Props
from roborock.roborock_typing import RoborockB01Q7Methods

Expand Down Expand Up @@ -104,9 +105,7 @@ async def send(self, command: CommandType, params: ParamsType) -> Any:
"""Send a command to the device."""
return await send_decoded_command(
self._channel,
dps=10000,
command=command,
params=params,
Q7RequestMessage(dps=10000, command=command, params=params),
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import logging
from dataclasses import dataclass, field
from typing import Any

from Crypto.Cipher import AES
Expand All @@ -13,6 +14,7 @@
RoborockMessage,
RoborockMessageProtocol,
)
from roborock.util import get_next_int

_LOGGER = logging.getLogger(__name__)

Expand All @@ -21,20 +23,32 @@
ParamsType = list | dict | int | None


def encode_mqtt_payload(dps: int, command: CommandType, params: ParamsType, msg_id: str) -> RoborockMessage:
"""Encode payload for B01 commands over MQTT."""
dps_data = {
"dps": {
dps: {
"method": str(command),
"msgId": msg_id,
@dataclass
class Q7RequestMessage:
"""Data class for B01 Q7 request message."""

dps: int
command: CommandType
params: ParamsType
msg_id: int = field(default_factory=lambda: get_next_int(100000000000, 999999999999))

def to_dps_value(self) -> dict[int, Any]:
"""Return the 'dps' payload dictionary."""
return {
self.dps: {
"method": str(self.command),
"msgId": str(self.msg_id),
# Important: some B01 methods use an empty object `{}` (not `[]`) for
# "no params", and some setters legitimately send `0` which is falsy.
# Only default to `[]` when params is actually None.
"params": params if params is not None else [],
"params": self.params if self.params is not None else [],
}
}
}


def encode_mqtt_payload(request: Q7RequestMessage) -> RoborockMessage:
"""Encode payload for B01 commands over MQTT."""
dps_data = {"dps": request.to_dps_value()}
payload = pad(json.dumps(dps_data).encode("utf-8"), AES.block_size)
return RoborockMessage(
protocol=RoborockMessageProtocol.RPC_REQUEST,
Expand Down
Empty file.
Loading
Loading