python-roborock 4.2.2__tar.gz → 4.4.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.2 → python_roborock-4.4.0}/PKG-INFO +1 -1
  2. {python_roborock-4.2.2 → python_roborock-4.4.0}/pyproject.toml +1 -1
  3. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/containers.py +7 -0
  4. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/v1/v1_containers.py +35 -7
  5. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/__init__.py +7 -1
  6. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/device_features.py +30 -6
  7. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/web_api.py +10 -6
  8. {python_roborock-4.2.2 → python_roborock-4.4.0}/.gitignore +0 -0
  9. {python_roborock-4.2.2 → python_roborock-4.4.0}/LICENSE +0 -0
  10. {python_roborock-4.2.2 → python_roborock-4.4.0}/README.md +0 -0
  11. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/__init__.py +0 -0
  12. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/broadcast_protocol.py +0 -0
  13. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/callbacks.py +0 -0
  14. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/cli.py +0 -0
  15. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/const.py +0 -0
  16. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/__init__.py +0 -0
  17. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/b01_q10/__init__.py +0 -0
  18. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  19. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  20. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/b01_q7/__init__.py +0 -0
  21. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  22. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  23. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/code_mappings.py +0 -0
  24. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/dyad/__init__.py +0 -0
  25. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  26. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/dyad/dyad_containers.py +0 -0
  27. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/v1/__init__.py +0 -0
  28. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/v1/v1_clean_modes.py +0 -0
  29. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/v1/v1_code_mappings.py +0 -0
  30. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/zeo/__init__.py +0 -0
  31. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  32. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/data/zeo/zeo_containers.py +0 -0
  33. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/device_features.py +0 -0
  34. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/README.md +0 -0
  35. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/__init__.py +0 -0
  36. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/cache.py +0 -0
  37. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/device.py +0 -0
  38. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/device_manager.py +0 -0
  39. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/file_cache.py +0 -0
  40. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/rpc/__init__.py +0 -0
  41. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/rpc/a01_channel.py +0 -0
  42. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/rpc/b01_q10_channel.py +0 -0
  43. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/rpc/b01_q7_channel.py +0 -0
  44. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/rpc/v1_channel.py +0 -0
  45. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/__init__.py +0 -0
  46. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/a01/__init__.py +0 -0
  47. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/b01/__init__.py +0 -0
  48. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
  49. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/b01/q10/command.py +0 -0
  50. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
  51. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/traits_mixin.py +0 -0
  52. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/child_lock.py +0 -0
  53. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  54. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/command.py +0 -0
  55. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/common.py +0 -0
  56. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  57. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  58. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  59. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  60. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/home.py +0 -0
  61. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/led_status.py +0 -0
  62. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/map_content.py +0 -0
  63. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/maps.py +0 -0
  64. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/network_info.py +0 -0
  65. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/rooms.py +0 -0
  66. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/routines.py +0 -0
  67. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  68. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/status.py +0 -0
  69. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  70. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/volume.py +0 -0
  71. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  72. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/transport/__init__.py +0 -0
  73. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/transport/channel.py +0 -0
  74. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/transport/local_channel.py +0 -0
  75. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/devices/transport/mqtt_channel.py +0 -0
  76. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/diagnostics.py +0 -0
  77. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/exceptions.py +0 -0
  78. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/map/__init__.py +0 -0
  79. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/map/map_parser.py +0 -0
  80. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/mqtt/__init__.py +0 -0
  81. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/mqtt/health_manager.py +0 -0
  82. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/mqtt/roborock_session.py +0 -0
  83. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/mqtt/session.py +0 -0
  84. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/protocol.py +0 -0
  85. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/protocols/__init__.py +0 -0
  86. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/protocols/a01_protocol.py +0 -0
  87. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/protocols/b01_q10_protocol.py +0 -0
  88. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/protocols/b01_q7_protocol.py +0 -0
  89. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/protocols/v1_protocol.py +0 -0
  90. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/py.typed +0 -0
  91. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/roborock_message.py +0 -0
  92. {python_roborock-4.2.2 → python_roborock-4.4.0}/roborock/roborock_typing.py +0 -0
  93. {python_roborock-4.2.2 → python_roborock-4.4.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.2
3
+ Version: 4.4.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.2"
3
+ version = "4.4.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
@@ -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
 
@@ -62,7 +62,7 @@ class RoborockApiClient:
62
62
  Rate(40, Duration.DAY),
63
63
  ]
64
64
 
65
- _login_limiter = Limiter(_LOGIN_RATES)
65
+ _login_limiter = Limiter(_LOGIN_RATES, max_delay=1000)
66
66
  _home_data_limiter = Limiter(_HOME_DATA_RATES)
67
67
 
68
68
  def __init__(
@@ -74,11 +74,11 @@ class RoborockApiClient:
74
74
  self._device_identifier = secrets.token_urlsafe(16)
75
75
  self.session = session
76
76
  self._iot_login_info: IotLoginInfo | None = None
77
+ self._base_urls = BASE_URLS if base_url is None else [base_url]
77
78
 
78
79
  async def _get_iot_login_info(self) -> IotLoginInfo:
79
80
  if self._iot_login_info is None:
80
- valid_urls = BASE_URLS if self._base_url is None else [self._base_url]
81
- for iot_url in valid_urls:
81
+ for iot_url in self._base_urls:
82
82
  url_request = PreparedRequest(iot_url, self.session)
83
83
  response = await url_request.request(
84
84
  "post",
@@ -205,7 +205,7 @@ class RoborockApiClient:
205
205
 
206
206
  async def request_code(self) -> None:
207
207
  try:
208
- self._login_limiter.try_acquire("login")
208
+ await self._login_limiter.try_acquire_async("login")
209
209
  except BucketFullException as ex:
210
210
  _LOGGER.info(ex.meta_info)
211
211
  raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex
@@ -239,7 +239,7 @@ class RoborockApiClient:
239
239
  _LOGGER.info("No country code or country found, trying old version of request code.")
240
240
  return await self.request_code()
241
241
  try:
242
- self._login_limiter.try_acquire("login")
242
+ await self._login_limiter.try_acquire_async("login")
243
243
  except BucketFullException as ex:
244
244
  _LOGGER.info(ex.meta_info)
245
245
  raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex
@@ -269,6 +269,10 @@ class RoborockApiClient:
269
269
  raise RoborockAccountDoesNotExist("Account does not exist - check your login and try again.")
270
270
  elif response_code == 9002:
271
271
  raise RoborockTooFrequentCodeRequests("You have attempted to request too many codes. Try again later")
272
+ elif response_code == 3030 and len(self._base_urls) > 1:
273
+ self._base_urls = self._base_urls[1:]
274
+ self._iot_login_info = None
275
+ return await self.request_code_v4()
272
276
  else:
273
277
  raise RoborockException(f"{code_response.get('msg')} - response code: {code_response.get('code')}")
274
278
 
@@ -363,7 +367,7 @@ class RoborockApiClient:
363
367
 
364
368
  async def pass_login(self, password: str) -> UserData:
365
369
  try:
366
- self._login_limiter.try_acquire("login")
370
+ await self._login_limiter.try_acquire_async("login")
367
371
  except BucketFullException as ex:
368
372
  _LOGGER.info(ex.meta_info)
369
373
  raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex
File without changes