python-roborock 5.29.0__tar.gz → 5.30.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 (124) hide show
  1. {python_roborock-5.29.0 → python_roborock-5.30.0}/PKG-INFO +1 -1
  2. {python_roborock-5.29.0 → python_roborock-5.30.0}/pyproject.toml +1 -1
  3. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/containers.py +19 -0
  4. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/v1/v1_containers.py +27 -7
  5. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/__init__.py +1 -0
  6. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/device_features.py +45 -18
  7. {python_roborock-5.29.0 → python_roborock-5.30.0}/.gitignore +0 -0
  8. {python_roborock-5.29.0 → python_roborock-5.30.0}/LICENSE +0 -0
  9. {python_roborock-5.29.0 → python_roborock-5.30.0}/README.md +0 -0
  10. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/__init__.py +0 -0
  11. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/broadcast_protocol.py +0 -0
  12. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/callbacks.py +0 -0
  13. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/cli.py +0 -0
  14. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/const.py +0 -0
  15. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/__init__.py +0 -0
  16. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/b01_q10/__init__.py +0 -0
  17. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  18. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  19. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/b01_q7/__init__.py +0 -0
  20. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  21. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  22. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/code_mappings.py +0 -0
  23. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/dyad/__init__.py +0 -0
  24. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  25. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/dyad/dyad_containers.py +0 -0
  26. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/mower/__init__.py +0 -0
  27. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/mower/mower_containers.py +0 -0
  28. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/v1/__init__.py +0 -0
  29. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  30. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  31. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/zeo/__init__.py +0 -0
  32. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  33. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/data/zeo/zeo_containers.py +0 -0
  34. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/device_features.py +0 -0
  35. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/README.md +0 -0
  36. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/__init__.py +0 -0
  37. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/cache.py +0 -0
  38. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/device.py +0 -0
  39. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/device_manager.py +0 -0
  40. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/file_cache.py +0 -0
  41. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/rpc/__init__.py +0 -0
  42. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/rpc/a01_channel.py +0 -0
  43. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
  44. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
  45. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/rpc/v1_channel.py +0 -0
  46. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/__init__.py +0 -0
  47. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/a01/__init__.py +0 -0
  48. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/__init__.py +0 -0
  49. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  50. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/button_light.py +0 -0
  51. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/child_lock.py +0 -0
  52. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/clean_history.py +0 -0
  53. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/command.py +0 -0
  54. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/common.py +0 -0
  55. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/consumable.py +0 -0
  56. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/do_not_disturb.py +0 -0
  57. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/dust_collection.py +0 -0
  58. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/map.py +0 -0
  59. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/network_info.py +0 -0
  60. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/remote.py +0 -0
  61. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/status.py +0 -0
  62. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/vacuum.py +0 -0
  63. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q10/volume.py +0 -0
  64. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
  65. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q7/clean_summary.py +0 -0
  66. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q7/map.py +0 -0
  67. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/b01/q7/map_content.py +0 -0
  68. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/common.py +0 -0
  69. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/traits_mixin.py +0 -0
  70. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  71. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  72. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/command.py +0 -0
  73. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/common.py +0 -0
  74. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  75. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  76. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  77. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  78. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/home.py +0 -0
  79. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/led_status.py +0 -0
  80. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/map_content.py +0 -0
  81. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/maps.py +0 -0
  82. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/network_info.py +0 -0
  83. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/rooms.py +0 -0
  84. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/routines.py +0 -0
  85. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  86. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/status.py +0 -0
  87. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  88. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/volume.py +0 -0
  89. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  90. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/transport/__init__.py +0 -0
  91. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/transport/channel.py +0 -0
  92. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/transport/local_channel.py +0 -0
  93. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/devices/transport/mqtt_channel.py +0 -0
  94. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/diagnostics.py +0 -0
  95. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/exceptions.py +0 -0
  96. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/map/__init__.py +0 -0
  97. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/map/b01_grid_layers.py +0 -0
  98. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/map/b01_map_parser.py +0 -0
  99. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/map/b01_q10_map_parser.py +0 -0
  100. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/map/b01_q10_overlays.py +0 -0
  101. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/map/map_parser.py +0 -0
  102. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/map/proto/__init__.py +0 -0
  103. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/map/proto/b01_scmap.proto +0 -0
  104. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/map/proto/b01_scmap_pb2.py +0 -0
  105. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/mqtt/__init__.py +0 -0
  106. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/mqtt/health_manager.py +0 -0
  107. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/mqtt/roborock_session.py +0 -0
  108. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/mqtt/session.py +0 -0
  109. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/protocol.py +0 -0
  110. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/protocols/__init__.py +0 -0
  111. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/protocols/a01_protocol.py +0 -0
  112. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/protocols/b01_q10_protocol.py +0 -0
  113. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/protocols/b01_q7_protocol.py +0 -0
  114. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/protocols/v1_protocol.py +0 -0
  115. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/py.typed +0 -0
  116. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/roborock_message.py +0 -0
  117. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/roborock_typing.py +0 -0
  118. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/testing/__init__.py +0 -0
  119. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/testing/channel.py +0 -0
  120. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/testing/cloud.py +0 -0
  121. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/testing/simulator.py +0 -0
  122. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/testing/v1_simulator.py +0 -0
  123. {python_roborock-5.29.0 → python_roborock-5.30.0}/roborock/util.py +0 -0
  124. {python_roborock-5.29.0 → python_roborock-5.30.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.29.0
3
+ Version: 5.30.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.29.0"
3
+ version = "5.30.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"
@@ -60,6 +60,25 @@ def _attr_repr(obj: Any) -> str:
60
60
  return f"{type(obj).__name__}({', '.join(parts)})"
61
61
 
62
62
 
63
+ def field_metadata(**kwargs):
64
+ """Decorator to attach capability check metadata to a property.
65
+
66
+ This attaches a `_field_metadata` dictionary to the underlying getter function,
67
+ which is then preserved when decorated with `@property`.
68
+
69
+ Supported metadata keys:
70
+ - `feature` (str): Name of a capability property on `DeviceFeaturesTrait`.
71
+ - `dock_feature` (str): Name of a capability property on `RoborockDockFeatures`.
72
+ - `dps` (str/int): RoborockDataProtocol ID to check against supported schema IDs.
73
+ """
74
+
75
+ def decorator(func):
76
+ func._field_metadata = kwargs
77
+ return func
78
+
79
+ return decorator
80
+
81
+
63
82
  @dataclass(repr=False)
64
83
  class RoborockBase:
65
84
  """Base class for all Roborock data classes."""
@@ -39,7 +39,7 @@ from roborock.const import (
39
39
  from roborock.exceptions import RoborockException
40
40
  from roborock.roborock_message import RoborockDataProtocol
41
41
 
42
- from ..containers import NamedRoomMapping, RoborockBase, RoborockBaseTimer, _attr_repr
42
+ from ..containers import NamedRoomMapping, RoborockBase, RoborockBaseTimer, _attr_repr, field_metadata
43
43
  from .v1_clean_modes import WashTowelModes
44
44
  from .v1_code_mappings import (
45
45
  CleanFluidStatus,
@@ -117,6 +117,15 @@ class StatusField(FieldNameBase):
117
117
  CHARGE_STATUS = "charge_status"
118
118
  DRY_STATUS = "dry_status"
119
119
  ERROR_CODE = "error_code"
120
+ WATER_BOX_CARRIAGE_STATUS = "water_box_carriage_status"
121
+ WATER_BOX_STATUS = "water_box_status"
122
+ WATER_SHORTAGE_STATUS = "water_shortage_status"
123
+ DIRTY_WATER_BOX_STATUS = "dirty_water_box_status"
124
+ CLEAR_WATER_BOX_STATUS = "clear_water_box_status"
125
+ CLEAN_FLUID_STATUS = "clean_fluid_status"
126
+ CLEAN_PERCENT = "clean_percent"
127
+ DOCK_ERROR_STATUS = "dock_error_status"
128
+ RDT = "rdt"
120
129
 
121
130
 
122
131
  @dataclass
@@ -246,12 +255,14 @@ class Status(RoborockBase):
246
255
  return (self.dss & 3) == 2
247
256
 
248
257
  @property
258
+ @field_metadata(dock_feature="is_washable")
249
259
  def clear_water_box_status(self) -> ClearWaterBoxStatus | None:
250
260
  if self.dss:
251
261
  return ClearWaterBoxStatus((self.dss >> 2) & 3)
252
262
  return None
253
263
 
254
264
  @property
265
+ @field_metadata(dock_feature="is_washable")
255
266
  def dirty_water_box_status(self) -> DirtyWaterBoxStatus | None:
256
267
  if self.dss:
257
268
  return DirtyWaterBoxStatus((self.dss >> 4) & 3)
@@ -270,6 +281,9 @@ class Status(RoborockBase):
270
281
  return None
271
282
 
272
283
  @property
284
+ @field_metadata(
285
+ dock_feature="is_clean_fluid_auto_delivery_supported",
286
+ )
273
287
  def clean_fluid_status(self) -> CleanFluidStatus | None:
274
288
  if self.dss:
275
289
  value = (self.dss >> 10) & 3
@@ -313,7 +327,7 @@ class StatusV2(RoborockBase):
313
327
  in_returning: int | None = None
314
328
  in_fresh_state: int | None = None
315
329
  lab_status: int | None = None
316
- water_box_status: int | None = None
330
+ water_box_status: int | None = field(default=None, metadata={"feature": "is_support_water_mode"})
317
331
  back_type: int | None = None
318
332
  wash_phase: int | None = None
319
333
  wash_ready: int | None = None
@@ -323,14 +337,14 @@ class StatusV2(RoborockBase):
323
337
  is_locating: int | None = None
324
338
  lock_status: int | None = None
325
339
  water_box_mode: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.WATER_BOX_MODE})
326
- water_box_carriage_status: int | None = None
340
+ water_box_carriage_status: int | None = field(default=None, metadata={"feature": "is_support_water_mode"})
327
341
  mop_forbidden_enable: int | None = None
328
342
  camera_status: int | None = None
329
343
  is_exploring: int | None = None
330
344
  home_sec_status: int | None = None
331
345
  home_sec_enable_password: int | None = None
332
346
  adbumper_status: list[int] | None = None
333
- water_shortage_status: int | None = None
347
+ water_shortage_status: int | None = field(default=None, metadata={"feature": "is_support_water_mode"})
334
348
  dock_type: RoborockDockTypeCode | None = None
335
349
  dust_collection_status: int | None = None
336
350
  auto_dust_collection: int | None = None
@@ -339,7 +353,8 @@ class StatusV2(RoborockBase):
339
353
  debug_mode: int | None = None
340
354
  collision_avoid_status: int | None = None
341
355
  switch_map_mode: int | None = None
342
- dock_error_status: RoborockDockErrorCode | None = None
356
+ dock_error_status: RoborockDockErrorCode | None = field(default=None, metadata={"dock_feature": "has_dock"})
357
+
343
358
  charge_status: RoborockChargeStatus | None = field(
344
359
  default=None, metadata={"dps": RoborockDataProtocol.CHARGE_STATUS}
345
360
  )
@@ -349,8 +364,8 @@ class StatusV2(RoborockBase):
349
364
  distance_off: int | None = None
350
365
  in_warmup: int | None = None
351
366
  dry_status: int | None = field(default=None, metadata={"dps": RoborockDataProtocol.DRYING_STATUS})
352
- rdt: int | None = None
353
- clean_percent: int | None = None
367
+ rdt: int | None = field(default=None, metadata={"feature": "is_supported_drying"})
368
+ clean_percent: int | None = field(default=None, metadata={"feature": "is_support_clean_estimate"})
354
369
  rss: int | None = None
355
370
  dss: int | None = None
356
371
  common_status: int | None = None
@@ -389,12 +404,14 @@ class StatusV2(RoborockBase):
389
404
  return (self.dss & 3) == 2
390
405
 
391
406
  @property
407
+ @field_metadata(dock_feature="is_washable")
392
408
  def clear_water_box_status(self) -> ClearWaterBoxStatus | None:
393
409
  if self.dss:
394
410
  return ClearWaterBoxStatus((self.dss >> 2) & 3)
395
411
  return None
396
412
 
397
413
  @property
414
+ @field_metadata(dock_feature="is_washable")
398
415
  def dirty_water_box_status(self) -> DirtyWaterBoxStatus | None:
399
416
  if self.dss:
400
417
  return DirtyWaterBoxStatus((self.dss >> 4) & 3)
@@ -413,6 +430,9 @@ class StatusV2(RoborockBase):
413
430
  return None
414
431
 
415
432
  @property
433
+ @field_metadata(
434
+ dock_feature="is_clean_fluid_auto_delivery_supported",
435
+ )
416
436
  def clean_fluid_status(self) -> CleanFluidStatus | None:
417
437
  if self.dss:
418
438
  value = (self.dss >> 10) & 3
@@ -264,6 +264,7 @@ class PropertiesApi(Trait):
264
264
  # Dock type also acts like a device feature for some traits.
265
265
  dock_type = await self._dock_type()
266
266
  dock_features = RoborockDockFeatures.from_dock_type(dock_type, has_am=self.status.has_am)
267
+ self.device_features.dock_features = dock_features
267
268
 
268
269
  # Initialize traits with special arguments before the generic loop
269
270
  if self.wash_towel_mode is None and self._is_supported(WashTowelModeTrait, "wash_towel_mode", dock_features):
@@ -1,12 +1,33 @@
1
- from dataclasses import Field, fields
1
+ from dataclasses import fields
2
+ from typing import Any
2
3
 
3
4
  from roborock.data import AppInitStatus, HomeDataProduct, RoborockBase
5
+ from roborock.data.v1 import RoborockDockTypeCode
4
6
  from roborock.data.v1.v1_containers import FieldNameBase
5
- from roborock.device_features import DeviceFeatures
7
+ from roborock.device_features import DeviceFeatures, RoborockDockFeatures
6
8
  from roborock.devices.cache import DeviceCache
7
9
  from roborock.devices.traits.v1 import common
8
10
  from roborock.roborock_typing import RoborockCommand
9
11
 
12
+ # Cache of metadata for each trait class
13
+ _metadata_cache: dict[type[RoborockBase], dict[str, dict[str, Any]]] = {}
14
+
15
+
16
+ def _get_field_metadata(cls: type[RoborockBase]) -> dict[str, Any]:
17
+ """Helper to get metadata from either class properties or dataclass fields."""
18
+ if cls not in _metadata_cache:
19
+ metadata_map = {}
20
+ # Inspect properties with @field_metadata
21
+ for name in dir(cls):
22
+ prop = getattr(cls, name, None)
23
+ if isinstance(prop, property):
24
+ metadata_map[name] = getattr(prop.fget, "_field_metadata", {})
25
+ # Inspect dataclass fields metadata
26
+ for f in fields(cls):
27
+ metadata_map[f.name] = f.metadata
28
+ _metadata_cache[cls] = metadata_map
29
+ return _metadata_cache[cls]
30
+
10
31
 
11
32
  class DeviceTraitsConverter(common.V1TraitDataConverter):
12
33
  """Converter for APP_GET_INIT_STATUS responses into DeviceFeatures."""
@@ -40,31 +61,37 @@ class DeviceFeaturesTrait(DeviceFeatures, common.V1TraitMixin):
40
61
  self.converter = DeviceTraitsConverter(product)
41
62
  self._product = product
42
63
  self._device_cache = device_cache
64
+ # Dock features are populated after device feature discovery
65
+ # is triggered.
66
+ self.dock_features: RoborockDockFeatures = RoborockDockFeatures.from_dock_type(RoborockDockTypeCode.o0_dock)
43
67
  # All fields of DeviceFeatures are required. Initialize them to False
44
68
  # so we have some known state.
45
69
  for field in fields(self):
46
70
  setattr(self, field.name, False)
47
71
 
48
- @staticmethod
49
- def _get_dataclass_field(cls: type[RoborockBase], field_name: FieldNameBase) -> Field:
50
- """Look up a dataclass field by its FieldNameBase name."""
51
- for f in fields(cls):
52
- if f.name == field_name:
53
- return f
54
- raise ValueError(f"Field {field_name!r} not found in {cls}")
55
-
56
72
  def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool:
57
73
  """Determines if the specified field is supported by this device.
58
74
 
59
- We use the `dps` dataclass field metadata to get the `RoborockDataProtocol`
60
- integer ID and check it against the set of supported schema IDs for the
61
- device returned in the product information.
75
+ We inspect the metadata defined for the field (either via dataclass field metadata
76
+ or the `@field_metadata` decorator on properties). Supported checks include:
77
+
78
+ - `feature`: Maps to a boolean capability property on `DeviceFeatures` / `DeviceFeaturesTrait`
79
+ (e.g. `is_support_water_mode`).
80
+ - `dock_feature`: Maps to a boolean capability property on `RoborockDockFeatures` (e.g. `is_washable`).
81
+ - `dps`: Maps to a `RoborockDataProtocol` ID checked against the product's supported schema IDs.
62
82
  """
63
- dataclass_field = self._get_dataclass_field(cls, field_name)
64
- if (dps := dataclass_field.metadata.get("dps")) is None:
65
- # No DPS metadata — field is assumed always supported
66
- return True
67
- return int(dps) in self._product.supported_schema_ids
83
+ if self.dock_features is None:
84
+ raise ValueError("DeviceFeaturesTrait was invoked but was not fully initialized")
85
+ metadata_map = _get_field_metadata(cls)
86
+ if (field_metadata := metadata_map.get(field_name)) is not None:
87
+ if (feature := field_metadata.get("feature")) is not None:
88
+ return getattr(self, feature, False)
89
+ if (dock_feature := field_metadata.get("dock_feature")) is not None:
90
+ return getattr(self.dock_features, dock_feature, False)
91
+ if (dps := field_metadata.get("dps")) is not None:
92
+ return int(dps) in self._product.supported_schema_ids
93
+ # No metadata, field is assumed always supported
94
+ return True
68
95
 
69
96
  async def refresh(self) -> None:
70
97
  """Refresh the contents of this trait.