python-roborock 5.9.1__tar.gz → 5.10.0__tar.gz
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.
- {python_roborock-5.9.1 → python_roborock-5.10.0}/PKG-INFO +1 -1
- {python_roborock-5.9.1 → python_roborock-5.10.0}/pyproject.toml +1 -1
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/callbacks.py +1 -1
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/cli.py +1 -1
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/v1/v1_containers.py +7 -5
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/device.py +3 -1
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/device_manager.py +1 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/rpc/v1_channel.py +31 -6
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/__init__.py +32 -1
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/consumeable.py +23 -2
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/status.py +19 -1
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/protocols/v1_protocol.py +54 -10
- {python_roborock-5.9.1 → python_roborock-5.10.0}/.gitignore +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/LICENSE +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/README.md +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/const.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/code_mappings.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/containers.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/device_features.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/README.md +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/cache.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/file_cache.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/rpc/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/rpc/a01_channel.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/b01/q10/command.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/b01/q10/status.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/b01/q7/map.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/b01/q7/map_content.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/common.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/common.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/transport/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/transport/channel.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/transport/local_channel.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/transport/mqtt_channel.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/diagnostics.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/exceptions.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/map/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/map/b01_map_parser.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/map/map_parser.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/map/proto/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/map/proto/b01_scmap.proto +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/map/proto/b01_scmap_pb2.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/protocol.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/protocols/__init__.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/protocols/b01_q10_protocol.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/protocols/b01_q7_protocol.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/py.typed +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/roborock_message.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/util.py +0 -0
- {python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.10.0
|
|
4
4
|
Summary: A package to control Roborock vacuums.
|
|
5
5
|
Project-URL: Repository, https://github.com/python-roborock/python-roborock
|
|
6
6
|
Project-URL: Documentation, https://python-roborock.readthedocs.io/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-roborock"
|
|
3
|
-
version = "5.
|
|
3
|
+
version = "5.10.0"
|
|
4
4
|
description = "A package to control Roborock vacuums."
|
|
5
5
|
authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
|
|
6
6
|
requires-python = ">=3.11, <4"
|
|
@@ -26,7 +26,7 @@ def safe_callback(
|
|
|
26
26
|
try:
|
|
27
27
|
callback(value)
|
|
28
28
|
except Exception as ex: # noqa: BLE001
|
|
29
|
-
logger.error("Uncaught error in callback '%s': %s", callback
|
|
29
|
+
logger.error("Uncaught error in callback '%s': %s", getattr(callback, "__name__", "Unknown"), ex)
|
|
30
30
|
|
|
31
31
|
return wrapper
|
|
32
32
|
|
|
@@ -419,7 +419,7 @@ async def _v1_trait(context: RoborockContext, device_id: str, display_func: Call
|
|
|
419
419
|
device = await device_manager.get_device(device_id)
|
|
420
420
|
if device.v1_properties is None:
|
|
421
421
|
raise RoborockUnsupportedFeature(f"Device {device.name} does not support V1 protocol")
|
|
422
|
-
await device.v1_properties.
|
|
422
|
+
await device.v1_properties.start()
|
|
423
423
|
trait = display_func(device.v1_properties)
|
|
424
424
|
if trait is None:
|
|
425
425
|
raise RoborockUnsupportedFeature("Trait not supported by device")
|
|
@@ -100,13 +100,14 @@ class FieldNameBase(StrEnum):
|
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
class StatusField(FieldNameBase):
|
|
103
|
-
"""An enum that represents a field in the `
|
|
103
|
+
"""An enum that represents a field in the `StatusV2` class.
|
|
104
104
|
|
|
105
105
|
This is used with `roborock.devices.traits.v1.status.DeviceFeaturesTrait`
|
|
106
106
|
to understand if a feature is supported by the device using `is_field_supported`.
|
|
107
107
|
|
|
108
|
-
The enum values are names of fields in the `
|
|
109
|
-
with
|
|
108
|
+
The enum values are names of fields in the `StatusV2` class. Each field is
|
|
109
|
+
annotated with `dps` metadata to map the field to a `RoborockDataProtocol`
|
|
110
|
+
value used to check support against the product schema.
|
|
110
111
|
"""
|
|
111
112
|
|
|
112
113
|
STATE = "state"
|
|
@@ -670,8 +671,9 @@ class ConsumableField(FieldNameBase):
|
|
|
670
671
|
This is used with `roborock.devices.traits.v1.status.DeviceFeaturesTrait`
|
|
671
672
|
to understand if a feature is supported by the device using `is_field_supported`.
|
|
672
673
|
|
|
673
|
-
The enum values are names of fields in the `Consumable` class. Each field is
|
|
674
|
-
with
|
|
674
|
+
The enum values are names of fields in the `Consumable` class. Each field is
|
|
675
|
+
annotated with `dps` metadata to map the field to a `RoborockDataProtocol`
|
|
676
|
+
value used to check support against the product schema.
|
|
675
677
|
"""
|
|
676
678
|
|
|
677
679
|
MAIN_BRUSH_WORK_TIME = "main_brush_work_time"
|
|
@@ -199,7 +199,7 @@ class RoborockDevice(ABC, TraitsMixin):
|
|
|
199
199
|
unsub = await self._channel.subscribe(self._on_message)
|
|
200
200
|
try:
|
|
201
201
|
if self.v1_properties is not None:
|
|
202
|
-
await self.v1_properties.
|
|
202
|
+
await self.v1_properties.start()
|
|
203
203
|
elif self.b01_q10_properties is not None:
|
|
204
204
|
await self.b01_q10_properties.start()
|
|
205
205
|
except RoborockException:
|
|
@@ -216,6 +216,8 @@ class RoborockDevice(ABC, TraitsMixin):
|
|
|
216
216
|
await self._connect_task
|
|
217
217
|
except asyncio.CancelledError:
|
|
218
218
|
pass
|
|
219
|
+
if self.v1_properties is not None:
|
|
220
|
+
self.v1_properties.close()
|
|
219
221
|
if self.b01_q10_properties is not None:
|
|
220
222
|
await self.b01_q10_properties.close()
|
|
221
223
|
if self._unsub:
|
|
@@ -11,6 +11,7 @@ from collections.abc import Callable
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from typing import Any, TypeVar
|
|
13
13
|
|
|
14
|
+
from roborock.callbacks import CallbackList
|
|
14
15
|
from roborock.data import HomeDataDevice, NetworkInfo, RoborockBase, UserData
|
|
15
16
|
from roborock.devices.cache import DeviceCache
|
|
16
17
|
from roborock.devices.transport.channel import Channel
|
|
@@ -30,9 +31,10 @@ from roborock.protocols.v1_protocol import (
|
|
|
30
31
|
V1RpcChannel,
|
|
31
32
|
create_map_response_decoder,
|
|
32
33
|
create_security_data,
|
|
34
|
+
decode_data_protocol_message,
|
|
33
35
|
decode_rpc_response,
|
|
34
36
|
)
|
|
35
|
-
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
|
|
37
|
+
from roborock.roborock_message import RoborockDataProtocol, RoborockMessage, RoborockMessageProtocol
|
|
36
38
|
from roborock.roborock_typing import RoborockCommand
|
|
37
39
|
from roborock.util import RoborockLoggerAdapter
|
|
38
40
|
|
|
@@ -188,6 +190,7 @@ class V1Channel(Channel):
|
|
|
188
190
|
self._device_cache = device_cache
|
|
189
191
|
self._reconnect_task: asyncio.Task[None] | None = None
|
|
190
192
|
self._last_network_info_refresh: datetime.datetime | None = None
|
|
193
|
+
self._dps_listeners = CallbackList[dict[RoborockDataProtocol, Any]](self._logger)
|
|
191
194
|
|
|
192
195
|
@property
|
|
193
196
|
def is_connected(self) -> bool:
|
|
@@ -305,12 +308,16 @@ class V1Channel(Channel):
|
|
|
305
308
|
loop = asyncio.get_running_loop()
|
|
306
309
|
self._reconnect_task = loop.create_task(self._background_reconnect())
|
|
307
310
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
# error and let the caller know we failed to subscribe.
|
|
311
|
+
# We maintain an active MQTT subscription even when connected locally to receive
|
|
312
|
+
# unsolicited status updates (DPS push messages) directly from the cloud.
|
|
313
|
+
try:
|
|
312
314
|
self._mqtt_unsub = await self._mqtt_channel.subscribe(self._on_mqtt_message)
|
|
313
|
-
|
|
315
|
+
except RoborockException as err:
|
|
316
|
+
if not self.is_local_connected:
|
|
317
|
+
# Propagate error if both local and MQTT failed
|
|
318
|
+
self._logger.debug("MQTT connection also failed: %s", err)
|
|
319
|
+
raise
|
|
320
|
+
self._logger.debug("MQTT subscription failed, continuing with local-only connection: %s", err)
|
|
314
321
|
|
|
315
322
|
def unsub() -> None:
|
|
316
323
|
"""Unsubscribe from all messages."""
|
|
@@ -328,6 +335,16 @@ class V1Channel(Channel):
|
|
|
328
335
|
self._callback = callback
|
|
329
336
|
return unsub
|
|
330
337
|
|
|
338
|
+
def add_dps_listener(self, listener: Callable[[dict[RoborockDataProtocol, Any]], None]) -> Callable[[], None]:
|
|
339
|
+
"""Add a listener for DPS updates.
|
|
340
|
+
|
|
341
|
+
This will attach a listener to the existing subscription, invoking
|
|
342
|
+
the listener whenever new DPS values arrive from the subscription.
|
|
343
|
+
This will only work if a subscription has already been setup, which is
|
|
344
|
+
handled by the device start.
|
|
345
|
+
"""
|
|
346
|
+
return self._dps_listeners.add_callback(listener)
|
|
347
|
+
|
|
331
348
|
async def _get_networking_info(self, *, prefer_cache: bool = True) -> NetworkInfo:
|
|
332
349
|
"""Retrieve networking information for the device.
|
|
333
350
|
|
|
@@ -428,6 +445,14 @@ class V1Channel(Channel):
|
|
|
428
445
|
self._logger.debug("V1Channel received MQTT message: %s", message)
|
|
429
446
|
if self._callback:
|
|
430
447
|
self._callback(message)
|
|
448
|
+
try:
|
|
449
|
+
datapoints = decode_data_protocol_message(message)
|
|
450
|
+
except RoborockException as e:
|
|
451
|
+
self._logger.debug("Error decoding data protocol message: %s", e)
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
if datapoints:
|
|
455
|
+
self._dps_listeners(datapoints)
|
|
431
456
|
|
|
432
457
|
def _on_local_message(self, message: RoborockMessage) -> None:
|
|
433
458
|
"""Handle incoming local messages."""
|
|
@@ -53,6 +53,7 @@ HomeDataProduct Schema that is required for the field to be supported.
|
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
55
|
import logging
|
|
56
|
+
from collections.abc import Callable
|
|
56
57
|
from dataclasses import dataclass, field, fields
|
|
57
58
|
from typing import Any, get_args
|
|
58
59
|
|
|
@@ -60,8 +61,10 @@ from roborock.data.containers import HomeData, HomeDataProduct, RoborockBase
|
|
|
60
61
|
from roborock.data.v1.v1_code_mappings import RoborockDockTypeCode
|
|
61
62
|
from roborock.devices.cache import DeviceCache
|
|
62
63
|
from roborock.devices.traits import Trait
|
|
64
|
+
from roborock.exceptions import RoborockException
|
|
63
65
|
from roborock.map.map_parser import MapParserConfig
|
|
64
|
-
from roborock.protocols.v1_protocol import V1RpcChannel
|
|
66
|
+
from roborock.protocols.v1_protocol import V1RpcChannel, decode_data_protocol_message
|
|
67
|
+
from roborock.roborock_message import RoborockDataProtocol, RoborockMessage
|
|
65
68
|
from roborock.web_api import UserWebApiClient
|
|
66
69
|
|
|
67
70
|
from . import (
|
|
@@ -176,6 +179,7 @@ class PropertiesApi(Trait):
|
|
|
176
179
|
rpc_channel: V1RpcChannel,
|
|
177
180
|
mqtt_rpc_channel: V1RpcChannel,
|
|
178
181
|
map_rpc_channel: V1RpcChannel,
|
|
182
|
+
add_dps_listener: Callable[[Callable[[dict[RoborockDataProtocol, Any]], None]], Callable[[], None]],
|
|
179
183
|
web_api: UserWebApiClient,
|
|
180
184
|
device_cache: DeviceCache,
|
|
181
185
|
map_parser_config: MapParserConfig | None = None,
|
|
@@ -189,6 +193,8 @@ class PropertiesApi(Trait):
|
|
|
189
193
|
self._web_api = web_api
|
|
190
194
|
self._device_cache = device_cache
|
|
191
195
|
self._region = region
|
|
196
|
+
self._unsub: Callable[[], None] | None = None
|
|
197
|
+
self._add_dps_listener = add_dps_listener
|
|
192
198
|
|
|
193
199
|
self.device_features = DeviceFeaturesTrait(product, self._device_cache)
|
|
194
200
|
self.status = StatusTrait(self.device_features, region=self._region)
|
|
@@ -227,6 +233,29 @@ class PropertiesApi(Trait):
|
|
|
227
233
|
else:
|
|
228
234
|
return self._rpc_channel
|
|
229
235
|
|
|
236
|
+
async def start(self) -> None:
|
|
237
|
+
"""Start the properties API and discover features."""
|
|
238
|
+
if self._unsub:
|
|
239
|
+
return
|
|
240
|
+
await self.discover_features()
|
|
241
|
+
self._unsub = self._add_dps_listener(self._on_dps_update)
|
|
242
|
+
|
|
243
|
+
def close(self) -> None:
|
|
244
|
+
if self._unsub:
|
|
245
|
+
self._unsub()
|
|
246
|
+
self._unsub = None
|
|
247
|
+
|
|
248
|
+
def _on_dps_update(self, dps: dict[RoborockDataProtocol, Any]) -> None:
|
|
249
|
+
"""Handle incoming messages from the device.
|
|
250
|
+
|
|
251
|
+
This will notify all traits of the new values. This can be improved in
|
|
252
|
+
the future to be dynamic when we have more traits that support dynamic
|
|
253
|
+
updates but for now we just invoke them manually.
|
|
254
|
+
"""
|
|
255
|
+
_LOGGER.debug("Received message from device: %s", dps)
|
|
256
|
+
self.status.update_from_dps(dps)
|
|
257
|
+
self.consumables.update_from_dps(dps)
|
|
258
|
+
|
|
230
259
|
async def discover_features(self) -> None:
|
|
231
260
|
"""Populate any supported traits that were not initialized in __init__."""
|
|
232
261
|
_LOGGER.debug("Starting optional trait discovery")
|
|
@@ -330,6 +359,7 @@ def create(
|
|
|
330
359
|
rpc_channel: V1RpcChannel,
|
|
331
360
|
mqtt_rpc_channel: V1RpcChannel,
|
|
332
361
|
map_rpc_channel: V1RpcChannel,
|
|
362
|
+
add_dps_listener: Callable[[Callable[[dict[RoborockDataProtocol, Any]], None]], Callable[[], None]],
|
|
333
363
|
web_api: UserWebApiClient,
|
|
334
364
|
device_cache: DeviceCache,
|
|
335
365
|
map_parser_config: MapParserConfig | None = None,
|
|
@@ -343,6 +373,7 @@ def create(
|
|
|
343
373
|
rpc_channel,
|
|
344
374
|
mqtt_rpc_channel,
|
|
345
375
|
map_rpc_channel,
|
|
376
|
+
add_dps_listener,
|
|
346
377
|
web_api,
|
|
347
378
|
device_cache,
|
|
348
379
|
map_parser_config,
|
|
@@ -4,17 +4,24 @@ A consumable attribute is one that is expected to be replaced or refilled
|
|
|
4
4
|
periodically, such as filters, brushes, etc.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import logging
|
|
7
8
|
from enum import StrEnum
|
|
8
|
-
from typing import Self
|
|
9
|
+
from typing import Any, Self
|
|
9
10
|
|
|
10
11
|
from roborock.data import Consumable
|
|
12
|
+
from roborock.devices.traits.common import DpsDataConverter, TraitUpdateListener
|
|
11
13
|
from roborock.devices.traits.v1 import common
|
|
14
|
+
from roborock.roborock_message import RoborockDataProtocol
|
|
12
15
|
from roborock.roborock_typing import RoborockCommand
|
|
13
16
|
|
|
14
17
|
__all__ = [
|
|
15
18
|
"ConsumableTrait",
|
|
16
19
|
]
|
|
17
20
|
|
|
21
|
+
_LOGGER = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_DPS_CONVERTER = DpsDataConverter.from_dataclass(Consumable)
|
|
24
|
+
|
|
18
25
|
|
|
19
26
|
class ConsumableAttribute(StrEnum):
|
|
20
27
|
"""Enum for consumable attributes."""
|
|
@@ -35,7 +42,7 @@ class ConsumableAttribute(StrEnum):
|
|
|
35
42
|
raise ValueError(f"Unknown ConsumableAttribute: {value}")
|
|
36
43
|
|
|
37
44
|
|
|
38
|
-
class ConsumableTrait(Consumable, common.V1TraitMixin):
|
|
45
|
+
class ConsumableTrait(Consumable, common.V1TraitMixin, TraitUpdateListener):
|
|
39
46
|
"""Trait for managing consumable attributes on Roborock devices.
|
|
40
47
|
|
|
41
48
|
After the first refresh, you can tell what consumables are supported by
|
|
@@ -45,7 +52,21 @@ class ConsumableTrait(Consumable, common.V1TraitMixin):
|
|
|
45
52
|
command = RoborockCommand.GET_CONSUMABLE
|
|
46
53
|
converter = common.DefaultConverter(Consumable)
|
|
47
54
|
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
"""Initialize the consumable trait."""
|
|
57
|
+
super().__init__()
|
|
58
|
+
TraitUpdateListener.__init__(self, logger=_LOGGER)
|
|
59
|
+
|
|
48
60
|
async def reset_consumable(self, consumable: ConsumableAttribute) -> None:
|
|
49
61
|
"""Reset a specific consumable attribute on the device."""
|
|
50
62
|
await self.rpc_channel.send_command(RoborockCommand.RESET_CONSUMABLE, params=[consumable.value])
|
|
51
63
|
await self.refresh()
|
|
64
|
+
|
|
65
|
+
def update_from_dps(self, decoded_dps: dict[RoborockDataProtocol, Any]) -> None:
|
|
66
|
+
"""Update the trait from data protocol push message data.
|
|
67
|
+
|
|
68
|
+
This handles unsolicited status updates pushed by the device
|
|
69
|
+
via RoborockDataProtocol codes (e.g. STATE=121, BATTERY=122).
|
|
70
|
+
"""
|
|
71
|
+
if _DPS_CONVERTER.update_from_dps(self, decoded_dps):
|
|
72
|
+
self._notify_update()
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from functools import cached_property
|
|
3
|
+
from typing import Any
|
|
2
4
|
|
|
3
5
|
from roborock import (
|
|
4
6
|
CleanRoutes,
|
|
@@ -10,13 +12,19 @@ from roborock import (
|
|
|
10
12
|
get_water_mode_mapping,
|
|
11
13
|
get_water_modes,
|
|
12
14
|
)
|
|
15
|
+
from roborock.devices.traits.common import DpsDataConverter, TraitUpdateListener
|
|
16
|
+
from roborock.roborock_message import RoborockDataProtocol
|
|
13
17
|
from roborock.roborock_typing import RoborockCommand
|
|
14
18
|
|
|
15
19
|
from . import common
|
|
16
20
|
from .device_features import DeviceFeaturesTrait
|
|
17
21
|
|
|
22
|
+
_LOGGER = logging.getLogger(__name__)
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
_DPS_CONVERTER = DpsDataConverter.from_dataclass(StatusV2)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StatusTrait(StatusV2, common.V1TraitMixin, TraitUpdateListener):
|
|
20
28
|
"""Trait for managing the status of Roborock devices.
|
|
21
29
|
|
|
22
30
|
The StatusTrait gives you the access to the state of a Roborock vacuum.
|
|
@@ -47,6 +55,7 @@ class StatusTrait(StatusV2, common.V1TraitMixin):
|
|
|
47
55
|
def __init__(self, device_feature_trait: DeviceFeaturesTrait, region: str | None = None) -> None:
|
|
48
56
|
"""Initialize the StatusTrait."""
|
|
49
57
|
super().__init__()
|
|
58
|
+
TraitUpdateListener.__init__(self, logger=_LOGGER)
|
|
50
59
|
self._device_features_trait = device_feature_trait
|
|
51
60
|
self._region = region
|
|
52
61
|
|
|
@@ -91,3 +100,12 @@ class StatusTrait(StatusV2, common.V1TraitMixin):
|
|
|
91
100
|
if self.mop_mode is None:
|
|
92
101
|
return None
|
|
93
102
|
return self.mop_route_mapping.get(self.mop_mode)
|
|
103
|
+
|
|
104
|
+
def update_from_dps(self, decoded_dps: dict[RoborockDataProtocol, Any]) -> None:
|
|
105
|
+
"""Update the trait from data protocol push message data.
|
|
106
|
+
|
|
107
|
+
This handles unsolicited status updates pushed by the device
|
|
108
|
+
via RoborockDataProtocol codes (e.g. STATE=121, BATTERY=122).
|
|
109
|
+
"""
|
|
110
|
+
if _DPS_CONVERTER.update_from_dps(self, decoded_dps):
|
|
111
|
+
self._notify_update()
|
|
@@ -15,7 +15,7 @@ from typing import Any, Protocol, TypeVar, overload
|
|
|
15
15
|
from roborock.data import RoborockBase, RRiot
|
|
16
16
|
from roborock.exceptions import RoborockException, RoborockInvalidStatus, RoborockUnsupportedFeature
|
|
17
17
|
from roborock.protocol import Utils
|
|
18
|
-
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
|
|
18
|
+
from roborock.roborock_message import RoborockDataProtocol, RoborockMessage, RoborockMessageProtocol
|
|
19
19
|
from roborock.roborock_typing import RoborockCommand
|
|
20
20
|
from roborock.util import get_next_int, get_timestamp
|
|
21
21
|
|
|
@@ -24,6 +24,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
24
24
|
__all__ = [
|
|
25
25
|
"SecurityData",
|
|
26
26
|
"create_security_data",
|
|
27
|
+
"decode_data_protocol_message",
|
|
27
28
|
"decode_rpc_response",
|
|
28
29
|
"V1RpcChannel",
|
|
29
30
|
]
|
|
@@ -139,6 +140,28 @@ class ResponseMessage:
|
|
|
139
140
|
"""The API error message of the response if any."""
|
|
140
141
|
|
|
141
142
|
|
|
143
|
+
def _decode_dps_message(message: RoborockMessage) -> dict[int, Any] | None:
|
|
144
|
+
"""Decode a V1 push message containing data protocol updates."""
|
|
145
|
+
if not message.payload:
|
|
146
|
+
return None
|
|
147
|
+
try:
|
|
148
|
+
payload = json.loads(message.payload.decode())
|
|
149
|
+
except (json.JSONDecodeError, TypeError, UnicodeDecodeError) as e:
|
|
150
|
+
raise RoborockException(f"Invalid V1 message payload: {e} for {message.payload!r}") from e
|
|
151
|
+
|
|
152
|
+
datapoints = payload.get("dps")
|
|
153
|
+
if not isinstance(datapoints, dict):
|
|
154
|
+
return None
|
|
155
|
+
result: dict[int, Any] = {}
|
|
156
|
+
for key, value in datapoints.items():
|
|
157
|
+
try:
|
|
158
|
+
code = int(key)
|
|
159
|
+
except (ValueError, TypeError):
|
|
160
|
+
continue
|
|
161
|
+
result[code] = value
|
|
162
|
+
return result if result else None
|
|
163
|
+
|
|
164
|
+
|
|
142
165
|
def decode_rpc_response(message: RoborockMessage) -> ResponseMessage:
|
|
143
166
|
"""Decode a V1 RPC_RESPONSE message.
|
|
144
167
|
|
|
@@ -149,17 +172,13 @@ def decode_rpc_response(message: RoborockMessage) -> ResponseMessage:
|
|
|
149
172
|
"""
|
|
150
173
|
if not message.payload:
|
|
151
174
|
return ResponseMessage(request_id=message.seq, data={})
|
|
152
|
-
try:
|
|
153
|
-
payload = json.loads(message.payload.decode())
|
|
154
|
-
except (json.JSONDecodeError, TypeError, UnicodeDecodeError) as e:
|
|
155
|
-
raise RoborockException(f"Invalid V1 message payload: {e} for {message.payload!r}") from e
|
|
156
175
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
176
|
+
if (datapoints := _decode_dps_message(message)) is None:
|
|
177
|
+
raise RoborockException(
|
|
178
|
+
f"Invalid V1 message format: missing or invalid 'dps' in payload for {message.payload!r}"
|
|
179
|
+
)
|
|
161
180
|
|
|
162
|
-
if not (data_point := datapoints.get(
|
|
181
|
+
if not (data_point := datapoints.get(RoborockMessageProtocol.RPC_RESPONSE)):
|
|
163
182
|
raise RoborockException(
|
|
164
183
|
f"Invalid V1 message format: missing '{RoborockMessageProtocol.RPC_RESPONSE}' data point"
|
|
165
184
|
)
|
|
@@ -206,6 +225,31 @@ def decode_rpc_response(message: RoborockMessage) -> ResponseMessage:
|
|
|
206
225
|
return ResponseMessage(request_id=request_id, data=result, api_error=api_error)
|
|
207
226
|
|
|
208
227
|
|
|
228
|
+
def decode_data_protocol_message(message: RoborockMessage) -> dict[RoborockDataProtocol, Any] | None:
|
|
229
|
+
"""Decode a V1 push message containing data protocol updates.
|
|
230
|
+
|
|
231
|
+
V1 devices push unsolicited status updates containing data points keyed
|
|
232
|
+
by RoborockDataProtocol codes (e.g., 121=STATE, 122=BATTERY). This function
|
|
233
|
+
extracts those data points from the message payload.
|
|
234
|
+
|
|
235
|
+
Returns a dict mapping RoborockDataProtocol to values, or None if the
|
|
236
|
+
message does not contain any recognized data protocol updates.
|
|
237
|
+
"""
|
|
238
|
+
if (datapoints := _decode_dps_message(message)) is None:
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
result: dict[RoborockDataProtocol, Any] = {}
|
|
242
|
+
for code, value in datapoints.items():
|
|
243
|
+
try:
|
|
244
|
+
protocol = RoborockDataProtocol(code)
|
|
245
|
+
except ValueError:
|
|
246
|
+
_LOGGER.debug("Ignoring unknown V1 data protocol code: %s", code)
|
|
247
|
+
continue
|
|
248
|
+
result[protocol] = value
|
|
249
|
+
|
|
250
|
+
return result if result else None
|
|
251
|
+
|
|
252
|
+
|
|
209
253
|
@dataclass
|
|
210
254
|
class MapResponse:
|
|
211
255
|
"""Data structure for the V1 Map response."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/b01_q10/b01_q10_code_mappings.py
RENAMED
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/b01_q10/b01_q10_containers.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/data/b01_q7/b01_q7_code_mappings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/b01/q10/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/b01/q7/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/b01/q7/map_content.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/device_features.py
RENAMED
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/dust_collection_mode.py
RENAMED
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/flow_led_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/smart_wash_params.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/traits/v1/wash_towel_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-5.9.1 → python_roborock-5.10.0}/roborock/devices/transport/local_channel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|