python-roborock 4.2.1__tar.gz → 4.3.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 (93) hide show
  1. {python_roborock-4.2.1 → python_roborock-4.3.0}/PKG-INFO +1 -1
  2. {python_roborock-4.2.1 → python_roborock-4.3.0}/pyproject.toml +1 -1
  3. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/containers.py +7 -0
  4. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/v1/v1_containers.py +35 -7
  5. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/device.py +8 -6
  6. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/device_manager.py +10 -5
  7. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/__init__.py +7 -1
  8. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/device_features.py +30 -6
  9. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/diagnostics.py +25 -5
  10. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/web_api.py +2 -2
  11. {python_roborock-4.2.1 → python_roborock-4.3.0}/.gitignore +0 -0
  12. {python_roborock-4.2.1 → python_roborock-4.3.0}/LICENSE +0 -0
  13. {python_roborock-4.2.1 → python_roborock-4.3.0}/README.md +0 -0
  14. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/__init__.py +0 -0
  15. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/broadcast_protocol.py +0 -0
  16. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/callbacks.py +0 -0
  17. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/cli.py +0 -0
  18. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/const.py +0 -0
  19. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/__init__.py +0 -0
  20. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/b01_q10/__init__.py +0 -0
  21. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  22. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  23. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/b01_q7/__init__.py +0 -0
  24. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  25. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  26. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/code_mappings.py +0 -0
  27. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/dyad/__init__.py +0 -0
  28. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  29. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/dyad/dyad_containers.py +0 -0
  30. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/v1/__init__.py +0 -0
  31. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  32. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  33. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/zeo/__init__.py +0 -0
  34. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  35. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/data/zeo/zeo_containers.py +0 -0
  36. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/device_features.py +0 -0
  37. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/README.md +0 -0
  38. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/__init__.py +0 -0
  39. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/cache.py +0 -0
  40. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/file_cache.py +0 -0
  41. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/rpc/__init__.py +0 -0
  42. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/rpc/a01_channel.py +0 -0
  43. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
  44. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
  45. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/rpc/v1_channel.py +0 -0
  46. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/__init__.py +0 -0
  47. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/a01/__init__.py +0 -0
  48. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/b01/__init__.py +0 -0
  49. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  50. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/b01/q10/command.py +0 -0
  51. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
  52. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/traits_mixin.py +0 -0
  53. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  54. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  55. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/command.py +0 -0
  56. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/common.py +0 -0
  57. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  58. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  59. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  60. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  61. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/home.py +0 -0
  62. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/led_status.py +0 -0
  63. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/map_content.py +0 -0
  64. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/maps.py +0 -0
  65. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/network_info.py +0 -0
  66. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/rooms.py +0 -0
  67. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/routines.py +0 -0
  68. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  69. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/status.py +0 -0
  70. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  71. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/volume.py +0 -0
  72. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  73. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/transport/__init__.py +0 -0
  74. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/transport/channel.py +0 -0
  75. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/transport/local_channel.py +0 -0
  76. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/devices/transport/mqtt_channel.py +0 -0
  77. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/exceptions.py +0 -0
  78. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/map/__init__.py +0 -0
  79. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/map/map_parser.py +0 -0
  80. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/mqtt/__init__.py +0 -0
  81. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/mqtt/health_manager.py +0 -0
  82. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/mqtt/roborock_session.py +0 -0
  83. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/mqtt/session.py +0 -0
  84. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/protocol.py +0 -0
  85. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/protocols/__init__.py +0 -0
  86. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/protocols/a01_protocol.py +0 -0
  87. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/protocols/b01_q10_protocol.py +0 -0
  88. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/protocols/b01_q7_protocol.py +0 -0
  89. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/protocols/v1_protocol.py +0 -0
  90. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/py.typed +0 -0
  91. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/roborock_message.py +0 -0
  92. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/roborock_typing.py +0 -0
  93. {python_roborock-4.2.1 → python_roborock-4.3.0}/roborock/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 4.2.1
3
+ Version: 4.3.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.2.1"
3
+ version = "4.3.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"
@@ -232,6 +232,13 @@ class HomeDataProduct(RoborockBase):
232
232
  """Return a string with key product information for logging purposes."""
233
233
  return f"{self.name} (model={self.model}, category={self.category})"
234
234
 
235
+ @cached_property
236
+ def supported_schema_codes(self) -> set[str]:
237
+ """Return a set of fields that are supported by the device."""
238
+ if self.schema is None:
239
+ return set()
240
+ return {schema.code for schema in self.schema if schema.code is not None}
241
+
235
242
 
236
243
  @dataclass
237
244
  class HomeDataDevice(RoborockBase):
@@ -1,6 +1,7 @@
1
1
  import datetime
2
2
  import logging
3
- from dataclasses import dataclass
3
+ from dataclasses import dataclass, field
4
+ from enum import StrEnum
4
5
  from typing import Any
5
6
 
6
7
  from roborock.const import (
@@ -91,12 +92,39 @@ from .v1_code_mappings import (
91
92
  _LOGGER = logging.getLogger(__name__)
92
93
 
93
94
 
95
+ class FieldNameBase(StrEnum):
96
+ """A base enum class that represents a field name in a RoborockBase dataclass."""
97
+
98
+
99
+ class StatusField(FieldNameBase):
100
+ """An enum that represents a field in the `Status` class.
101
+
102
+ This is used with `roborock.devices.traits.v1.status.DeviceFeaturesTrait`
103
+ to understand if a feature is supported by the device using `is_field_supported`.
104
+
105
+ The enum values are names of fields in the `Status` class. Each field is
106
+ annotated with `requires_schema_code` metadata to map the field to a schema
107
+ code in the product schema, which may have a different name than the field/attribute name.
108
+ """
109
+
110
+ STATE = "state"
111
+ BATTERY = "battery"
112
+ FAN_POWER = "fan_power"
113
+ WATER_BOX_MODE = "water_box_mode"
114
+ CHARGE_STATUS = "charge_status"
115
+ DRY_STATUS = "dry_status"
116
+
117
+
118
+ def _requires_schema_code(requires_schema_code: str, default=None) -> Any:
119
+ return field(metadata={"requires_schema_code": requires_schema_code}, default=default)
120
+
121
+
94
122
  @dataclass
95
123
  class Status(RoborockBase):
96
124
  msg_ver: int | None = None
97
125
  msg_seq: int | None = None
98
- state: RoborockStateCode | None = None
99
- battery: int | None = None
126
+ state: RoborockStateCode | None = _requires_schema_code("state", default=None)
127
+ battery: int | None = _requires_schema_code("battery", default=None)
100
128
  clean_time: int | None = None
101
129
  clean_area: int | None = None
102
130
  error_code: RoborockErrorCode | None = None
@@ -109,12 +137,12 @@ class Status(RoborockBase):
109
137
  back_type: int | None = None
110
138
  wash_phase: int | None = None
111
139
  wash_ready: int | None = None
112
- fan_power: RoborockFanPowerCode | None = None
140
+ fan_power: RoborockFanPowerCode | None = _requires_schema_code("fan_power", default=None)
113
141
  dnd_enabled: int | None = None
114
142
  map_status: int | None = None
115
143
  is_locating: int | None = None
116
144
  lock_status: int | None = None
117
- water_box_mode: RoborockMopIntensityCode | None = None
145
+ water_box_mode: RoborockMopIntensityCode | None = _requires_schema_code("water_box_mode", default=None)
118
146
  water_box_carriage_status: int | None = None
119
147
  mop_forbidden_enable: int | None = None
120
148
  camera_status: int | None = None
@@ -132,13 +160,13 @@ class Status(RoborockBase):
132
160
  collision_avoid_status: int | None = None
133
161
  switch_map_mode: int | None = None
134
162
  dock_error_status: RoborockDockErrorCode | None = None
135
- charge_status: int | None = None
163
+ charge_status: int | None = _requires_schema_code("charge_status", default=None)
136
164
  unsave_map_reason: int | None = None
137
165
  unsave_map_flag: int | None = None
138
166
  wash_status: int | None = None
139
167
  distance_off: int | None = None
140
168
  in_warmup: int | None = None
141
- dry_status: int | None = None
169
+ dry_status: int | None = _requires_schema_code("drying_status", default=None)
142
170
  rdt: int | None = None
143
171
  clean_percent: int | None = None
144
172
  rss: int | None = None
@@ -226,9 +226,11 @@ class RoborockDevice(ABC, TraitsMixin):
226
226
  """Return diagnostics information about the device."""
227
227
  extra: dict[str, Any] = {}
228
228
  if self.v1_properties:
229
- extra["traits"] = redact_device_data(self.v1_properties.as_dict())
230
- return {
231
- "device": redact_device_data(self.device_info.as_dict()),
232
- "product": redact_device_data(self.product.as_dict()),
233
- **extra,
234
- }
229
+ extra["traits"] = self.v1_properties.as_dict()
230
+ return redact_device_data(
231
+ {
232
+ "device": self.device_info.as_dict(),
233
+ "product": self.product.as_dict(),
234
+ **extra,
235
+ }
236
+ )
@@ -16,7 +16,7 @@ from roborock.data import (
16
16
  UserData,
17
17
  )
18
18
  from roborock.devices.device import DeviceReadyCallback, RoborockDevice
19
- from roborock.diagnostics import Diagnostics
19
+ from roborock.diagnostics import Diagnostics, redact_device_data
20
20
  from roborock.exceptions import RoborockException
21
21
  from roborock.map.map_parser import MapParserConfig
22
22
  from roborock.mqtt.roborock_session import create_lazy_mqtt_session
@@ -76,6 +76,7 @@ class DeviceManager:
76
76
  self._devices: dict[str, RoborockDevice] = {}
77
77
  self._mqtt_session = mqtt_session
78
78
  self._diagnostics = diagnostics
79
+ self._home_data: HomeData | None = None
79
80
 
80
81
  async def discover_devices(self, prefer_cache: bool = True) -> list[RoborockDevice]:
81
82
  """Discover all devices for the logged-in user."""
@@ -91,9 +92,9 @@ class DeviceManager:
91
92
  raise
92
93
  _LOGGER.debug("Failed to fetch home data, using cached data: %s", ex)
93
94
  await self._cache.set(cache_data)
94
- home_data = cache_data.home_data
95
+ self._home_data = cache_data.home_data
95
96
 
96
- device_products = home_data.device_products
97
+ device_products = self._home_data.device_products
97
98
  _LOGGER.debug("Discovered %d devices", len(device_products))
98
99
 
99
100
  # These are connected serially to avoid overwhelming the MQTT broker
@@ -106,7 +107,7 @@ class DeviceManager:
106
107
  if duid in self._devices:
107
108
  continue
108
109
  try:
109
- new_device = self._device_creator(home_data, device, product)
110
+ new_device = self._device_creator(self._home_data, device, product)
110
111
  except UnsupportedDeviceError:
111
112
  _LOGGER.info("Skipping unsupported device %s %s", product.summary_info(), device.summary_info())
112
113
  unsupported_devices_counter.increment(device.pv or "unknown")
@@ -136,7 +137,11 @@ class DeviceManager:
136
137
 
137
138
  def diagnostic_data(self) -> Mapping[str, Any]:
138
139
  """Return diagnostics information about the device manager."""
139
- return self._diagnostics.as_dict()
140
+ return {
141
+ "home_data": redact_device_data(self._home_data.as_dict()) if self._home_data else None,
142
+ "devices": [device.diagnostic_data() for device in self._devices.values()],
143
+ "diagnostics": self._diagnostics.as_dict(),
144
+ }
140
145
 
141
146
 
142
147
  @dataclass
@@ -44,6 +44,12 @@ optional traits:
44
44
  available features.
45
45
  - `requires_dock_type` - If set, this is a function that accepts a `RoborockDockTypeCode`
46
46
  and returns a boolean indicating whether the trait is supported for that dock type.
47
+
48
+ Additionally, DeviceFeaturesTrait has a method `is_field_supported` that is used to
49
+ check individual trait field values. This is a more fine grained version to allow
50
+ optional fields in a dataclass, vs the above feature checks that apply to an entire
51
+ trait. The `requires_schema_code` field metadata attribute is a string of the schema
52
+ code in HomeDataProduct Schema that is required for the field to be supported.
47
53
  """
48
54
 
49
55
  import logging
@@ -189,7 +195,7 @@ class PropertiesApi(Trait):
189
195
  self.maps = MapsTrait(self.status)
190
196
  self.map_content = MapContentTrait(map_parser_config)
191
197
  self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, self._device_cache)
192
- self.device_features = DeviceFeaturesTrait(product.product_nickname, self._device_cache)
198
+ self.device_features = DeviceFeaturesTrait(product, self._device_cache)
193
199
  self.network_info = NetworkInfoTrait(device_uid, self._device_cache)
194
200
  self.routines = RoutinesTrait(device_uid, web_api)
195
201
 
@@ -1,6 +1,7 @@
1
- from dataclasses import fields
1
+ from dataclasses import Field, fields
2
2
 
3
- from roborock.data import AppInitStatus, RoborockProductNickname
3
+ from roborock.data import AppInitStatus, HomeDataProduct, RoborockBase
4
+ from roborock.data.v1.v1_containers import FieldNameBase
4
5
  from roborock.device_features import DeviceFeatures
5
6
  from roborock.devices.cache import DeviceCache
6
7
  from roborock.devices.traits.v1 import common
@@ -8,19 +9,42 @@ from roborock.roborock_typing import RoborockCommand
8
9
 
9
10
 
10
11
  class DeviceFeaturesTrait(DeviceFeatures, common.V1TraitMixin):
11
- """Trait for managing Do Not Disturb (DND) settings on Roborock devices."""
12
+ """Trait for managing supported features on Roborock devices."""
12
13
 
13
14
  command = RoborockCommand.APP_GET_INIT_STATUS
14
15
 
15
- def __init__(self, product_nickname: RoborockProductNickname, device_cache: DeviceCache) -> None: # pylint: disable=super-init-not-called
16
- """Initialize MapContentTrait."""
17
- self._nickname = product_nickname
16
+ def __init__(self, product: HomeDataProduct, device_cache: DeviceCache) -> None: # pylint: disable=super-init-not-called
17
+ """Initialize DeviceFeaturesTrait."""
18
+ self._product = product
19
+ self._nickname = product.product_nickname
18
20
  self._device_cache = device_cache
19
21
  # All fields of DeviceFeatures are required. Initialize them to False
20
22
  # so we have some known state.
21
23
  for field in fields(self):
22
24
  setattr(self, field.name, False)
23
25
 
26
+ def is_field_supported(self, cls: type[RoborockBase], field_name: FieldNameBase) -> bool:
27
+ """Determines if the specified field is supported by this device.
28
+
29
+ We use dataclass attributes on the field to specify the schema code that is required
30
+ for the field to be supported and it is compared against the list of
31
+ supported schema codes for the device returned in the product information.
32
+ """
33
+ dataclass_field: Field | None = None
34
+ for field in fields(cls):
35
+ if field.name == field_name:
36
+ dataclass_field = field
37
+ break
38
+ if dataclass_field is None:
39
+ raise ValueError(f"Field {field_name} not found in {cls}")
40
+
41
+ requires_schema_code = dataclass_field.metadata.get("requires_schema_code", None)
42
+ if requires_schema_code is None:
43
+ # We assume the field is supported
44
+ return True
45
+ # If the field requires a protocol that is not supported, we return False
46
+ return requires_schema_code in self._product.supported_schema_codes
47
+
24
48
  async def refresh(self) -> None:
25
49
  """Refresh the contents of this trait.
26
50
 
@@ -101,30 +101,50 @@ REDACT_KEYS = {
101
101
  "imageContent",
102
102
  "mapData",
103
103
  "rawApiResponse",
104
+ # Home data
105
+ "id", # We want to redact home_data.id but keep some other ids, see below
106
+ "name",
107
+ "productId",
108
+ "ipAddress",
109
+ "wifiName",
110
+ "lat",
111
+ "long",
112
+ }
113
+ KEEP_KEYS = {
114
+ # Product information not unique per user
115
+ "product.id",
116
+ "product.schema.id",
117
+ "product.schema.name",
118
+ # Room ids are likely unique per user, but don't seem too sensitive and are
119
+ # useful for debugging
120
+ "rooms.id",
104
121
  }
105
122
  DEVICE_UID = "duid"
106
123
  REDACTED = "**REDACTED**"
107
124
 
108
125
 
109
- def redact_device_data(data: T) -> T | dict[str, Any]:
126
+ def redact_device_data(data: T, path: str = "") -> T | dict[str, Any]:
110
127
  """Redact sensitive data in a dict."""
111
128
  if not isinstance(data, (Mapping, list)):
112
129
  return data
113
130
 
114
131
  if isinstance(data, list):
115
- return cast(T, [redact_device_data(item) for item in data])
132
+ return cast(T, [redact_device_data(item, path) for item in data])
116
133
 
117
134
  redacted = {**data}
118
135
 
119
136
  for key, value in redacted.items():
120
- if key in REDACT_KEYS:
137
+ curr_path = f"{path}.{key}" if path else key
138
+ if key in KEEP_KEYS or curr_path in KEEP_KEYS:
139
+ continue
140
+ if key in REDACT_KEYS or curr_path in REDACT_KEYS:
121
141
  redacted[key] = REDACTED
122
142
  elif key == DEVICE_UID and isinstance(value, str):
123
143
  redacted[key] = redact_device_uid(value)
124
144
  elif isinstance(value, dict):
125
- redacted[key] = redact_device_data(value)
145
+ redacted[key] = redact_device_data(value, curr_path)
126
146
  elif isinstance(value, list):
127
- redacted[key] = [redact_device_data(item) for item in value]
147
+ redacted[key] = [redact_device_data(item, curr_path) for item in value]
128
148
 
129
149
  return redacted
130
150
 
@@ -57,8 +57,8 @@ class RoborockApiClient:
57
57
  ]
58
58
  _HOME_DATA_RATES = [
59
59
  Rate(1, Duration.SECOND),
60
- Rate(5, Duration.MINUTE),
61
- Rate(15, Duration.HOUR),
60
+ Rate(3, Duration.MINUTE),
61
+ Rate(5, Duration.HOUR),
62
62
  Rate(40, Duration.DAY),
63
63
  ]
64
64
 
File without changes