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.
Files changed (99) hide show
  1. {python_roborock-4.13.0 → python_roborock-4.15.0}/PKG-INFO +1 -1
  2. {python_roborock-4.13.0 → python_roborock-4.15.0}/pyproject.toml +1 -1
  3. python_roborock-4.15.0/roborock/data/b01_q10/b01_q10_containers.py +102 -0
  4. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/containers.py +20 -3
  5. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/device.py +9 -5
  6. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/rpc/b01_q10_channel.py +21 -0
  7. python_roborock-4.15.0/roborock/devices/traits/b01/q10/__init__.py +76 -0
  8. python_roborock-4.15.0/roborock/devices/traits/b01/q10/common.py +82 -0
  9. python_roborock-4.15.0/roborock/devices/traits/b01/q10/status.py +41 -0
  10. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/transport/mqtt_channel.py +17 -1
  11. python_roborock-4.13.0/roborock/data/b01_q10/b01_q10_containers.py +0 -53
  12. python_roborock-4.13.0/roborock/devices/traits/b01/q10/__init__.py +0 -31
  13. {python_roborock-4.13.0 → python_roborock-4.15.0}/.gitignore +0 -0
  14. {python_roborock-4.13.0 → python_roborock-4.15.0}/LICENSE +0 -0
  15. {python_roborock-4.13.0 → python_roborock-4.15.0}/README.md +0 -0
  16. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/__init__.py +0 -0
  17. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/broadcast_protocol.py +0 -0
  18. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/callbacks.py +0 -0
  19. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/cli.py +0 -0
  20. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/const.py +0 -0
  21. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/__init__.py +0 -0
  22. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/b01_q10/__init__.py +0 -0
  23. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  24. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/b01_q7/__init__.py +0 -0
  25. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  26. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  27. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/code_mappings.py +0 -0
  28. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/dyad/__init__.py +0 -0
  29. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  30. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/dyad/dyad_containers.py +0 -0
  31. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/v1/__init__.py +0 -0
  32. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  33. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  34. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/v1/v1_containers.py +0 -0
  35. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/zeo/__init__.py +0 -0
  36. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  37. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/data/zeo/zeo_containers.py +0 -0
  38. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/device_features.py +0 -0
  39. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/README.md +0 -0
  40. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/__init__.py +0 -0
  41. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/cache.py +0 -0
  42. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/device_manager.py +0 -0
  43. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/file_cache.py +0 -0
  44. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/rpc/__init__.py +0 -0
  45. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/rpc/a01_channel.py +0 -0
  46. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
  47. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/rpc/v1_channel.py +0 -0
  48. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/__init__.py +0 -0
  49. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/a01/__init__.py +0 -0
  50. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/b01/__init__.py +0 -0
  51. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/b01/q10/command.py +0 -0
  52. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
  53. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
  54. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
  55. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/traits_mixin.py +0 -0
  56. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/__init__.py +0 -0
  57. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  58. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  59. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/command.py +0 -0
  60. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/common.py +0 -0
  61. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  62. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/device_features.py +0 -0
  63. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  64. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  65. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  66. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/home.py +0 -0
  67. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/led_status.py +0 -0
  68. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/map_content.py +0 -0
  69. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/maps.py +0 -0
  70. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/network_info.py +0 -0
  71. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/rooms.py +0 -0
  72. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/routines.py +0 -0
  73. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  74. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/status.py +0 -0
  75. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  76. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/volume.py +0 -0
  77. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  78. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/transport/__init__.py +0 -0
  79. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/transport/channel.py +0 -0
  80. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/devices/transport/local_channel.py +0 -0
  81. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/diagnostics.py +0 -0
  82. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/exceptions.py +0 -0
  83. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/map/__init__.py +0 -0
  84. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/map/map_parser.py +0 -0
  85. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/mqtt/__init__.py +0 -0
  86. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/mqtt/health_manager.py +0 -0
  87. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/mqtt/roborock_session.py +0 -0
  88. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/mqtt/session.py +0 -0
  89. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/protocol.py +0 -0
  90. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/protocols/__init__.py +0 -0
  91. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/protocols/a01_protocol.py +0 -0
  92. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/protocols/b01_q10_protocol.py +0 -0
  93. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/protocols/b01_q7_protocol.py +0 -0
  94. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/protocols/v1_protocol.py +0 -0
  95. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/py.typed +0 -0
  96. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/roborock_message.py +0 -0
  97. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/roborock_typing.py +0 -0
  98. {python_roborock-4.13.0 → python_roborock-4.15.0}/roborock/util.py +0 -0
  99. {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.13.0
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.13.0"
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
- result: dict[str, Any] = {}
94
+ normalized_data: dict[str, Any] = {}
95
95
  for orig_key, value in data.items():
96
96
  key = _decamelize(orig_key)
97
- if (field_type := field_types.get(key)) is None:
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 cls(**result)
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
- if self.v1_properties is not None:
201
- try:
200
+ try:
201
+ if self.v1_properties is not None:
202
202
  await self.v1_properties.discover_features()
203
- except RoborockException:
204
- unsub()
205
- raise
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)
@@ -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)