python-roborock 5.7.1__tar.gz → 5.9.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 (103) hide show
  1. {python_roborock-5.7.1 → python_roborock-5.9.0}/PKG-INFO +1 -1
  2. {python_roborock-5.7.1 → python_roborock-5.9.0}/pyproject.toml +1 -1
  3. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/v1/v1_code_mappings.py +55 -0
  4. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/v1/v1_containers.py +44 -3
  5. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/b01/q10/status.py +1 -2
  6. {python_roborock-5.7.1/roborock/devices/traits/b01/q10 → python_roborock-5.9.0/roborock/devices/traits}/common.py +12 -13
  7. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/clean_summary.py +10 -1
  8. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/common.py +11 -2
  9. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/rooms.py +16 -2
  10. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/exceptions.py +14 -0
  11. {python_roborock-5.7.1 → python_roborock-5.9.0}/.gitignore +0 -0
  12. {python_roborock-5.7.1 → python_roborock-5.9.0}/LICENSE +0 -0
  13. {python_roborock-5.7.1 → python_roborock-5.9.0}/README.md +0 -0
  14. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/__init__.py +0 -0
  15. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/broadcast_protocol.py +0 -0
  16. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/callbacks.py +0 -0
  17. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/cli.py +0 -0
  18. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/const.py +0 -0
  19. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/__init__.py +0 -0
  20. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/b01_q10/__init__.py +0 -0
  21. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  22. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  23. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/b01_q7/__init__.py +0 -0
  24. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  25. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  26. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/code_mappings.py +0 -0
  27. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/containers.py +0 -0
  28. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/dyad/__init__.py +0 -0
  29. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  30. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/dyad/dyad_containers.py +0 -0
  31. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/v1/__init__.py +0 -0
  32. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  33. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/zeo/__init__.py +0 -0
  34. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  35. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/data/zeo/zeo_containers.py +0 -0
  36. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/device_features.py +0 -0
  37. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/README.md +0 -0
  38. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/__init__.py +0 -0
  39. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/cache.py +0 -0
  40. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/device.py +0 -0
  41. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/device_manager.py +0 -0
  42. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/file_cache.py +0 -0
  43. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/rpc/__init__.py +0 -0
  44. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/rpc/a01_channel.py +0 -0
  45. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
  46. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
  47. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/rpc/v1_channel.py +0 -0
  48. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/__init__.py +0 -0
  49. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/a01/__init__.py +0 -0
  50. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/b01/__init__.py +0 -0
  51. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  52. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/b01/q10/command.py +0 -0
  53. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
  54. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
  55. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
  56. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/b01/q7/map.py +0 -0
  57. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/b01/q7/map_content.py +0 -0
  58. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/traits_mixin.py +0 -0
  59. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/__init__.py +0 -0
  60. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  61. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/command.py +0 -0
  62. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  63. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/device_features.py +0 -0
  64. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  65. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  66. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  67. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/home.py +0 -0
  68. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/led_status.py +0 -0
  69. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/map_content.py +0 -0
  70. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/maps.py +0 -0
  71. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/network_info.py +0 -0
  72. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/routines.py +0 -0
  73. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  74. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/status.py +0 -0
  75. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  76. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/volume.py +0 -0
  77. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  78. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/transport/__init__.py +0 -0
  79. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/transport/channel.py +0 -0
  80. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/transport/local_channel.py +0 -0
  81. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/devices/transport/mqtt_channel.py +0 -0
  82. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/diagnostics.py +0 -0
  83. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/map/__init__.py +0 -0
  84. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/map/b01_map_parser.py +0 -0
  85. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/map/map_parser.py +0 -0
  86. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/map/proto/__init__.py +0 -0
  87. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/map/proto/b01_scmap.proto +0 -0
  88. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/map/proto/b01_scmap_pb2.py +0 -0
  89. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/mqtt/__init__.py +0 -0
  90. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/mqtt/health_manager.py +0 -0
  91. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/mqtt/roborock_session.py +0 -0
  92. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/mqtt/session.py +0 -0
  93. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/protocol.py +0 -0
  94. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/protocols/__init__.py +0 -0
  95. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/protocols/a01_protocol.py +0 -0
  96. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/protocols/b01_q10_protocol.py +0 -0
  97. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/protocols/b01_q7_protocol.py +0 -0
  98. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/protocols/v1_protocol.py +0 -0
  99. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/py.typed +0 -0
  100. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/roborock_message.py +0 -0
  101. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/roborock_typing.py +0 -0
  102. {python_roborock-5.7.1 → python_roborock-5.9.0}/roborock/util.py +0 -0
  103. {python_roborock-5.7.1 → python_roborock-5.9.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.7.1
3
+ Version: 5.9.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.7.1"
3
+ version = "5.9.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"
@@ -1,3 +1,4 @@
1
+ from enum import StrEnum
1
2
  from typing import Self
2
3
 
3
4
  from ..code_mappings import RoborockEnum
@@ -63,6 +64,60 @@ class RoborockCleanType(RoborockEnum):
63
64
  pet_patrol = 6
64
65
 
65
66
 
67
+ class RoborockChargeStatus(RoborockEnum):
68
+ """Describes the charging status of the device."""
69
+
70
+ unknown = -1
71
+ charge_waiting = 0
72
+ charging = 1
73
+
74
+
75
+ class RoborockDockState(StrEnum):
76
+ """Synthesized high-level dock and power state of the device.
77
+
78
+ This enum represents a unified "UI-level" state that combines multiple raw
79
+ device data points (`state`, `charge_status`, `battery`) into a single,
80
+ human-readable status that accurately reflects what the vacuum is doing
81
+ relative to the dock.
82
+
83
+ It is highly recommended for consumers of this API
84
+ to use this synthesized state to determine if the vacuum is charging or
85
+ docked, rather than attempting to parse the raw integer data points, as
86
+ this safely handles backward compatibility for older models that lack
87
+ explicit off-peak schedule reporting.
88
+ """
89
+
90
+ unknown = "unknown"
91
+ """The dock state could not be determined or is unmapped."""
92
+
93
+ idle = "idle"
94
+ """The vacuum is away from the dock (e.g., cleaning, paused, or errored).
95
+ In the official app, this state presents the 'Return to Dock' or 'Recharge' action."""
96
+
97
+ returning = "returning"
98
+ """The vacuum is actively navigating its way back to the dock.
99
+ In the official app, this state presents the 'Stop' or 'Pause' action."""
100
+
101
+ charging = "charging"
102
+ """The vacuum is on the dock and actively receiving electricity.
103
+ In the official app, this state is displayed as 'Charging'."""
104
+
105
+ off_peak_waiting = "off_peak_waiting"
106
+ """The vacuum is on the dock but charging is paused. It is waiting for the
107
+ user's scheduled 'Valley Electricity' off-peak hours to begin before
108
+ drawing power.
109
+ In the official app, this state is displayed as 'Charging paused during peak hours'."""
110
+
111
+ full = "full"
112
+ """The vacuum is on the dock and the battery is at 100% capacity.
113
+ In the official app, this state is displayed as 'Fully charged'."""
114
+
115
+ dusting = "dusting"
116
+ """The vacuum is on the dock and is currently being evacuated by the
117
+ auto-empty base.
118
+ In the official app, this state is displayed as 'Emptying dustbin'."""
119
+
120
+
66
121
  class RoborockStartType(RoborockEnum):
67
122
  button = 1
68
123
  app = 2
@@ -46,9 +46,11 @@ from .v1_code_mappings import (
46
46
  ClearWaterBoxStatus,
47
47
  DirtyWaterBoxStatus,
48
48
  DustBagStatus,
49
+ RoborockChargeStatus,
49
50
  RoborockCleanType,
50
51
  RoborockDockDustCollectionModeCode,
51
52
  RoborockDockErrorCode,
53
+ RoborockDockState,
52
54
  RoborockDockTypeCode,
53
55
  RoborockErrorCode,
54
56
  RoborockFanPowerCode,
@@ -161,7 +163,9 @@ class Status(RoborockBase):
161
163
  collision_avoid_status: int | None = None
162
164
  switch_map_mode: int | None = None
163
165
  dock_error_status: RoborockDockErrorCode | None = None
164
- charge_status: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.CHARGE_STATUS})
166
+ charge_status: RoborockChargeStatus | None = field(
167
+ default=None, metadata={"dps": RoborockDataProtocol.CHARGE_STATUS}
168
+ )
165
169
  unsave_map_reason: int | None = None
166
170
  unsave_map_flag: int | None = None
167
171
  wash_status: int | None = None
@@ -329,7 +333,9 @@ class StatusV2(RoborockBase):
329
333
  collision_avoid_status: int | None = None
330
334
  switch_map_mode: int | None = None
331
335
  dock_error_status: RoborockDockErrorCode | None = None
332
- charge_status: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.CHARGE_STATUS})
336
+ charge_status: RoborockChargeStatus | None = field(
337
+ default=None, metadata={"dps": RoborockDataProtocol.CHARGE_STATUS}
338
+ )
333
339
  unsave_map_reason: int | None = None
334
340
  unsave_map_flag: int | None = None
335
341
  wash_status: int | None = None
@@ -414,6 +420,41 @@ class StatusV2(RoborockBase):
414
420
  return (self.dss >> 15) & 3
415
421
  return None
416
422
 
423
+ @property
424
+ def dock_state(self) -> RoborockDockState:
425
+ """A synthesized, high-level dock state reflecting the UI's display.
426
+
427
+ This property simplifies integration by handling the complex logic
428
+ of checking state, charge_status, and battery level simultaneously. It handles
429
+ newer off-peak charging logic seamlessly while maintaining backwards compatibility
430
+ with older devices.
431
+ """
432
+ if self.state is None or self.state == RoborockStateCode.unknown:
433
+ return RoborockDockState.unknown
434
+
435
+ # 6. DUSTING
436
+ if self.state == RoborockStateCode.emptying_the_bin:
437
+ return RoborockDockState.dusting
438
+
439
+ # 5. FULL
440
+ if self.state == RoborockStateCode.charging_complete or (
441
+ self.state == RoborockStateCode.charging and self.battery == 100
442
+ ):
443
+ return RoborockDockState.full
444
+
445
+ # 3 & 4. CHARGING and CHARGE_WAITING
446
+ if self.state == RoborockStateCode.charging:
447
+ if self.charge_status == RoborockChargeStatus.charge_waiting:
448
+ return RoborockDockState.off_peak_waiting
449
+ return RoborockDockState.charging
450
+
451
+ # 2. RECHARGING
452
+ if self.state in (RoborockStateCode.returning_home, RoborockStateCode.docking):
453
+ return RoborockDockState.returning
454
+
455
+ # 1. IDLE (Not on dock, or doing something else)
456
+ return RoborockDockState.idle
457
+
417
458
  def __repr__(self) -> str:
418
459
  return _attr_repr(self)
419
460
 
@@ -792,7 +833,7 @@ class AppInitStatusLocalInfo(RoborockBase):
792
833
  class AppInitStatus(RoborockBase):
793
834
  local_info: AppInitStatusLocalInfo
794
835
  feature_info: list[int]
795
- new_feature_info: int
836
+ new_feature_info: int = 0
796
837
  new_feature_info_str: str = ""
797
838
  new_feature_info_2: int | None = None
798
839
  carriage_type: int | None = None
@@ -5,8 +5,7 @@ from typing import Any
5
5
 
6
6
  from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
7
7
  from roborock.data.b01_q10.b01_q10_containers import Q10Status
8
-
9
- from .common import DpsDataConverter, TraitUpdateListener
8
+ from roborock.devices.traits.common import DpsDataConverter, TraitUpdateListener
10
9
 
11
10
  _LOGGER = logging.getLogger(__name__)
12
11
 
@@ -1,8 +1,7 @@
1
- """Common utilities for Q10 traits.
1
+ """Common utilities for device traits.
2
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.
3
+ This module provides shared infrastructure for mapping Roborock Data Points (DPS) to
4
+ Python dataclass fields and handling the lifecycle of data updates from the device.
6
5
 
7
6
  ### DPS Metadata Annotation
8
7
 
@@ -21,7 +20,7 @@ class MyStatus(RoborockBase):
21
20
 
22
21
  ### Update Lifecycle
23
22
  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}`).
23
+ 2. **Decoding**: The transport layer decodes these into a dictionary (e.g., {"101": 80}).
25
24
  3. **Conversion**: `DpsDataConverter` uses `RoborockBase.convert_dict` to transform
26
25
  raw values into appropriate Python types (e.g., Enums, ints) based on the
27
26
  dataclass field types.
@@ -32,18 +31,18 @@ class MyStatus(RoborockBase):
32
31
 
33
32
  Typically, a trait will instantiate a single `DpsDataConverter` for its status class
34
33
  and call `update_from_dps` whenever new data is received from the device stream.
35
-
36
34
  """
37
35
 
38
36
  import dataclasses
39
37
  import logging
40
38
  from collections.abc import Callable
41
- from typing import Any
39
+ from typing import Any, Generic, TypeVar
42
40
 
43
41
  from roborock.callbacks import CallbackList
44
- from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP
45
42
  from roborock.data.containers import RoborockBase
46
43
 
44
+ TDps = TypeVar("TDps", bound=int)
45
+
47
46
 
48
47
  class TraitUpdateListener:
49
48
  """Trait update listener.
@@ -71,14 +70,14 @@ class TraitUpdateListener:
71
70
  self._update_callbacks(None)
72
71
 
73
72
 
74
- class DpsDataConverter:
73
+ class DpsDataConverter(Generic[TDps]):
75
74
  """Utility to handle the transformation and merging of DPS data into models.
76
75
 
77
76
  This class pre-calculates the mapping between Data Point IDs and dataclass fields
78
77
  to optimize repeated updates from device streams.
79
78
  """
80
79
 
81
- def __init__(self, dps_type_map: dict[B01_Q10_DP, type], dps_field_map: dict[B01_Q10_DP, str]):
80
+ def __init__(self, dps_type_map: dict[TDps, type], dps_field_map: dict[TDps, str]):
82
81
  """Initialize the converter for a specific RoborockBase-derived class."""
83
82
  self._dps_type_map = dps_type_map
84
83
  self._dps_field_map = dps_field_map
@@ -86,8 +85,8 @@ class DpsDataConverter:
86
85
  @classmethod
87
86
  def from_dataclass(cls, dataclass_type: type[RoborockBase]):
88
87
  """Initialize the converter for a specific RoborockBase-derived class."""
89
- dps_type_map: dict[B01_Q10_DP, type] = {}
90
- dps_field_map: dict[B01_Q10_DP, str] = {}
88
+ dps_type_map: dict[TDps, type] = {}
89
+ dps_field_map: dict[TDps, str] = {}
91
90
  for field_obj in dataclasses.fields(dataclass_type):
92
91
  if field_obj.metadata and "dps" in field_obj.metadata:
93
92
  dps_id = field_obj.metadata["dps"]
@@ -95,7 +94,7 @@ class DpsDataConverter:
95
94
  dps_field_map[dps_id] = field_obj.name
96
95
  return cls(dps_type_map, dps_field_map)
97
96
 
98
- def update_from_dps(self, target: RoborockBase, decoded_dps: dict[B01_Q10_DP, Any]) -> bool:
97
+ def update_from_dps(self, target: RoborockBase, decoded_dps: dict[TDps, Any]) -> bool:
99
98
  """Convert and merge raw DPS data into the target object.
100
99
 
101
100
  Uses the pre-calculated type mapping to ensure values are converted to the
@@ -2,6 +2,7 @@ import logging
2
2
 
3
3
  from roborock.data import CleanRecord, CleanSummaryWithDetail, RoborockBase
4
4
  from roborock.devices.traits.v1 import common
5
+ from roborock.exceptions import RoborockParsingException
5
6
  from roborock.roborock_typing import RoborockCommand
6
7
  from roborock.util import unpack_list
7
8
 
@@ -87,4 +88,12 @@ class CleanSummaryTrait(CleanSummaryWithDetail, common.V1TraitMixin):
87
88
  async def get_clean_record(self, record_id: int) -> CleanRecord:
88
89
  """Load a specific clean record by ID."""
89
90
  response = await self.rpc_channel.send_command(RoborockCommand.GET_CLEAN_RECORD, params=[record_id])
90
- return self.clean_record_converter.convert(response)
91
+ try:
92
+ return self.clean_record_converter.convert(response)
93
+ except (TypeError, ValueError) as err:
94
+ raise RoborockParsingException(
95
+ trait_name=type(self).__name__,
96
+ command=RoborockCommand.GET_CLEAN_RECORD,
97
+ payload=response,
98
+ inner_error=err,
99
+ ) from err
@@ -9,6 +9,7 @@ from dataclasses import fields
9
9
  from typing import ClassVar
10
10
 
11
11
  from roborock.data import RoborockBase
12
+ from roborock.exceptions import RoborockParsingException
12
13
  from roborock.protocols.v1_protocol import V1RpcChannel
13
14
  from roborock.roborock_typing import RoborockCommand
14
15
 
@@ -63,7 +64,7 @@ class V1TraitMixin(ABC):
63
64
 
64
65
  def __init__(self) -> None:
65
66
  """Initialize the V1TraitMixin."""
66
- self._rpc_channel = None
67
+ self._rpc_channel: V1RpcChannel | None = None
67
68
 
68
69
  @property
69
70
  def rpc_channel(self) -> V1RpcChannel:
@@ -75,7 +76,15 @@ class V1TraitMixin(ABC):
75
76
  async def refresh(self) -> None:
76
77
  """Refresh the contents of this trait."""
77
78
  response = await self.rpc_channel.send_command(self.command)
78
- new_data = self.converter.convert(response)
79
+ try:
80
+ new_data = self.converter.convert(response)
81
+ except (TypeError, ValueError) as err:
82
+ raise RoborockParsingException(
83
+ trait_name=type(self).__name__,
84
+ command=self.command,
85
+ payload=response,
86
+ inner_error=err,
87
+ ) from err
79
88
  merge_trait_values(self, new_data) # type: ignore[arg-type]
80
89
 
81
90
 
@@ -5,6 +5,7 @@ from dataclasses import dataclass
5
5
 
6
6
  from roborock.data import HomeData, HomeDataRoom, NamedRoomMapping, RoborockBase
7
7
  from roborock.devices.traits.v1 import common
8
+ from roborock.exceptions import RoborockParsingException
8
9
  from roborock.roborock_typing import RoborockCommand
9
10
  from roborock.web_api import UserWebApiClient
10
11
 
@@ -94,7 +95,12 @@ class RoomsTrait(Rooms, common.V1TraitMixin):
94
95
  """Refresh room mappings and backfill unknown room names from the web API."""
95
96
  response = await self.rpc_channel.send_command(self.command)
96
97
  if not isinstance(response, list):
97
- raise ValueError(f"Unexpected RoomsTrait response format: {response!r}")
98
+ raise RoborockParsingException(
99
+ trait_name=type(self).__name__,
100
+ command=self.command,
101
+ payload=response,
102
+ inner_error="Unexpected RoomsTrait response format",
103
+ )
98
104
 
99
105
  segment_map = RoomsConverter.extract_segment_map(response)
100
106
  # Track all iot ids seen before. Refresh the room list when new ids are found.
@@ -105,8 +111,16 @@ class RoomsTrait(Rooms, common.V1TraitMixin):
105
111
  _LOGGER.debug("Updating rooms: %s", list(updated_rooms))
106
112
  self._home_data.rooms = updated_rooms
107
113
  self._discovered_iot_ids.update(new_iot_ids)
114
+ try:
115
+ rooms = self.converter.convert(response)
116
+ except (TypeError, ValueError) as err:
117
+ raise RoborockParsingException(
118
+ trait_name=type(self).__name__,
119
+ command=self.command,
120
+ payload=response,
121
+ inner_error=err,
122
+ ) from err
108
123
 
109
- rooms = self.converter.convert(response)
110
124
  rooms = rooms.with_room_names(self._home_data.rooms_name_map)
111
125
  common.merge_trait_values(self, rooms)
112
126
 
@@ -1,5 +1,8 @@
1
1
  """Roborock exceptions."""
2
2
 
3
+ from enum import Enum
4
+ from typing import Any
5
+
3
6
 
4
7
  class RoborockException(Exception):
5
8
  """Class for Roborock exceptions."""
@@ -91,3 +94,14 @@ class RoborockInvalidStatus(RoborockException):
91
94
 
92
95
  class RoborockUnsupportedFeature(RoborockException):
93
96
  """Class for Roborock unsupported feature exceptions."""
97
+
98
+
99
+ class RoborockParsingException(RoborockException):
100
+ """Class for Roborock exceptions when parsing device responses."""
101
+
102
+ def __init__(self, trait_name: str, command: Enum | str, payload: Any, inner_error: Exception | str) -> None:
103
+ cmd_name = command.name if isinstance(command, Enum) else str(command)
104
+ self.message = (
105
+ f"Failed to parse {cmd_name} response for {trait_name}. Payload: {payload!r} Error: {inner_error!r}"
106
+ )
107
+ super().__init__(self.message)
File without changes