roborock-cli 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- roborock_cli/__init__.py +3 -0
- roborock_cli/__main__.py +76 -0
- roborock_cli/_vendor/VERSION +6 -0
- roborock_cli/_vendor/__init__.py +0 -0
- roborock_cli/_vendor/roborock/__init__.py +27 -0
- roborock_cli/_vendor/roborock/broadcast_protocol.py +114 -0
- roborock_cli/_vendor/roborock/callbacks.py +130 -0
- roborock_cli/_vendor/roborock/cli.py +1338 -0
- roborock_cli/_vendor/roborock/const.py +84 -0
- roborock_cli/_vendor/roborock/data/__init__.py +9 -0
- roborock_cli/_vendor/roborock/data/b01_q10/__init__.py +2 -0
- roborock_cli/_vendor/roborock/data/b01_q10/b01_q10_code_mappings.py +213 -0
- roborock_cli/_vendor/roborock/data/b01_q10/b01_q10_containers.py +102 -0
- roborock_cli/_vendor/roborock/data/b01_q7/__init__.py +2 -0
- roborock_cli/_vendor/roborock/data/b01_q7/b01_q7_code_mappings.py +303 -0
- roborock_cli/_vendor/roborock/data/b01_q7/b01_q7_containers.py +302 -0
- roborock_cli/_vendor/roborock/data/code_mappings.py +198 -0
- roborock_cli/_vendor/roborock/data/containers.py +530 -0
- roborock_cli/_vendor/roborock/data/dyad/__init__.py +2 -0
- roborock_cli/_vendor/roborock/data/dyad/dyad_code_mappings.py +102 -0
- roborock_cli/_vendor/roborock/data/dyad/dyad_containers.py +28 -0
- roborock_cli/_vendor/roborock/data/v1/__init__.py +3 -0
- roborock_cli/_vendor/roborock/data/v1/v1_clean_modes.py +192 -0
- roborock_cli/_vendor/roborock/data/v1/v1_code_mappings.py +644 -0
- roborock_cli/_vendor/roborock/data/v1/v1_containers.py +800 -0
- roborock_cli/_vendor/roborock/data/zeo/__init__.py +2 -0
- roborock_cli/_vendor/roborock/data/zeo/zeo_code_mappings.py +138 -0
- roborock_cli/_vendor/roborock/data/zeo/zeo_containers.py +0 -0
- roborock_cli/_vendor/roborock/device_features.py +668 -0
- roborock_cli/_vendor/roborock/devices/README.md +41 -0
- roborock_cli/_vendor/roborock/devices/__init__.py +11 -0
- roborock_cli/_vendor/roborock/devices/cache.py +143 -0
- roborock_cli/_vendor/roborock/devices/device.py +240 -0
- roborock_cli/_vendor/roborock/devices/device_manager.py +269 -0
- roborock_cli/_vendor/roborock/devices/file_cache.py +79 -0
- roborock_cli/_vendor/roborock/devices/rpc/__init__.py +14 -0
- roborock_cli/_vendor/roborock/devices/rpc/a01_channel.py +94 -0
- roborock_cli/_vendor/roborock/devices/rpc/b01_q10_channel.py +57 -0
- roborock_cli/_vendor/roborock/devices/rpc/b01_q7_channel.py +101 -0
- roborock_cli/_vendor/roborock/devices/rpc/v1_channel.py +457 -0
- roborock_cli/_vendor/roborock/devices/traits/__init__.py +28 -0
- roborock_cli/_vendor/roborock/devices/traits/a01/__init__.py +191 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/__init__.py +12 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/__init__.py +76 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/command.py +32 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/common.py +115 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/status.py +32 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/vacuum.py +81 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q7/__init__.py +136 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q7/clean_summary.py +75 -0
- roborock_cli/_vendor/roborock/devices/traits/traits_mixin.py +64 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/__init__.py +344 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/child_lock.py +29 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/clean_summary.py +83 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/command.py +38 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/common.py +172 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/consumeable.py +48 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/device_features.py +74 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/do_not_disturb.py +41 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/dust_collection_mode.py +13 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/flow_led_status.py +29 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/home.py +285 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/led_status.py +43 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/map_content.py +83 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/maps.py +80 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/network_info.py +55 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/rooms.py +105 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/routines.py +26 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/smart_wash_params.py +13 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/status.py +101 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/valley_electricity_timer.py +44 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/volume.py +27 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/wash_towel_mode.py +13 -0
- roborock_cli/_vendor/roborock/devices/transport/__init__.py +8 -0
- roborock_cli/_vendor/roborock/devices/transport/channel.py +32 -0
- roborock_cli/_vendor/roborock/devices/transport/local_channel.py +295 -0
- roborock_cli/_vendor/roborock/devices/transport/mqtt_channel.py +118 -0
- roborock_cli/_vendor/roborock/diagnostics.py +166 -0
- roborock_cli/_vendor/roborock/exceptions.py +95 -0
- roborock_cli/_vendor/roborock/map/__init__.py +7 -0
- roborock_cli/_vendor/roborock/map/map_parser.py +123 -0
- roborock_cli/_vendor/roborock/mqtt/__init__.py +10 -0
- roborock_cli/_vendor/roborock/mqtt/health_manager.py +60 -0
- roborock_cli/_vendor/roborock/mqtt/roborock_session.py +463 -0
- roborock_cli/_vendor/roborock/mqtt/session.py +108 -0
- roborock_cli/_vendor/roborock/protocol.py +558 -0
- roborock_cli/_vendor/roborock/protocols/__init__.py +3 -0
- roborock_cli/_vendor/roborock/protocols/a01_protocol.py +74 -0
- roborock_cli/_vendor/roborock/protocols/b01_q10_protocol.py +87 -0
- roborock_cli/_vendor/roborock/protocols/b01_q7_protocol.py +81 -0
- roborock_cli/_vendor/roborock/protocols/v1_protocol.py +271 -0
- roborock_cli/_vendor/roborock/py.typed +0 -0
- roborock_cli/_vendor/roborock/roborock_message.py +246 -0
- roborock_cli/_vendor/roborock/roborock_typing.py +382 -0
- roborock_cli/_vendor/roborock/util.py +54 -0
- roborock_cli/_vendor/roborock/web_api.py +761 -0
- roborock_cli/cli.py +715 -0
- roborock_cli/connection.py +202 -0
- roborock_cli/helpers.py +71 -0
- roborock_cli/server.py +759 -0
- roborock_cli/setup_auth.py +92 -0
- roborock_cli-0.1.1.dist-info/METADATA +172 -0
- roborock_cli-0.1.1.dist-info/RECORD +106 -0
- roborock_cli-0.1.1.dist-info/WHEEL +4 -0
- roborock_cli-0.1.1.dist-info/entry_points.txt +2 -0
- roborock_cli-0.1.1.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Roborock Device Manager
|
|
2
|
+
|
|
3
|
+
This library provides a high-level interface for discovering and controlling Roborock devices. It abstracts the underlying communication protocols (MQTT, Local TCP) and provides a unified `DeviceManager` for interacting with your devices.
|
|
4
|
+
|
|
5
|
+
For internal architecture details, protocol specifications, and design documentation, please refer to [docs/DEVICES.md](https://github.com/python-roborock/python-roborock/docs/DEVICES.md).
|
|
6
|
+
|
|
7
|
+
## Getting Started
|
|
8
|
+
|
|
9
|
+
### Credentials
|
|
10
|
+
|
|
11
|
+
To connect to your devices, you first need to obtain your user data (including the `rriot` token) from the Roborock Cloud. This is handled via the `RoborockApiClient`.
|
|
12
|
+
|
|
13
|
+
## Usage Guide
|
|
14
|
+
|
|
15
|
+
The core entry point for the library is the `DeviceManager`. It handles:
|
|
16
|
+
1. **Device Discovery**: Fetching the list of devices associated with your account.
|
|
17
|
+
2. **Connection Management**: Automatically determining the best connection method (Local vs MQTT) and protocol version (V1 vs A01/B01).
|
|
18
|
+
3. **Command Execution**: Sending commands and query status.
|
|
19
|
+
|
|
20
|
+
### Example
|
|
21
|
+
|
|
22
|
+
See [examples/example.py](https://github.com/python-roborock/python-roborock/examples/example.py) for a complete example of how to login, create a device manager, and list the status of your vacuums.
|
|
23
|
+
|
|
24
|
+
### Device Properties
|
|
25
|
+
|
|
26
|
+
Different devices support different property sets:
|
|
27
|
+
|
|
28
|
+
* **`v1_properties`**: Primarily for Vacuum Robots (S7, S8, Q5, etc.). Supports traits like `status`, `consumables`, `fan_power`, `water_box`.
|
|
29
|
+
* **`a01_properties`**: For Washer/Dryers and handheld Wet/Dry Vacuums (Dyad, Zeo) that use another newer protocol.
|
|
30
|
+
* **`b01_q7_properties`** and **`b01_q10_properties`**: For newer Vacuum/Mop devices using newer protocol instead of v1.
|
|
31
|
+
|
|
32
|
+
You can check if a property set is available by checking if the property on the device object is not `None` (e.g. `if device.v1_properties:`).
|
|
33
|
+
|
|
34
|
+
### Caching
|
|
35
|
+
|
|
36
|
+
Use `FileCache` or your own `Cache` implementation to persist:
|
|
37
|
+
- `HomeData`: The list of your home's rooms and devices.
|
|
38
|
+
- `NetworkingInfo`: Device IP addresses and tokens.
|
|
39
|
+
- `Device Capabilities`: What features your specific model supports.
|
|
40
|
+
|
|
41
|
+
This speeds up startup time and reduces load on the Roborock cloud APIs.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""This module provides caching functionality for the Roborock device management system.
|
|
2
|
+
|
|
3
|
+
This module defines a cache interface that you may use to cache device
|
|
4
|
+
information to avoid unnecessary API calls. Callers may implement
|
|
5
|
+
this interface to provide their own caching mechanism.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Protocol
|
|
10
|
+
|
|
11
|
+
from roborock_cli._vendor.roborock.data import CombinedMapInfo, HomeData, NetworkInfo, RoborockBase
|
|
12
|
+
from roborock_cli._vendor.roborock.device_features import DeviceFeatures
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class DeviceCacheData(RoborockBase):
|
|
17
|
+
"""Data structure for caching device information."""
|
|
18
|
+
|
|
19
|
+
network_info: NetworkInfo | None = None
|
|
20
|
+
"""Network information for the device"""
|
|
21
|
+
|
|
22
|
+
home_map_info: dict[int, CombinedMapInfo] | None = None
|
|
23
|
+
"""Home map information for the device by map_flag."""
|
|
24
|
+
|
|
25
|
+
home_map_content_base64: dict[int, str] | None = None
|
|
26
|
+
"""Home cache content for the device (encoded base64) by map_flag."""
|
|
27
|
+
|
|
28
|
+
device_features: DeviceFeatures | None = None
|
|
29
|
+
"""Device features information."""
|
|
30
|
+
|
|
31
|
+
trait_data: dict[str, Any] | None = None
|
|
32
|
+
"""Trait-specific cached data used internally for caching device features."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class CacheData(RoborockBase):
|
|
37
|
+
"""Data structure for caching device information."""
|
|
38
|
+
|
|
39
|
+
home_data: HomeData | None = None
|
|
40
|
+
"""Home data containing device and product information."""
|
|
41
|
+
|
|
42
|
+
device_info: dict[str, DeviceCacheData] = field(default_factory=dict)
|
|
43
|
+
"""Per-device cached information indexed by device DUID."""
|
|
44
|
+
|
|
45
|
+
network_info: dict[str, NetworkInfo] = field(default_factory=dict)
|
|
46
|
+
"""Network information indexed by device DUID.
|
|
47
|
+
|
|
48
|
+
This is deprecated. Use the per-device `network_info` field instead.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
home_map_info: dict[int, CombinedMapInfo] = field(default_factory=dict)
|
|
52
|
+
"""Home map information indexed by map_flag.
|
|
53
|
+
|
|
54
|
+
This is deprecated. Use the per-device `home_map_info` field instead.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
home_map_content: dict[int, bytes] = field(default_factory=dict)
|
|
58
|
+
"""Home cache content for each map data indexed by map_flag.
|
|
59
|
+
|
|
60
|
+
This is deprecated. Use the per-device `home_map_content_base64` field instead.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
home_map_content_base64: dict[int, str] = field(default_factory=dict)
|
|
64
|
+
"""Home cache content for each map data (encoded base64) indexed by map_flag.
|
|
65
|
+
|
|
66
|
+
This is deprecated. Use the per-device `home_map_content_base64` field instead.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
device_features: DeviceFeatures | None = None
|
|
70
|
+
"""Device features information.
|
|
71
|
+
|
|
72
|
+
This is deprecated. Use the per-device `device_features` field instead.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
trait_data: dict[str, Any] | None = None
|
|
76
|
+
"""Trait-specific cached data used internally for caching device features.
|
|
77
|
+
|
|
78
|
+
This is deprecated. Use the per-device `trait_data` field instead.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Cache(Protocol):
|
|
83
|
+
"""Protocol for a cache that can store and retrieve values."""
|
|
84
|
+
|
|
85
|
+
async def get(self) -> CacheData:
|
|
86
|
+
"""Get cached value."""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
async def set(self, value: CacheData) -> None:
|
|
90
|
+
"""Set value in the cache."""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class DeviceCache(RoborockBase):
|
|
96
|
+
"""Provides a cache interface for a specific device.
|
|
97
|
+
|
|
98
|
+
This is a convenience wrapper around a general Cache implementation to
|
|
99
|
+
provide device-specific caching functionality.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, duid: str, cache: Cache) -> None:
|
|
103
|
+
"""Initialize the device cache with the given cache implementation."""
|
|
104
|
+
self._duid = duid
|
|
105
|
+
self._cache = cache
|
|
106
|
+
|
|
107
|
+
async def get(self) -> DeviceCacheData:
|
|
108
|
+
"""Get cached device-specific information."""
|
|
109
|
+
cache_data = await self._cache.get()
|
|
110
|
+
if self._duid not in cache_data.device_info:
|
|
111
|
+
cache_data.device_info[self._duid] = DeviceCacheData()
|
|
112
|
+
await self._cache.set(cache_data)
|
|
113
|
+
return cache_data.device_info[self._duid]
|
|
114
|
+
|
|
115
|
+
async def set(self, device_cache_data: DeviceCacheData) -> None:
|
|
116
|
+
"""Set cached device-specific information."""
|
|
117
|
+
cache_data = await self._cache.get()
|
|
118
|
+
cache_data.device_info[self._duid] = device_cache_data
|
|
119
|
+
await self._cache.set(cache_data)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class InMemoryCache(Cache):
|
|
123
|
+
"""In-memory cache implementation."""
|
|
124
|
+
|
|
125
|
+
def __init__(self) -> None:
|
|
126
|
+
"""Initialize the in-memory cache."""
|
|
127
|
+
self._data = CacheData()
|
|
128
|
+
|
|
129
|
+
async def get(self) -> CacheData:
|
|
130
|
+
return self._data
|
|
131
|
+
|
|
132
|
+
async def set(self, value: CacheData) -> None:
|
|
133
|
+
self._data = value
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class NoCache(Cache):
|
|
137
|
+
"""No-op cache implementation."""
|
|
138
|
+
|
|
139
|
+
async def get(self) -> CacheData:
|
|
140
|
+
return CacheData()
|
|
141
|
+
|
|
142
|
+
async def set(self, value: CacheData) -> None:
|
|
143
|
+
pass
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Module for Roborock devices.
|
|
2
|
+
|
|
3
|
+
This interface is experimental and subject to breaking changes without notice
|
|
4
|
+
until the API is stable.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import datetime
|
|
9
|
+
import logging
|
|
10
|
+
from abc import ABC
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from roborock_cli._vendor.roborock.callbacks import CallbackList
|
|
15
|
+
from roborock_cli._vendor.roborock.data import HomeDataDevice, HomeDataProduct
|
|
16
|
+
from roborock_cli._vendor.roborock.diagnostics import redact_device_data
|
|
17
|
+
from roborock_cli._vendor.roborock.exceptions import RoborockException
|
|
18
|
+
from roborock_cli._vendor.roborock.roborock_message import RoborockMessage
|
|
19
|
+
from roborock_cli._vendor.roborock.util import RoborockLoggerAdapter
|
|
20
|
+
|
|
21
|
+
from .traits import Trait
|
|
22
|
+
from .traits.traits_mixin import TraitsMixin
|
|
23
|
+
from .transport.channel import Channel
|
|
24
|
+
|
|
25
|
+
_LOGGER = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"DeviceReadyCallback",
|
|
29
|
+
"RoborockDevice",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# Exponential backoff parameters
|
|
33
|
+
MIN_BACKOFF_INTERVAL = datetime.timedelta(seconds=10)
|
|
34
|
+
MAX_BACKOFF_INTERVAL = datetime.timedelta(minutes=30)
|
|
35
|
+
BACKOFF_MULTIPLIER = 1.5
|
|
36
|
+
# Give time for the NETWORK_INFO fetch and V1 hello attempt
|
|
37
|
+
# and potential fallback to L01.
|
|
38
|
+
START_ATTEMPT_TIMEOUT = datetime.timedelta(seconds=15)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
DeviceReadyCallback = Callable[["RoborockDevice"], None]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RoborockDevice(ABC, TraitsMixin):
|
|
45
|
+
"""A generic channel for establishing a connection with a Roborock device.
|
|
46
|
+
|
|
47
|
+
Individual channel implementations have their own methods for speaking to
|
|
48
|
+
the device that hide some of the protocol specific complexity, but they
|
|
49
|
+
are still specialized for the device type and protocol.
|
|
50
|
+
|
|
51
|
+
Attributes of the device are exposed through traits, which are mixed in
|
|
52
|
+
through the TraitsMixin class. Traits are optional and may not be present
|
|
53
|
+
on all devices.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
device_info: HomeDataDevice,
|
|
59
|
+
product: HomeDataProduct,
|
|
60
|
+
channel: Channel,
|
|
61
|
+
trait: Trait,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Initialize the RoborockDevice.
|
|
64
|
+
|
|
65
|
+
The device takes ownership of the channel for communication with the device.
|
|
66
|
+
Use `connect()` to establish the connection, which will set up the appropriate
|
|
67
|
+
protocol channel. Use `close()` to clean up all connections.
|
|
68
|
+
"""
|
|
69
|
+
TraitsMixin.__init__(self, trait)
|
|
70
|
+
self._duid = device_info.duid
|
|
71
|
+
self._logger = RoborockLoggerAdapter(duid=self._duid, logger=_LOGGER)
|
|
72
|
+
self._name = device_info.name
|
|
73
|
+
self._device_info = device_info
|
|
74
|
+
self._product = product
|
|
75
|
+
self._channel = channel
|
|
76
|
+
self._connect_task: asyncio.Task[None] | None = None
|
|
77
|
+
self._unsub: Callable[[], None] | None = None
|
|
78
|
+
self._ready_callbacks = CallbackList["RoborockDevice"]()
|
|
79
|
+
self._has_connected = False
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def duid(self) -> str:
|
|
83
|
+
"""Return the device unique identifier (DUID)."""
|
|
84
|
+
return self._duid
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def name(self) -> str:
|
|
88
|
+
"""Return the device name."""
|
|
89
|
+
return self._name
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def device_info(self) -> HomeDataDevice:
|
|
93
|
+
"""Return the device information.
|
|
94
|
+
|
|
95
|
+
This includes information specific to the device like its identifier or
|
|
96
|
+
firmware version.
|
|
97
|
+
"""
|
|
98
|
+
return self._device_info
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def product(self) -> HomeDataProduct:
|
|
102
|
+
"""Return the device product name.
|
|
103
|
+
|
|
104
|
+
This returns product level information such as the model name.
|
|
105
|
+
"""
|
|
106
|
+
return self._product
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def is_connected(self) -> bool:
|
|
110
|
+
"""Return whether the device is connected."""
|
|
111
|
+
return self._channel.is_connected
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def is_local_connected(self) -> bool:
|
|
115
|
+
"""Return whether the device is connected locally.
|
|
116
|
+
|
|
117
|
+
This can be used to determine if the device is reachable over a local
|
|
118
|
+
network connection, as opposed to a cloud connection. This is useful
|
|
119
|
+
for adjusting behavior like polling frequency.
|
|
120
|
+
"""
|
|
121
|
+
return self._channel.is_local_connected
|
|
122
|
+
|
|
123
|
+
def add_ready_callback(self, callback: DeviceReadyCallback) -> Callable[[], None]:
|
|
124
|
+
"""Add a callback to be notified when the device is ready.
|
|
125
|
+
|
|
126
|
+
A device is considered ready when it has successfully connected. It may go
|
|
127
|
+
offline later, but this callback will only be called once when the device
|
|
128
|
+
first connects.
|
|
129
|
+
|
|
130
|
+
The callback will be called immediately if the device has already previously
|
|
131
|
+
connected.
|
|
132
|
+
"""
|
|
133
|
+
remove = self._ready_callbacks.add_callback(callback)
|
|
134
|
+
if self._has_connected:
|
|
135
|
+
callback(self)
|
|
136
|
+
|
|
137
|
+
return remove
|
|
138
|
+
|
|
139
|
+
async def start_connect(self) -> None:
|
|
140
|
+
"""Start a background task to connect to the device.
|
|
141
|
+
|
|
142
|
+
This will give a moment for the first connection attempt to start so
|
|
143
|
+
that the device will have connections established -- however, this will
|
|
144
|
+
never directly fail.
|
|
145
|
+
|
|
146
|
+
If the connection fails, it will retry in the background with
|
|
147
|
+
exponential backoff.
|
|
148
|
+
|
|
149
|
+
Once connected, the device will remain connected until `close()` is
|
|
150
|
+
called. The device will automatically attempt to reconnect if the connection
|
|
151
|
+
is lost.
|
|
152
|
+
"""
|
|
153
|
+
# The future will be set to True if the first attempt succeeds, False if
|
|
154
|
+
# it fails, or an exception if an unexpected error occurs.
|
|
155
|
+
# We use this to wait a short time for the first attempt to complete. We
|
|
156
|
+
# don't actually care about the result, just that we waited long enough.
|
|
157
|
+
start_attempt: asyncio.Future[bool] = asyncio.Future()
|
|
158
|
+
|
|
159
|
+
async def connect_loop() -> None:
|
|
160
|
+
try:
|
|
161
|
+
backoff = MIN_BACKOFF_INTERVAL
|
|
162
|
+
while True:
|
|
163
|
+
try:
|
|
164
|
+
await self.connect()
|
|
165
|
+
if not start_attempt.done():
|
|
166
|
+
start_attempt.set_result(True)
|
|
167
|
+
self._has_connected = True
|
|
168
|
+
self._ready_callbacks(self)
|
|
169
|
+
return
|
|
170
|
+
except RoborockException as e:
|
|
171
|
+
if not start_attempt.done():
|
|
172
|
+
start_attempt.set_result(False)
|
|
173
|
+
self._logger.info("Failed to connect (retry %s): %s", backoff.total_seconds(), e)
|
|
174
|
+
await asyncio.sleep(backoff.total_seconds())
|
|
175
|
+
backoff = min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_INTERVAL)
|
|
176
|
+
except Exception as e: # pylint: disable=broad-except
|
|
177
|
+
if not start_attempt.done():
|
|
178
|
+
start_attempt.set_exception(e)
|
|
179
|
+
self._logger.exception("Uncaught error during connect: %s", e)
|
|
180
|
+
return
|
|
181
|
+
except asyncio.CancelledError:
|
|
182
|
+
self._logger.debug("connect_loop was cancelled for device %s", self.duid)
|
|
183
|
+
finally:
|
|
184
|
+
if not start_attempt.done():
|
|
185
|
+
start_attempt.set_result(False)
|
|
186
|
+
|
|
187
|
+
self._connect_task = asyncio.create_task(connect_loop())
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
async with asyncio.timeout(START_ATTEMPT_TIMEOUT.total_seconds()):
|
|
191
|
+
await start_attempt
|
|
192
|
+
except TimeoutError:
|
|
193
|
+
self._logger.debug("Initial connection attempt took longer than expected, will keep trying in background")
|
|
194
|
+
|
|
195
|
+
async def connect(self) -> None:
|
|
196
|
+
"""Connect to the device using the appropriate protocol channel."""
|
|
197
|
+
if self._unsub:
|
|
198
|
+
raise ValueError("Already connected to the device")
|
|
199
|
+
unsub = await self._channel.subscribe(self._on_message)
|
|
200
|
+
try:
|
|
201
|
+
if self.v1_properties is not None:
|
|
202
|
+
await self.v1_properties.discover_features()
|
|
203
|
+
elif self.b01_q10_properties is not None:
|
|
204
|
+
await self.b01_q10_properties.start()
|
|
205
|
+
except RoborockException:
|
|
206
|
+
unsub()
|
|
207
|
+
raise
|
|
208
|
+
self._logger.info("Connected to device")
|
|
209
|
+
self._unsub = unsub
|
|
210
|
+
|
|
211
|
+
async def close(self) -> None:
|
|
212
|
+
"""Close all connections to the device."""
|
|
213
|
+
if self._connect_task:
|
|
214
|
+
self._connect_task.cancel()
|
|
215
|
+
try:
|
|
216
|
+
await self._connect_task
|
|
217
|
+
except asyncio.CancelledError:
|
|
218
|
+
pass
|
|
219
|
+
if self.b01_q10_properties is not None:
|
|
220
|
+
await self.b01_q10_properties.close()
|
|
221
|
+
if self._unsub:
|
|
222
|
+
self._unsub()
|
|
223
|
+
self._unsub = None
|
|
224
|
+
|
|
225
|
+
def _on_message(self, message: RoborockMessage) -> None:
|
|
226
|
+
"""Handle incoming messages from the device."""
|
|
227
|
+
self._logger.debug("Received message from device: %s", message)
|
|
228
|
+
|
|
229
|
+
def diagnostic_data(self) -> dict[str, Any]:
|
|
230
|
+
"""Return diagnostics information about the device."""
|
|
231
|
+
extra: dict[str, Any] = {}
|
|
232
|
+
if self.v1_properties:
|
|
233
|
+
extra["traits"] = self.v1_properties.as_dict()
|
|
234
|
+
return redact_device_data(
|
|
235
|
+
{
|
|
236
|
+
"device": self.device_info.as_dict(),
|
|
237
|
+
"product": self.product.as_dict(),
|
|
238
|
+
**extra,
|
|
239
|
+
}
|
|
240
|
+
)
|