Skip to content

Commit 691c569

Browse files
committed
chore: add design documentation and copilot instructions
1 parent e912dac commit 691c569

File tree

2 files changed

+241
-0
lines changed

2 files changed

+241
-0
lines changed

.github/copilot-instructions.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# GitHub Copilot Instructions for python-roborock
2+
3+
This document provides context and guidelines for GitHub Copilot to generate high-quality code for the `python-roborock` project.
4+
5+
## Project Overview
6+
7+
`python-roborock` is an asynchronous Python library for controlling Roborock vacuum cleaners. It supports communicating with devices via both Roborock's Cloud (MQTT) and local network (TCP) protocols.
8+
9+
## Key Documentation
10+
11+
* **Architecture Design**: `roborock/devices/DESIGN.md` - Detailed explanation of the system architecture, communication channels, and protocol details.
12+
* **Device Discovery**: `roborock/devices/README.md` - Information about the device discovery lifecycle, login, and home data.
13+
14+
## Tech Stack
15+
16+
* **Language**: Python 3.11+
17+
* **Async Framework**: `asyncio`
18+
* **Web Requests**: `aiohttp`
19+
* **MQTT**: `aiomqtt` (v2+)
20+
* **Binary Parsing**: `construct`
21+
* **Encryption**: `pycryptodome`
22+
* **Testing**: `pytest`, `pytest-asyncio`
23+
* **Linting/Formatting**: `ruff`
24+
25+
## Coding Standards
26+
27+
### 1. Typing
28+
* **Strict Typing**: All functions and methods must have type hints.
29+
* **Generics**: Use `list[str]` instead of `List[str]`, `dict[str, Any]` instead of `Dict[str, Any]`, etc.
30+
* **Optional**: Use `str | None` instead of `Optional[str]`.
31+
32+
### 2. Asynchronous Programming
33+
* **Async/Await**: Use `async def` and `await` for all I/O bound operations.
34+
* **Context Managers**: Use `async with` for managing resources like network sessions and locks.
35+
* **Concurrency**: Use `asyncio.gather` for concurrent operations. Avoid blocking calls in async functions.
36+
37+
### 3. Documentation
38+
* **Docstrings**: Use Google-style docstrings for all modules, classes, and functions.
39+
* **Comments**: Comment complex logic, especially protocol parsing and encryption details.
40+
41+
### 4. Error Handling
42+
* **Base Exception**: All custom exceptions must inherit from `roborock.exceptions.RoborockException`.
43+
* **Specific Exceptions**: Use specific exceptions like `RoborockTimeout`, `RoborockConnectionException` where appropriate.
44+
* **Wrapping**: Wrap external library exceptions (e.g., `aiohttp.ClientError`, `aiomqtt.MqttError`) in `RoborockException` subclasses to provide a consistent API surface.
45+
46+
## Architecture & Patterns
47+
48+
### 1. Device Model
49+
* **`RoborockDevice`**: The base class for all devices.
50+
* **Traits**: Functionality is composed using traits (e.g., `FanSpeedTrait`, `CleaningTrait`) mixed into device classes.
51+
* **Discovery**: `DeviceManager` handles authentication and device discovery.
52+
53+
### 2. Communication Channels
54+
* **`Channel` Protocol**: Defines the interface for communicating with a device.
55+
* **`MqttChannel`**: Handles communication via the Roborock MQTT broker.
56+
* **`LocalChannel`**: Handles direct TCP communication with the device.
57+
* **`V1Channel`**: A composite channel that manages both MQTT and Local connections, implementing fallback logic.
58+
59+
### 3. Protocol Parsing
60+
* **`construct` Library**: Use `construct` structs and adapters for defining binary message formats.
61+
* **Encryption**: Protocol encryption (AES-ECB, AES-GCM, AES-CBC) is handled in `roborock/protocol.py` and `roborock/devices/local_channel.py`.
62+
63+
## Testing Guidelines
64+
65+
* **Framework**: Use `pytest` with `pytest-asyncio`.
66+
* **Fixtures**: Use fixtures defined in `tests/conftest.py` for common setup (e.g., `mock_mqtt_client`, `mock_local_client`).
67+
* **Mocking**: Prefer `unittest.mock.AsyncMock` for mocking async methods.
68+
* **Network Isolation**: Tests should not make real network requests. Use `aioresponses` for HTTP mocking and custom mocks for MQTT/TCP.
69+
70+
## Data Classes & Serialization
71+
72+
* **`RoborockBase`**: The base class for all data models (`roborock/data/containers.py`).
73+
* **Automatic Conversion**: `RoborockBase` handles the conversion between the API's camelCase JSON keys and the Python dataclass's snake_case fields.
74+
* **Deserialization**: Use `MyClass.from_dict(data)` to instantiate objects from API responses.
75+
* **Nesting**: `RoborockBase` supports nested dataclasses, lists, and enums automatically.
76+
77+
## Example: Adding a New Trait / Command
78+
79+
Functionality is organized into "Traits". To add a new command (for v1 devices), follow these steps:
80+
81+
1. **Define Command**: Add the command string to the `RoborockCommand` enum in `roborock/roborock_typing.py`.
82+
2. **Create Data Model**: Define a dataclass inheriting from `RoborockBase` (or `RoborockValueBase` for single values) to represent the state.
83+
3. **Create Trait**: For v1, create a class inheriting from your data model and `V1TraitMixin`.
84+
* Set the `command` class variable to your `RoborockCommand`.
85+
* Add methods to perform actions using `self.rpc_channel.send_command()`.
86+
4. **Register Trait**: Add the trait to `PropertiesApi` in `roborock/devices/traits/v1/__init__.py`.
87+
88+
```python
89+
# 1. Define Data Model
90+
@dataclass
91+
class SoundVolume(RoborockValueBase):
92+
volume: int | None = field(default=None, metadata={"roborock_value": True})
93+
94+
# 2. Define Trait
95+
class SoundVolumeTrait(SoundVolume, V1TraitMixin):
96+
command = RoborockCommand.GET_SOUND_VOLUME
97+
98+
async def set_volume(self, volume: int) -> None:
99+
# 3. Send Command
100+
await self.rpc_channel.send_command(RoborockCommand.CHANGE_SOUND_VOLUME, params=[volume])
101+
# Optimistic update
102+
self.volume = volume
103+
```
104+
105+
## Example: Error Handling
106+
107+
```python
108+
from roborock.exceptions import RoborockException, RoborockTimeout
109+
110+
async def my_operation(self) -> None:
111+
try:
112+
await self._channel.send_command("some_command")
113+
except TimeoutError as err:
114+
raise RoborockTimeout("Operation timed out") from err
115+
except Exception as err:
116+
raise RoborockException(f"Operation failed: {err}") from err
117+
```

roborock/devices/DESIGN.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Roborock Python Library Design
2+
3+
This document outlines the current architecture and design of the `python-roborock` library.
4+
5+
## High-Level Architecture
6+
7+
The library is designed to communicate with Roborock devices via two primary transport mechanisms:
8+
1. **Cloud (MQTT)**: Uses the Roborock cloud infrastructure.
9+
2. **Local (TCP)**: Direct connection to the device on the local network.
10+
11+
The core components are:
12+
* **Device Manager**: Handles discovery and lifecycle of devices.
13+
* **Web API**: Fetches user and home configuration data.
14+
* **Device Model**: Represents a physical device and its capabilities.
15+
* **Communication Channels**: Abstracts the transport layer (MQTT vs Local).
16+
17+
## Component Detail
18+
19+
### 1. Device Discovery (`DeviceManager`)
20+
21+
The `DeviceManager` (`roborock/devices/device_manager.py`) is the entry point.
22+
* **Input**: `UserParams` (credentials).
23+
* **Process**:
24+
1. Authenticates via `UserWebApiClient`.
25+
2. Fetches `HomeData` (list of devices, products, rooms).
26+
3. Iterates through devices and uses a factory pattern (`device_creator`) to instantiate specific `RoborockDevice` subclasses based on the protocol version (`V1`, `A01`, `B01`).
27+
* **Output**: A list of `RoborockDevice` instances.
28+
29+
### 2. Device Model (`RoborockDevice`)
30+
31+
The `RoborockDevice` (`roborock/devices/device.py`) is the base class for all devices.
32+
* **Composition**:
33+
* `HomeDataDevice`: Static info (DUID, name).
34+
* `HomeDataProduct`: Model info.
35+
* `Channel`: The communication pipe.
36+
* `Traits`: Capabilities mixed in via `TraitsMixin`.
37+
* **Traits System**: Devices expose functionality through traits (e.g., `FanSpeedTrait`, `CleaningTrait`). This allows for a unified interface across different device protocols.
38+
39+
### 3. Communication Layer
40+
41+
The library uses a layered channel architecture to abstract the differences between MQTT and Local connections.
42+
43+
#### Channels (`Channel` Protocol)
44+
* **`MqttChannel`**: Wraps the `MqttSession`. Handles topic construction (`rr/m/i/...`) and message encoding/decoding using the device's `local_key`.
45+
* **`LocalChannel`**: Manages a direct TCP connection (port 58867). Handles the custom handshake/heartbeat protocol and message framing.
46+
* **`V1Channel`**: A "smart" channel for V1 devices. It holds both an `MqttChannel` and a `LocalChannel`. It manages the complexity of:
47+
* Fetching `NetworkingInfo` (to get the local IP).
48+
* Establishing the local connection.
49+
* Fallback logic (preferring local, falling back to MQTT).
50+
51+
#### RPC Abstraction (`V1RpcChannel`)
52+
Above the raw byte-oriented `Channel`, the `V1RpcChannel` provides a command-oriented interface (`send_command`).
53+
* **`PayloadEncodedV1RpcChannel`**: Handles serialization of RPC commands (JSON payload -> Encrypted Bytes).
54+
* **`PickFirstAvailable`**: A composite channel that attempts to send a command via the Local channel first, and falls back to MQTT if the local connection is unavailable.
55+
56+
### 4. Session Management
57+
58+
* **`RoborockMqttSession`**: Manages the persistent connection to the Roborock MQTT broker. It handles authentication, keepalives, and dispatching incoming messages to the appropriate `MqttChannel` based on topic.
59+
* **`LocalSession`**: Currently a factory for creating `LocalChannel` instances.
60+
61+
## Protocol Details
62+
63+
The library handles two variations of the underlying wire protocol depending on the transport.
64+
65+
#### Message Framing
66+
* **Local (TCP)**: Messages are **length-prefixed**. A 4-byte integer at the start of each packet indicates the total length of the message. This is necessary for framing over the streaming TCP connection.
67+
* **MQTT**: Messages are **raw**. The MQTT packet boundaries themselves serve as the framing mechanism, so no length prefix is added.
68+
69+
#### MQTT Authentication
70+
The connection to the Roborock MQTT broker requires specific credentials derived from the user's `rriot` data (obtained during login):
71+
* **Username**: Derived from `MD5(rriot.u + ":" + rriot.k)`.
72+
* **Password**: Derived from `MD5(rriot.s + ":" + rriot.k)`.
73+
* **Topics**:
74+
* Command (Publish): `rr/m/i/{rriot.u}/{username}/{duid}`
75+
* Response (Subscribe): `rr/m/o/{rriot.u}/{username}/{duid}`
76+
77+
#### Local Handshake
78+
1. **Negotiation**: The client attempts to connect using a list of supported versions (currently `V1` and `L01`).
79+
2. **Hello Request**: Client sends a `HELLO_REQUEST` message containing the version string and a `connect_nonce`.
80+
3. **Hello Response**: Device responds with `HELLO_RESPONSE`. The client extracts the `ack_nonce` (from the message's `random` field).
81+
4. **Session Setup**: The `local_key`, `connect_nonce`, and `ack_nonce` are used to configure the encryption for subsequent messages.
82+
83+
#### Protocol Versions
84+
85+
The library supports multiple protocol versions which differ primarily in their encryption schemes:
86+
87+
* **V1 (Legacy/Standard)**:
88+
* **Encryption**: AES-128-ECB.
89+
* **Key Derivation**: `MD5(timestamp + local_key + SALT)`.
90+
* **Structure**: Header (Version, Seq, Random, Timestamp, Protocol) + Encrypted Payload + CRC32 Checksum.
91+
92+
* **L01 (Newer)**:
93+
* **Encryption**: AES-256-GCM (Authenticated Encryption).
94+
* **Key Derivation**: SHA256 based on `timestamp`, `local_key`, and `SALT`.
95+
* **IV/AAD**: Derived from sequence numbers and nonces (`connect_nonce`, `ack_nonce`) exchanged during handshake.
96+
* **Security**: Provides better security against replay attacks and tampering compared to V1.
97+
98+
* **A01 / B01**:
99+
* **Encryption**: AES-CBC.
100+
* **IV**: Derived from `MD5(random + HASH)`.
101+
* These are typically used by newer camera-equipped models (e.g., S7 MaxV, Zeo).
102+
103+
## Data Flow (V1 Device Example)
104+
105+
1. **Initialization**: `DeviceManager` creates a `V1Channel` with an `MqttChannel` and `LocalSession`.
106+
2. **Connection**:
107+
* The `MqttChannel` is ready immediately (sharing the global `MqttSession`).
108+
* The `V1Channel` attempts to connect locally in the background:
109+
1. Sends a request via MQTT to get `NetworkingInfo` (contains Local IP).
110+
2. Uses `LocalSession` to create a `LocalChannel` to that IP.
111+
3. Performs the local handshake.
112+
3. **Command Execution**:
113+
* User calls a method (e.g., `start_cleaning`).
114+
* The method calls `send_command` on the device's `V1RpcChannel`.
115+
* The `PickFirstAvailable` logic checks if `LocalChannel` is connected.
116+
* **If Yes**: Sends via TCP.
117+
* **If No**: Sends via MQTT.
118+
4. **Response**: The response is received, decrypted, decoded, and returned to the caller.
119+
120+
## Current Design Observations
121+
122+
* **Complexity**: The wrapping of channels (`Device` -> `V1Channel` -> `V1RpcChannel` -> `PickFirstAvailable` -> `PayloadEncoded...` -> `Mqtt/LocalChannel`) is deep.
123+
* **State Management**: Synchronization between the global MQTT session and individual device local connections is handled within `V1Channel`.
124+
* **Protocol Versions**: Distinct logic paths exist for V1, A01, and B01 protocols, though they share the underlying MQTT transport.

0 commit comments

Comments
 (0)