python-roborock 4.13.0__tar.gz → 4.15.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-4.13.0 → python_roborock-4.15.0}/PKG-INFO +1 -1
- {python_roborock-4.13.0 → python_roborock-4.15.0}/pyproject.toml +1 -1
- python_roborock-4.15.0/roborock/data/b01_q10/b01_q10_containers.py +102 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/containers.py +20 -3
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/device.py +9 -5
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/rpc/b01_q10_channel.py +21 -0
- python_roborock-4.15.0/roborock/devices/traits/b01/q10/__init__.py +76 -0
- python_roborock-4.15.0/roborock/devices/traits/b01/q10/common.py +82 -0
- python_roborock-4.15.0/roborock/devices/traits/b01/q10/status.py +41 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/transport/mqtt_channel.py +17 -1
- python_roborock-4.13.0/roborock/data/b01_q10/b01_q10_containers.py +0 -53
- python_roborock-4.13.0/roborock/devices/traits/b01/q10/__init__.py +0 -31
- {python_roborock-4.13.0 → python_roborock-4.15.0}/.gitignore +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/LICENSE +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/README.md +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/callbacks.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/cli.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/const.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/code_mappings.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/v1/v1_containers.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/device_features.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/README.md +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/cache.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/device_manager.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/file_cache.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/rpc/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/rpc/a01_channel.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/rpc/v1_channel.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/b01/q10/command.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/common.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/transport/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/transport/channel.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/transport/local_channel.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/diagnostics.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/exceptions.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/map/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/map/map_parser.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/protocol.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/protocols/__init__.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/protocols/b01_q10_protocol.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/protocols/b01_q7_protocol.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/py.typed +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/roborock_message.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/util.py +0 -0
- {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.15.0
|
|
4
4
|
Summary: A package to control Roborock vacuums.
|
|
5
5
|
Project-URL: Repository, https://github.com/humbertogontijo/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 = "4.
|
|
3
|
+
version = "4.15.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"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Data container classes for Q10 B01 devices.
|
|
2
|
+
|
|
3
|
+
Many of these classes use the `field(metadata={"dps": ...})` convention to map
|
|
4
|
+
dataclass fields to device Data Points (DPS). This metadata is utilized by the
|
|
5
|
+
`update_from_dps` helper in `roborock.devices.traits.b01.q10.common` to
|
|
6
|
+
automatically update objects from raw device responses.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
from ..containers import RoborockBase
|
|
12
|
+
from .b01_q10_code_mappings import (
|
|
13
|
+
B01_Q10_DP,
|
|
14
|
+
YXBackType,
|
|
15
|
+
YXDeviceCleanTask,
|
|
16
|
+
YXDeviceState,
|
|
17
|
+
YXDeviceWorkMode,
|
|
18
|
+
YXFanLevel,
|
|
19
|
+
YXWaterLevel,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class dpCleanRecord(RoborockBase):
|
|
25
|
+
op: str
|
|
26
|
+
result: int
|
|
27
|
+
id: str
|
|
28
|
+
data: list
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class dpMultiMap(RoborockBase):
|
|
33
|
+
op: str
|
|
34
|
+
result: int
|
|
35
|
+
data: list
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class dpGetCarpet(RoborockBase):
|
|
40
|
+
op: str
|
|
41
|
+
result: int
|
|
42
|
+
data: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class dpSelfIdentifyingCarpet(RoborockBase):
|
|
47
|
+
op: str
|
|
48
|
+
result: int
|
|
49
|
+
data: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class dpNetInfo(RoborockBase):
|
|
54
|
+
wifiName: str
|
|
55
|
+
ipAdress: str
|
|
56
|
+
mac: str
|
|
57
|
+
signal: int
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class dpNotDisturbExpand(RoborockBase):
|
|
62
|
+
disturb_dust_enable: int
|
|
63
|
+
disturb_light: int
|
|
64
|
+
disturb_resume_clean: int
|
|
65
|
+
disturb_voice: int
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class dpCurrentCleanRoomIds(RoborockBase):
|
|
70
|
+
room_id_list: list
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class dpVoiceVersion(RoborockBase):
|
|
75
|
+
version: int
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class dpTimeZone(RoborockBase):
|
|
80
|
+
timeZoneCity: str
|
|
81
|
+
timeZoneSec: int
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class Q10Status(RoborockBase):
|
|
86
|
+
"""Status for Q10 devices.
|
|
87
|
+
|
|
88
|
+
Fields are mapped to DPS values using metadata. Objects of this class can be
|
|
89
|
+
automatically updated using the `update_from_dps` helper.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
clean_time: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TIME})
|
|
93
|
+
clean_area: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_AREA})
|
|
94
|
+
battery: int | None = field(default=None, metadata={"dps": B01_Q10_DP.BATTERY})
|
|
95
|
+
status: YXDeviceState | None = field(default=None, metadata={"dps": B01_Q10_DP.STATUS})
|
|
96
|
+
fan_level: YXFanLevel | None = field(default=None, metadata={"dps": B01_Q10_DP.FAN_LEVEL})
|
|
97
|
+
water_level: YXWaterLevel | None = field(default=None, metadata={"dps": B01_Q10_DP.WATER_LEVEL})
|
|
98
|
+
clean_count: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_COUNT})
|
|
99
|
+
clean_mode: YXDeviceWorkMode | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_MODE})
|
|
100
|
+
clean_task_type: YXDeviceCleanTask | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEAN_TASK_TYPE})
|
|
101
|
+
back_type: YXBackType | None = field(default=None, metadata={"dps": B01_Q10_DP.BACK_TYPE})
|
|
102
|
+
cleaning_progress: int | None = field(default=None, metadata={"dps": B01_Q10_DP.CLEANING_PROGRESS})
|
|
@@ -91,10 +91,10 @@ class RoborockBase:
|
|
|
91
91
|
if not isinstance(data, dict):
|
|
92
92
|
return None
|
|
93
93
|
field_types = {field.name: field.type for field in dataclasses.fields(cls)}
|
|
94
|
-
|
|
94
|
+
normalized_data: dict[str, Any] = {}
|
|
95
95
|
for orig_key, value in data.items():
|
|
96
96
|
key = _decamelize(orig_key)
|
|
97
|
-
if
|
|
97
|
+
if field_types.get(key) is None:
|
|
98
98
|
if (log_key := f"{cls.__name__}.{key}") not in RoborockBase._missing_logged:
|
|
99
99
|
_LOGGER.debug(
|
|
100
100
|
"Key '%s' (decamelized: '%s') not found in %s fields, skipping",
|
|
@@ -104,6 +104,23 @@ class RoborockBase:
|
|
|
104
104
|
)
|
|
105
105
|
RoborockBase._missing_logged.add(log_key)
|
|
106
106
|
continue
|
|
107
|
+
normalized_data[key] = value
|
|
108
|
+
|
|
109
|
+
result = RoborockBase.convert_dict(field_types, normalized_data)
|
|
110
|
+
return cls(**result)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def convert_dict(types_map: dict[Any, type], data: dict[Any, Any]) -> dict[Any, Any]:
|
|
114
|
+
"""Generic helper to convert a dictionary of values based on a schema map of types.
|
|
115
|
+
|
|
116
|
+
This is meant to be used by traits that use dataclass reflection similar to
|
|
117
|
+
`Roborock.from_dict` to merge in new data updates.
|
|
118
|
+
"""
|
|
119
|
+
result: dict[Any, Any] = {}
|
|
120
|
+
for key, value in data.items():
|
|
121
|
+
if key not in types_map:
|
|
122
|
+
continue
|
|
123
|
+
field_type = types_map[key]
|
|
107
124
|
if value == "None" or value is None:
|
|
108
125
|
result[key] = None
|
|
109
126
|
continue
|
|
@@ -124,7 +141,7 @@ class RoborockBase:
|
|
|
124
141
|
_LOGGER.exception(f"Failed to convert {key} with value {value} to type {field_type}")
|
|
125
142
|
continue
|
|
126
143
|
|
|
127
|
-
return
|
|
144
|
+
return result
|
|
128
145
|
|
|
129
146
|
def as_dict(self) -> dict:
|
|
130
147
|
return asdict(
|
|
@@ -197,12 +197,14 @@ class RoborockDevice(ABC, TraitsMixin):
|
|
|
197
197
|
if self._unsub:
|
|
198
198
|
raise ValueError("Already connected to the device")
|
|
199
199
|
unsub = await self._channel.subscribe(self._on_message)
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
try:
|
|
201
|
+
if self.v1_properties is not None:
|
|
202
202
|
await self.v1_properties.discover_features()
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
elif self.b01_q10_properties is not None:
|
|
204
|
+
await self.b01_q10_properties.start()
|
|
205
|
+
except RoborockException:
|
|
206
|
+
unsub()
|
|
207
|
+
raise
|
|
206
208
|
self._logger.info("Connected to device")
|
|
207
209
|
self._unsub = unsub
|
|
208
210
|
|
|
@@ -214,6 +216,8 @@ class RoborockDevice(ABC, TraitsMixin):
|
|
|
214
216
|
await self._connect_task
|
|
215
217
|
except asyncio.CancelledError:
|
|
216
218
|
pass
|
|
219
|
+
if self.b01_q10_properties is not None:
|
|
220
|
+
await self.b01_q10_properties.close()
|
|
217
221
|
if self._unsub:
|
|
218
222
|
self._unsub()
|
|
219
223
|
self._unsub = None
|
|
@@ -3,18 +3,39 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
from collections.abc import AsyncGenerator
|
|
7
|
+
from typing import Any
|
|
6
8
|
|
|
7
9
|
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
8
10
|
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
9
11
|
from roborock.exceptions import RoborockException
|
|
10
12
|
from roborock.protocols.b01_q10_protocol import (
|
|
11
13
|
ParamsType,
|
|
14
|
+
decode_rpc_response,
|
|
12
15
|
encode_mqtt_payload,
|
|
13
16
|
)
|
|
14
17
|
|
|
15
18
|
_LOGGER = logging.getLogger(__name__)
|
|
16
19
|
|
|
17
20
|
|
|
21
|
+
async def stream_decoded_responses(
|
|
22
|
+
mqtt_channel: MqttChannel,
|
|
23
|
+
) -> AsyncGenerator[dict[B01_Q10_DP, Any], None]:
|
|
24
|
+
"""Stream decoded DPS messages received via MQTT."""
|
|
25
|
+
|
|
26
|
+
async for response_message in mqtt_channel.subscribe_stream():
|
|
27
|
+
try:
|
|
28
|
+
decoded_dps = decode_rpc_response(response_message)
|
|
29
|
+
except RoborockException as ex:
|
|
30
|
+
_LOGGER.debug(
|
|
31
|
+
"Failed to decode B01 Q10 RPC response: %s: %s",
|
|
32
|
+
response_message,
|
|
33
|
+
ex,
|
|
34
|
+
)
|
|
35
|
+
continue
|
|
36
|
+
yield decoded_dps
|
|
37
|
+
|
|
38
|
+
|
|
18
39
|
async def send_command(
|
|
19
40
|
mqtt_channel: MqttChannel,
|
|
20
41
|
command: B01_Q10_DP,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Traits for Q10 B01 devices."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
8
|
+
from roborock.devices.rpc.b01_q10_channel import stream_decoded_responses
|
|
9
|
+
from roborock.devices.traits import Trait
|
|
10
|
+
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
11
|
+
|
|
12
|
+
from .command import CommandTrait
|
|
13
|
+
from .status import StatusTrait
|
|
14
|
+
from .vacuum import VacuumTrait
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Q10PropertiesApi",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
_LOGGER = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Q10PropertiesApi(Trait):
|
|
24
|
+
"""API for interacting with B01 devices."""
|
|
25
|
+
|
|
26
|
+
command: CommandTrait
|
|
27
|
+
"""Trait for sending commands to Q10 devices."""
|
|
28
|
+
|
|
29
|
+
status: StatusTrait
|
|
30
|
+
"""Trait for managing the status of Q10 devices."""
|
|
31
|
+
|
|
32
|
+
vacuum: VacuumTrait
|
|
33
|
+
"""Trait for sending vacuum related commands to Q10 devices."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, channel: MqttChannel) -> None:
|
|
36
|
+
"""Initialize the B01Props API."""
|
|
37
|
+
self._channel = channel
|
|
38
|
+
self.command = CommandTrait(channel)
|
|
39
|
+
self.vacuum = VacuumTrait(self.command)
|
|
40
|
+
self.status = StatusTrait()
|
|
41
|
+
self._subscribe_task: asyncio.Task[None] | None = None
|
|
42
|
+
|
|
43
|
+
async def start(self) -> None:
|
|
44
|
+
"""Start any necessary subscriptions for the trait."""
|
|
45
|
+
self._subscribe_task = asyncio.create_task(self._subscribe_loop())
|
|
46
|
+
|
|
47
|
+
async def close(self) -> None:
|
|
48
|
+
"""Close any resources held by the trait."""
|
|
49
|
+
if self._subscribe_task is not None:
|
|
50
|
+
self._subscribe_task.cancel()
|
|
51
|
+
try:
|
|
52
|
+
await self._subscribe_task
|
|
53
|
+
except asyncio.CancelledError:
|
|
54
|
+
pass # ignore cancellation errors
|
|
55
|
+
self._subscribe_task = None
|
|
56
|
+
|
|
57
|
+
async def refresh(self) -> None:
|
|
58
|
+
"""Refresh all traits."""
|
|
59
|
+
# Sending the REQUEST_DPS will cause the device to send all DPS values
|
|
60
|
+
# to the device. Updates will be received by the subscribe loop below.
|
|
61
|
+
await self.command.send(B01_Q10_DP.REQUEST_DPS, params={})
|
|
62
|
+
|
|
63
|
+
async def _subscribe_loop(self) -> None:
|
|
64
|
+
"""Persistent loop to listen for status updates."""
|
|
65
|
+
async for decoded_dps in stream_decoded_responses(self._channel):
|
|
66
|
+
_LOGGER.debug("Received Q10 status update: %s", decoded_dps)
|
|
67
|
+
|
|
68
|
+
# Notify all traits about a new message and each trait will
|
|
69
|
+
# only update what fields that it is responsible for.
|
|
70
|
+
# More traits can be added here below.
|
|
71
|
+
self.status.update_from_dps(decoded_dps)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def create(channel: MqttChannel) -> Q10PropertiesApi:
|
|
75
|
+
"""Create traits for B01 devices."""
|
|
76
|
+
return Q10PropertiesApi(channel)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Common utilities for Q10 traits.
|
|
2
|
+
|
|
3
|
+
This module provides infrastructure for mapping Roborock Data Points (DPS) to
|
|
4
|
+
Python dataclass fields and handling the lifecycle of data updates from the
|
|
5
|
+
device.
|
|
6
|
+
|
|
7
|
+
### DPS Metadata Annotation
|
|
8
|
+
|
|
9
|
+
Classes extending `RoborockBase` can annotate their fields with DPS IDs using
|
|
10
|
+
the `field(metadata={"dps": ...})` convention. This creates a declarative
|
|
11
|
+
mapping that `DpsDataConverter` uses to automatically route incoming device
|
|
12
|
+
data to the correct attribute.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
@dataclass
|
|
18
|
+
class MyStatus(RoborockBase):
|
|
19
|
+
battery: int = field(metadata={"dps": B01_Q10_DP.BATTERY})
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Update Lifecycle
|
|
23
|
+
1. **Raw Data**: The device sends encoded DPS updates over MQTT.
|
|
24
|
+
2. **Decoding**: The transport layer decodes these into a dictionary (e.g., `{"101": 80}`).
|
|
25
|
+
3. **Conversion**: `DpsDataConverter` uses `RoborockBase.convert_dict` to transform
|
|
26
|
+
raw values into appropriate Python types (e.g., Enums, ints) based on the
|
|
27
|
+
dataclass field types.
|
|
28
|
+
4. **Update**: `update_from_dps` maps these converted values to field names and
|
|
29
|
+
updates the target object using `setattr`.
|
|
30
|
+
|
|
31
|
+
### Usage
|
|
32
|
+
|
|
33
|
+
Typically, a trait will instantiate a single `DpsDataConverter` for its status class
|
|
34
|
+
and call `update_from_dps` whenever new data is received from the device stream.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import dataclasses
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
42
|
+
from roborock.data.containers import RoborockBase
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DpsDataConverter:
|
|
46
|
+
"""Utility to handle the transformation and merging of DPS data into models.
|
|
47
|
+
|
|
48
|
+
This class pre-calculates the mapping between Data Point IDs and dataclass fields
|
|
49
|
+
to optimize repeated updates from device streams.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, dps_type_map: dict[B01_Q10_DP, type], dps_field_map: dict[B01_Q10_DP, str]):
|
|
53
|
+
"""Initialize the converter for a specific RoborockBase-derived class."""
|
|
54
|
+
self._dps_type_map = dps_type_map
|
|
55
|
+
self._dps_field_map = dps_field_map
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_dataclass(cls, dataclass_type: type[RoborockBase]):
|
|
59
|
+
"""Initialize the converter for a specific RoborockBase-derived class."""
|
|
60
|
+
dps_type_map: dict[B01_Q10_DP, type] = {}
|
|
61
|
+
dps_field_map: dict[B01_Q10_DP, str] = {}
|
|
62
|
+
for field_obj in dataclasses.fields(dataclass_type):
|
|
63
|
+
if field_obj.metadata and "dps" in field_obj.metadata:
|
|
64
|
+
dps_id = field_obj.metadata["dps"]
|
|
65
|
+
dps_type_map[dps_id] = field_obj.type
|
|
66
|
+
dps_field_map[dps_id] = field_obj.name
|
|
67
|
+
return cls(dps_type_map, dps_field_map)
|
|
68
|
+
|
|
69
|
+
def update_from_dps(self, target: RoborockBase, decoded_dps: dict[B01_Q10_DP, Any]) -> None:
|
|
70
|
+
"""Convert and merge raw DPS data into the target object.
|
|
71
|
+
|
|
72
|
+
Uses the pre-calculated type mapping to ensure values are converted to the
|
|
73
|
+
correct Python types before being updated on the target.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
target: The target object to update.
|
|
77
|
+
decoded_dps: The decoded DPS data to convert.
|
|
78
|
+
"""
|
|
79
|
+
conversions = RoborockBase.convert_dict(self._dps_type_map, decoded_dps)
|
|
80
|
+
for dps_id, value in conversions.items():
|
|
81
|
+
field_name = self._dps_field_map[dps_id]
|
|
82
|
+
setattr(target, field_name, value)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Status trait for Q10 B01 devices."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from roborock.callbacks import CallbackList
|
|
8
|
+
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
|
|
9
|
+
from roborock.data.b01_q10.b01_q10_containers import Q10Status
|
|
10
|
+
|
|
11
|
+
from .common import DpsDataConverter
|
|
12
|
+
|
|
13
|
+
_LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
_CONVERTER = DpsDataConverter.from_dataclass(Q10Status)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StatusTrait(Q10Status):
|
|
19
|
+
"""Trait for managing the status of Q10 Roborock devices.
|
|
20
|
+
|
|
21
|
+
This is a thin wrapper around Q10Status that provides the Trait interface.
|
|
22
|
+
The current values reflect the most recently received data from the device.
|
|
23
|
+
New values can be requested through the `Q10PropertiesApi`'s `refresh` method.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
"""Initialize the status trait."""
|
|
28
|
+
super().__init__()
|
|
29
|
+
self._update_callbacks: CallbackList[dict[B01_Q10_DP, Any]] = CallbackList(logger=_LOGGER)
|
|
30
|
+
|
|
31
|
+
def add_update_listener(self, callback: Callable[[dict[B01_Q10_DP, Any]], None]) -> Callable[[], None]:
|
|
32
|
+
"""Register a callback for decoded DPS updates.
|
|
33
|
+
|
|
34
|
+
Returns a callable to remove the listener.
|
|
35
|
+
"""
|
|
36
|
+
return self._update_callbacks.add_callback(callback)
|
|
37
|
+
|
|
38
|
+
def update_from_dps(self, decoded_dps: dict[B01_Q10_DP, Any]) -> None:
|
|
39
|
+
"""Update the trait from raw DPS data."""
|
|
40
|
+
_CONVERTER.update_from_dps(self, decoded_dps)
|
|
41
|
+
self._update_callbacks(decoded_dps)
|
{python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/transport/mqtt_channel.py
RENAMED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Modules for communicating with specific Roborock devices over MQTT."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
4
|
-
from collections.abc import Callable
|
|
5
|
+
from collections.abc import AsyncGenerator, Callable
|
|
5
6
|
|
|
6
7
|
from roborock.callbacks import decoder_callback
|
|
7
8
|
from roborock.data import HomeDataDevice, RRiot, UserData
|
|
@@ -73,6 +74,21 @@ class MqttChannel(Channel):
|
|
|
73
74
|
dispatch = decoder_callback(self._decoder, callback, _LOGGER)
|
|
74
75
|
return await self._mqtt_session.subscribe(self._subscribe_topic, dispatch)
|
|
75
76
|
|
|
77
|
+
async def subscribe_stream(self) -> AsyncGenerator[RoborockMessage, None]:
|
|
78
|
+
"""Subscribe to the device's message stream.
|
|
79
|
+
|
|
80
|
+
This is useful for processing all incoming messages in an async for loop,
|
|
81
|
+
when they are not necessarily associated with a specific request.
|
|
82
|
+
"""
|
|
83
|
+
message_queue: asyncio.Queue[RoborockMessage] = asyncio.Queue()
|
|
84
|
+
unsub = await self.subscribe(message_queue.put_nowait)
|
|
85
|
+
try:
|
|
86
|
+
while True:
|
|
87
|
+
message = await message_queue.get()
|
|
88
|
+
yield message
|
|
89
|
+
finally:
|
|
90
|
+
unsub()
|
|
91
|
+
|
|
76
92
|
async def publish(self, message: RoborockMessage) -> None:
|
|
77
93
|
"""Publish a command message.
|
|
78
94
|
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
from ..containers import RoborockBase
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class dpCleanRecord(RoborockBase):
|
|
5
|
-
op: str
|
|
6
|
-
result: int
|
|
7
|
-
id: str
|
|
8
|
-
data: list
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class dpMultiMap(RoborockBase):
|
|
12
|
-
op: str
|
|
13
|
-
result: int
|
|
14
|
-
data: list
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class dpGetCarpet(RoborockBase):
|
|
18
|
-
op: str
|
|
19
|
-
result: int
|
|
20
|
-
data: str
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class dpSelfIdentifyingCarpet(RoborockBase):
|
|
24
|
-
op: str
|
|
25
|
-
result: int
|
|
26
|
-
data: str
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class dpNetInfo(RoborockBase):
|
|
30
|
-
wifiName: str
|
|
31
|
-
ipAdress: str
|
|
32
|
-
mac: str
|
|
33
|
-
signal: int
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class dpNotDisturbExpand(RoborockBase):
|
|
37
|
-
disturb_dust_enable: int
|
|
38
|
-
disturb_light: int
|
|
39
|
-
disturb_resume_clean: int
|
|
40
|
-
disturb_voice: int
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class dpCurrentCleanRoomIds(RoborockBase):
|
|
44
|
-
room_id_list: list
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class dpVoiceVersion(RoborockBase):
|
|
48
|
-
version: int
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class dpTimeZone(RoborockBase):
|
|
52
|
-
timeZoneCity: str
|
|
53
|
-
timeZoneSec: int
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
"""Traits for Q10 B01 devices."""
|
|
2
|
-
|
|
3
|
-
from roborock.devices.traits import Trait
|
|
4
|
-
from roborock.devices.transport.mqtt_channel import MqttChannel
|
|
5
|
-
|
|
6
|
-
from .command import CommandTrait
|
|
7
|
-
from .vacuum import VacuumTrait
|
|
8
|
-
|
|
9
|
-
__all__ = [
|
|
10
|
-
"Q10PropertiesApi",
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Q10PropertiesApi(Trait):
|
|
15
|
-
"""API for interacting with B01 devices."""
|
|
16
|
-
|
|
17
|
-
command: CommandTrait
|
|
18
|
-
"""Trait for sending commands to Q10 devices."""
|
|
19
|
-
|
|
20
|
-
vacuum: VacuumTrait
|
|
21
|
-
"""Trait for sending vacuum related commands to Q10 devices."""
|
|
22
|
-
|
|
23
|
-
def __init__(self, channel: MqttChannel) -> None:
|
|
24
|
-
"""Initialize the B01Props API."""
|
|
25
|
-
self.command = CommandTrait(channel)
|
|
26
|
-
self.vacuum = VacuumTrait(self.command)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def create(channel: MqttChannel) -> Q10PropertiesApi:
|
|
30
|
-
"""Create traits for B01 devices."""
|
|
31
|
-
return Q10PropertiesApi(channel)
|
|
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-4.13.0 → python_roborock-4.15.0}/roborock/data/b01_q10/b01_q10_code_mappings.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-4.13.0 → python_roborock-4.15.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
|
|
File without changes
|
{python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/b01/q10/command.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/b01/q7/__init__.py
RENAMED
|
File without changes
|
{python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/b01/q7/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/device_features.py
RENAMED
|
File without changes
|
{python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/dust_collection_mode.py
RENAMED
|
File without changes
|
{python_roborock-4.13.0 → python_roborock-4.15.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
|
{python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/network_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/smart_wash_params.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/wash_towel_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-4.13.0 → python_roborock-4.15.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
|