Skip to content

Commit fa69bf2

Browse files
allenporterCopilot
andauthored
chore: Update documentation to point to the newer device APIs (#589)
* Update documentation to point to the newer device APIs * chore: Update roborock/devices/traits/b01/__init__.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: Update roborock/devices/traits/v1/__init__.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feat: add examples that show how to use the cache and implement a file cache * chore: fix lint --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 3994110 commit fa69bf2

File tree

10 files changed

+286
-57
lines changed

10 files changed

+286
-57
lines changed

README.md

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,14 @@ Install this via pip (or your favourite package manager):
2020

2121
You can see all of the commands supported [here](https://python-roborock.readthedocs.io/en/latest/api_commands.html)
2222

23-
## Sending Commands
23+
## Example Usage
2424

25-
Here is an example that requires no manual intervention and can be done all automatically. You can skip some steps by
26-
caching values or looking at them and grabbing them manually.
2725
```python
2826
import asyncio
2927

30-
from roborock import HomeDataProduct, DeviceData, RoborockCommand
31-
from roborock.version_1_apis import RoborockMqttClientV1, RoborockLocalClientV1
3228
from roborock.web_api import RoborockApiClient
29+
from roborock.devices.device_manager import create_device_manager, UserParams
30+
3331

3432
async def main():
3533
web_api = RoborockApiClient(username="youremailhere")
@@ -40,30 +38,31 @@ async def main():
4038
code = input("What is the code?")
4139
user_data = await web_api.code_login(code)
4240

43-
# Get home data
44-
home_data = await web_api.get_home_data_v2(user_data)
45-
46-
# Get the device you want
47-
device = home_data.devices[0]
48-
49-
# Get product ids:
50-
product_info: dict[str, HomeDataProduct] = {
51-
product.id: product for product in home_data.products
52-
}
53-
# Create the Mqtt(aka cloud required) Client
54-
device_data = DeviceData(device, product_info[device.product_id].model)
55-
mqtt_client = RoborockMqttClientV1(user_data, device_data)
56-
networking = await mqtt_client.get_networking()
57-
local_device_data = DeviceData(device, product_info[device.product_id].model, networking.ip)
58-
local_client = RoborockLocalClientV1(local_device_data)
59-
# You can use the send_command to send any command to the device
60-
status = await local_client.send_command(RoborockCommand.GET_STATUS)
61-
# Or use existing functions that will give you data classes
62-
status = await local_client.get_status()
41+
# Create a device manager that can discover devices.
42+
user_params = UserParams(
43+
username="youremailhere",
44+
user_data=user_data,
45+
)
46+
device_manager = await create_device_manager(user_params)
47+
devices = await device_manager.get_devices()
48+
49+
# Get all vacuum devices that support the v1 PropertiesApi
50+
for device in devices:
51+
if not device.v1_properties:
52+
continue
53+
54+
# Refresh the current device status
55+
status_trait = device.v1_properties.status
56+
await status_trait.refresh()
57+
print(status_trait)
6358

6459
asyncio.run(main())
6560
```
6661

62+
See [examples/example.py](examples/example.py) for a more full featured example
63+
that has performance improvements to cache cloud information to prefer
64+
connections over the local network.
65+
6766
## Supported devices
6867

6968
You can find what devices are supported

examples/example.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Example script demonstrating how to connect to Roborock devices and print their status."""
2+
3+
import asyncio
4+
import dataclasses
5+
import json
6+
import pathlib
7+
from typing import Any
8+
9+
from roborock.devices.device_manager import UserParams, create_device_manager
10+
from roborock.devices.file_cache import FileCache, load_value, store_value
11+
from roborock.web_api import RoborockApiClient
12+
13+
# We typically store the login credentials/information separately from other cached data.
14+
USER_PARAMS_PATH = pathlib.Path.home() / ".cache" / "roborock-user-params.pkl"
15+
16+
# Device connection information is cached to speed up future connections.
17+
CACHE_PATH = pathlib.Path.home() / ".cache" / "roborock-cache-data.pkl"
18+
19+
20+
async def login_flow() -> UserParams:
21+
"""Perform the login flow to obtain UserData from the web API."""
22+
username = input("Email: ")
23+
web_api = RoborockApiClient(username=username)
24+
print("Requesting login code sent to email...")
25+
await web_api.request_code()
26+
code = input("Code: ")
27+
user_data = await web_api.code_login(code)
28+
# We store the base_url to avoid future discovery calls.
29+
base_url = await web_api.base_url
30+
return UserParams(
31+
username=username,
32+
user_data=user_data,
33+
base_url=base_url,
34+
)
35+
36+
37+
async def get_or_create_session() -> UserParams:
38+
"""Initialize the session by logging in if necessary."""
39+
user_params = await load_value(USER_PARAMS_PATH)
40+
if user_params is None:
41+
print("No cached login data found, please login.")
42+
user_params = await login_flow()
43+
print("Login successful, caching login data...")
44+
await store_value(USER_PARAMS_PATH, user_params)
45+
print(f"Cached login data to {USER_PARAMS_PATH}.")
46+
return user_params
47+
48+
49+
def remove_none_values(data: dict[str, Any]) -> dict[str, Any]:
50+
return {k: v for k, v in data.items() if v is not None}
51+
52+
53+
async def main():
54+
user_params = await get_or_create_session()
55+
cache = FileCache(CACHE_PATH)
56+
57+
# Create a device manager that can discover devices.
58+
device_manager = await create_device_manager(user_params, cache=cache)
59+
devices = await device_manager.get_devices()
60+
61+
# Get all vacuum devices that support the v1 PropertiesApi
62+
device_results = []
63+
for device in devices:
64+
if not device.v1_properties:
65+
continue
66+
67+
# Refresh the current device status
68+
status_trait = device.v1_properties.status
69+
await status_trait.refresh()
70+
71+
# Print the device status as JSON
72+
device_results.append(
73+
{
74+
"device": device.name,
75+
"status": remove_none_values(dataclasses.asdict(status_trait)),
76+
}
77+
)
78+
79+
print(json.dumps(device_results, indent=2))
80+
81+
await cache.flush()
82+
83+
84+
if __name__ == "__main__":
85+
asyncio.run(main())

roborock/__init__.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
cloud_api,
1212
const,
1313
data,
14+
devices,
1415
exceptions,
1516
roborock_typing,
1617
version_1_apis,
@@ -19,13 +20,11 @@
1920
)
2021

2122
__all__ = [
23+
"devices",
24+
"data",
25+
"map",
2226
"web_api",
23-
"version_1_apis",
24-
"version_a01_apis",
25-
"const",
26-
"cloud_api",
2727
"roborock_typing",
2828
"exceptions",
29-
"data",
30-
# Add new APIs here in the future when they are public e.g. devices/
29+
"const",
3130
]

roborock/devices/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
# Roborock Device Discovery
1+
# Roborock Devices & Discovery
22

3-
This page documents the full lifecycle of device discovery across Cloud and Network.
3+
The devices module provides functionality to discover Roborock devices on the
4+
network. This section documents the full lifecycle of device discovery across
5+
Cloud and Network.
46

57
## Init account setup
68

@@ -61,7 +63,7 @@ that a newer version of the API should be used.
6163

6264
## Design
6365

64-
### Current API Issues
66+
### Prior API Issues
6567

6668
- Complex Inheritance Hierarchy: Multiple inheritance with classes like RoborockMqttClientV1 inheriting from both RoborockMqttClient and RoborockClientV1
6769

roborock/devices/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
"""The devices module provides functionality to discover Roborock devices on the network."""
1+
"""
2+
.. include:: ./README.md
3+
"""
24

35
__all__ = [
46
"device",
57
"device_manager",
68
"cache",
9+
"file_cache",
10+
"traits",
711
]

roborock/devices/file_cache.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""This module implements a file-backed cache for device information.
2+
3+
This module provides a `FileCache` class that implements the `Cache` protocol
4+
to store and retrieve cached device information from a file on disk. This allows
5+
persistent caching of device data across application restarts.
6+
"""
7+
8+
import asyncio
9+
import pathlib
10+
import pickle
11+
from collections.abc import Callable
12+
from typing import Any
13+
14+
from .cache import Cache, CacheData
15+
16+
17+
class FileCache(Cache):
18+
"""File backed cache implementation."""
19+
20+
def __init__(self, file_path: pathlib.Path, init_fn: Callable[[], CacheData] = CacheData) -> None:
21+
"""Initialize the file cache with the given file path."""
22+
self._init_fn = init_fn
23+
self._file_path = file_path
24+
self._cache_data: CacheData | None = None
25+
26+
async def get(self) -> CacheData:
27+
"""Get cached value."""
28+
if self._cache_data is not None:
29+
return self._cache_data
30+
31+
data = await load_value(self._file_path)
32+
if data is not None and not isinstance(data, CacheData):
33+
raise TypeError(f"Invalid cache data loaded from {self._file_path}")
34+
35+
self._cache_data = data or self._init_fn()
36+
return self._cache_data
37+
38+
async def set(self, value: CacheData) -> None: # type: ignore[override]
39+
"""Set value in the cache."""
40+
self._cache_data = value
41+
42+
async def flush(self) -> None:
43+
"""Flush the cache to disk."""
44+
if self._cache_data is None:
45+
return
46+
await store_value(self._file_path, self._cache_data)
47+
48+
49+
async def store_value(file_path: pathlib.Path, value: Any) -> None:
50+
"""Store a value to the given file path."""
51+
52+
def _store_to_disk(file_path: pathlib.Path, value: Any) -> None:
53+
with open(file_path, "wb") as f:
54+
data = pickle.dumps(value)
55+
f.write(data)
56+
57+
await asyncio.to_thread(_store_to_disk, file_path, value)
58+
59+
60+
async def load_value(file_path: pathlib.Path) -> Any | None:
61+
"""Load a value from the given file path."""
62+
63+
def _load_from_disk(file_path: pathlib.Path) -> Any | None:
64+
if not file_path.exists():
65+
return None
66+
with open(file_path, "rb") as f:
67+
data = f.read()
68+
return pickle.loads(data)
69+
70+
return await asyncio.to_thread(_load_from_disk, file_path)

roborock/devices/traits/b01/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
from roborock.devices.traits import Trait
77
from roborock.roborock_message import RoborockB01Props
88

9-
__init__ = [
10-
"create_b01_traits",
9+
__all__ = [
1110
"PropertiesApi",
1211
]
1312

roborock/devices/traits/v1/__init__.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -67,27 +67,27 @@
6767
_LOGGER = logging.getLogger(__name__)
6868

6969
__all__ = [
70-
"create",
7170
"PropertiesApi",
72-
"StatusTrait",
73-
"DoNotDisturbTrait",
74-
"CleanSummaryTrait",
75-
"SoundVolumeTrait",
76-
"MapsTrait",
77-
"MapContentTrait",
78-
"ConsumableTrait",
79-
"HomeTrait",
80-
"DeviceFeaturesTrait",
81-
"CommandTrait",
82-
"ChildLockTrait",
83-
"FlowLedStatusTrait",
84-
"LedStatusTrait",
85-
"ValleyElectricityTimerTrait",
86-
"DustCollectionModeTrait",
87-
"WashTowelModeTrait",
88-
"SmartWashParamsTrait",
89-
"NetworkInfoTrait",
90-
"RoutinesTrait",
71+
"child_lock",
72+
"clean_summary",
73+
"common",
74+
"consumeable",
75+
"device_features",
76+
"do_not_disturb",
77+
"dust_collection_mode",
78+
"flow_led_status",
79+
"home",
80+
"led_status",
81+
"map_content",
82+
"maps",
83+
"network_info",
84+
"rooms",
85+
"routines",
86+
"smart_wash_params",
87+
"status",
88+
"valley_electricity_timer",
89+
"volume",
90+
"wash_towel_mode",
9191
]
9292

9393

roborock/protocols/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Protocols for communicating with Roborock devices."""
2+
3+
__all__: list[str] = []

0 commit comments

Comments
 (0)