python-roborock 2.50.0__tar.gz → 2.50.2__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 (62) hide show
  1. {python_roborock-2.50.0 → python_roborock-2.50.2}/PKG-INFO +1 -1
  2. {python_roborock-2.50.0 → python_roborock-2.50.2}/pyproject.toml +1 -1
  3. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/const.py +4 -4
  4. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/exceptions.py +4 -0
  5. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/web_api.py +87 -43
  6. {python_roborock-2.50.0 → python_roborock-2.50.2}/LICENSE +0 -0
  7. {python_roborock-2.50.0 → python_roborock-2.50.2}/README.md +0 -0
  8. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/__init__.py +0 -0
  9. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/api.py +0 -0
  10. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/b01_containers.py +0 -0
  11. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/broadcast_protocol.py +0 -0
  12. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/callbacks.py +0 -0
  13. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/clean_modes.py +0 -0
  14. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/cli.py +0 -0
  15. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/cloud_api.py +0 -0
  16. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/code_mappings.py +0 -0
  17. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/command_cache.py +0 -0
  18. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/containers.py +0 -0
  19. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/device_features.py +0 -0
  20. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/README.md +0 -0
  21. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/__init__.py +0 -0
  22. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/a01_channel.py +0 -0
  23. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/b01_channel.py +0 -0
  24. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/cache.py +0 -0
  25. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/channel.py +0 -0
  26. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/device.py +0 -0
  27. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/device_manager.py +0 -0
  28. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/local_channel.py +0 -0
  29. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/mqtt_channel.py +0 -0
  30. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/traits/__init__.py +0 -0
  31. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/traits/a01/__init__.py +0 -0
  32. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/traits/b01/__init__.py +0 -0
  33. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/traits/traits_mixin.py +0 -0
  34. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/traits/v1/__init__.py +0 -0
  35. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/traits/v1/clean_summary.py +0 -0
  36. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/traits/v1/common.py +0 -0
  37. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  38. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/traits/v1/maps.py +0 -0
  39. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/traits/v1/status.py +0 -0
  40. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/traits/v1/volume.py +0 -0
  41. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/v1_channel.py +0 -0
  42. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/devices/v1_rpc_channel.py +0 -0
  43. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/map/map_parser.py +0 -0
  44. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/mqtt/__init__.py +0 -0
  45. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/mqtt/roborock_session.py +0 -0
  46. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/mqtt/session.py +0 -0
  47. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/protocol.py +0 -0
  48. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/protocols/a01_protocol.py +0 -0
  49. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/protocols/b01_protocol.py +0 -0
  50. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/protocols/v1_protocol.py +0 -0
  51. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/py.typed +0 -0
  52. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/roborock_future.py +0 -0
  53. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/roborock_message.py +0 -0
  54. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/roborock_typing.py +0 -0
  55. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/util.py +0 -0
  56. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/version_1_apis/__init__.py +0 -0
  57. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  58. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  59. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  60. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/version_a01_apis/__init__.py +0 -0
  61. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  62. {python_roborock-2.50.0 → python_roborock-2.50.2}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-roborock
3
- Version: 2.50.0
3
+ Version: 2.50.2
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Home-page: https://github.com/humbertogontijo/python-roborock
6
6
  License: GPL-3.0-only
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-roborock"
3
- version = "2.50.0"
3
+ version = "2.50.2"
4
4
  description = "A package to control Roborock vacuums."
5
5
  authors = ["humbertogontijo <humbertogontijo@users.noreply.github.com>"]
6
6
  license = "GPL-3.0-only"
@@ -4,10 +4,10 @@ SIDE_BRUSH_REPLACE_TIME = 720000
4
4
  FILTER_REPLACE_TIME = 540000
5
5
  SENSOR_DIRTY_REPLACE_TIME = 108000
6
6
  MOP_ROLLER_REPLACE_TIME = 1080000
7
- STRAINER_REPLACE_TIME = 540000
8
- CLEANING_BRUSH_REPLACE_TIME = 1080000
9
- DUST_COLLECTION_REPLACE_TIME = 81000
10
- FLOOR_CLEANER_REPLACE_TIME = 1080000
7
+ STRAINER_REPLACE_TIME = 150
8
+ CLEANING_BRUSH_REPLACE_TIME = 300
9
+ DUST_COLLECTION_REPLACE_TIME = 90
10
+ FLOOR_CLEANER_REPLACE_TIME = 300
11
11
 
12
12
 
13
13
  ROBOROCK_V1 = "ROBOROCK.vacuum.v1"
@@ -77,3 +77,7 @@ class RoborockTooManyRequest(RoborockException):
77
77
 
78
78
  class RoborockRateLimit(RoborockException):
79
79
  """Class for our rate limits exceptions."""
80
+
81
+
82
+ class RoborockNoResponseFromBaseURL(RoborockException):
83
+ """We could not find an url that had a record of the given account."""
@@ -8,6 +8,7 @@ import math
8
8
  import secrets
9
9
  import string
10
10
  import time
11
+ from dataclasses import dataclass
11
12
 
12
13
  import aiohttp
13
14
  from aiohttp import ContentTypeError, FormData
@@ -22,14 +23,28 @@ from roborock.exceptions import (
22
23
  RoborockInvalidEmail,
23
24
  RoborockInvalidUserAgreement,
24
25
  RoborockMissingParameters,
26
+ RoborockNoResponseFromBaseURL,
25
27
  RoborockNoUserAgreement,
26
28
  RoborockRateLimit,
27
29
  RoborockTooFrequentCodeRequests,
28
- RoborockTooManyRequest,
29
- RoborockUrlException,
30
30
  )
31
31
 
32
32
  _LOGGER = logging.getLogger(__name__)
33
+ BASE_URLS = [
34
+ "https://usiot.roborock.com",
35
+ "https://euiot.roborock.com",
36
+ "https://cniot.roborock.com",
37
+ "https://ruiot.roborock.com",
38
+ ]
39
+
40
+
41
+ @dataclass
42
+ class IotLoginInfo:
43
+ """Information about the login to the iot server."""
44
+
45
+ base_url: str
46
+ country_code: str
47
+ country: str
33
48
 
34
49
 
35
50
  class RoborockApiClient:
@@ -49,41 +64,64 @@ class RoborockApiClient:
49
64
  _login_limiter = Limiter(_LOGIN_RATES)
50
65
  _home_data_limiter = Limiter(_HOME_DATA_RATES)
51
66
 
52
- def __init__(self, username: str, base_url=None, session: aiohttp.ClientSession | None = None) -> None:
67
+ def __init__(
68
+ self, username: str, base_url: str | None = None, session: aiohttp.ClientSession | None = None
69
+ ) -> None:
53
70
  """Sample API Client."""
54
71
  self._username = username
55
- self._default_url = "https://euiot.roborock.com"
56
- self.base_url = base_url
72
+ self._base_url = base_url
57
73
  self._device_identifier = secrets.token_urlsafe(16)
58
74
  self.session = session
59
-
60
- async def _get_base_url(self) -> str:
61
- if not self.base_url:
62
- url_request = PreparedRequest(self._default_url, self.session)
63
- response = await url_request.request(
64
- "post",
65
- "/api/v1/getUrlByEmail",
66
- params={"email": self._username, "needtwostepauth": "false"},
67
- )
68
- if response is None:
69
- raise RoborockUrlException("get url by email returned None")
70
- response_code = response.get("code")
71
- if response_code != 200:
72
- _LOGGER.info("Get base url failed for %s with the following context: %s", self._username, response)
73
- if response_code == 2003:
74
- raise RoborockInvalidEmail("Your email was incorrectly formatted.")
75
- elif response_code == 1001:
76
- raise RoborockMissingParameters(
77
- "You are missing parameters for this request, are you sure you entered your username?"
75
+ self._iot_login_info: IotLoginInfo | None = None
76
+
77
+ async def _get_iot_login_info(self) -> IotLoginInfo:
78
+ if self._iot_login_info is None:
79
+ valid_urls = BASE_URLS if self._base_url is None else [self._base_url]
80
+ for iot_url in valid_urls:
81
+ url_request = PreparedRequest(iot_url, self.session)
82
+ response = await url_request.request(
83
+ "post",
84
+ "/api/v1/getUrlByEmail",
85
+ params={"email": self._username, "needtwostepauth": "false"},
86
+ )
87
+ if response is None:
88
+ continue
89
+ response_code = response.get("code")
90
+ if response_code != 200:
91
+ if response_code == 2003:
92
+ raise RoborockInvalidEmail("Your email was incorrectly formatted.")
93
+ elif response_code == 1001:
94
+ raise RoborockMissingParameters(
95
+ "You are missing parameters for this request, are you sure you entered your username?"
96
+ )
97
+ else:
98
+ raise RoborockException(f"{response.get('msg')} - response code: {response_code}")
99
+ if response["data"]["countrycode"] is not None:
100
+ self._iot_login_info = IotLoginInfo(
101
+ base_url=response["data"]["url"],
102
+ country=response["data"]["country"],
103
+ country_code=response["data"]["countrycode"],
78
104
  )
79
- elif response_code == 9002:
80
- raise RoborockTooManyRequest("Please temporarily disable making requests and try again later.")
81
- raise RoborockUrlException(f"error code: {response_code} msg: {response.get('error')}")
82
- response_data = response.get("data")
83
- if response_data is None:
84
- raise RoborockUrlException("response does not have 'data'")
85
- self.base_url = response_data.get("url")
86
- return self.base_url
105
+ return self._iot_login_info
106
+ raise RoborockNoResponseFromBaseURL(
107
+ "No account was found for any base url we tried. Either your email is incorrect or we do not have a"
108
+ " record of the roborock server your device is on."
109
+ )
110
+ return self._iot_login_info
111
+
112
+ @property
113
+ async def base_url(self):
114
+ if self._base_url is not None:
115
+ return self._base_url
116
+ return (await self._get_iot_login_info()).base_url
117
+
118
+ @property
119
+ async def country(self):
120
+ return (await self._get_iot_login_info()).country
121
+
122
+ @property
123
+ async def country_code(self):
124
+ return (await self._get_iot_login_info()).country_code
87
125
 
88
126
  def _get_header_client_id(self):
89
127
  md5 = hashlib.md5()
@@ -167,7 +205,7 @@ class RoborockApiClient:
167
205
  except BucketFullException as ex:
168
206
  _LOGGER.info(ex.meta_info)
169
207
  raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex
170
- base_url = await self._get_base_url()
208
+ base_url = await self.base_url
171
209
  header_clientid = self._get_header_client_id()
172
210
  code_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid})
173
211
 
@@ -198,7 +236,7 @@ class RoborockApiClient:
198
236
  except BucketFullException as ex:
199
237
  _LOGGER.info(ex.meta_info)
200
238
  raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex
201
- base_url = await self._get_base_url()
239
+ base_url = await self.base_url
202
240
  header_clientid = self._get_header_client_id()
203
241
  code_request = PreparedRequest(
204
242
  base_url,
@@ -229,7 +267,7 @@ class RoborockApiClient:
229
267
 
230
268
  async def _sign_key_v3(self, s: str) -> str:
231
269
  """Sign a randomly generated string."""
232
- base_url = await self._get_base_url()
270
+ base_url = await self.base_url
233
271
  header_clientid = self._get_header_client_id()
234
272
  code_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid})
235
273
 
@@ -249,14 +287,20 @@ class RoborockApiClient:
249
287
 
250
288
  return code_response["data"]["k"]
251
289
 
252
- async def code_login_v4(self, code: int | str, country: str, country_code: int) -> UserData:
290
+ async def code_login_v4(
291
+ self, code: int | str, country: str | None = None, country_code: int | None = None
292
+ ) -> UserData:
253
293
  """
254
294
  Login via code authentication.
255
295
  :param code: The code from the email.
256
296
  :param country: The two-character representation of the country, i.e. "US"
257
297
  :param country_code: the country phone number code i.e. 1 for US.
258
298
  """
259
- base_url = await self._get_base_url()
299
+ base_url = await self.base_url
300
+ if country is None:
301
+ country = await self.country
302
+ if country_code is None:
303
+ country_code = await self.country_code
260
304
  header_clientid = self._get_header_client_id()
261
305
  x_mercy_ks = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
262
306
  x_mercy_k = await self._sign_key_v3(x_mercy_ks)
@@ -304,7 +348,7 @@ class RoborockApiClient:
304
348
  except BucketFullException as ex:
305
349
  _LOGGER.info(ex.meta_info)
306
350
  raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex
307
- base_url = await self._get_base_url()
351
+ base_url = await self.base_url
308
352
  header_clientid = self._get_header_client_id()
309
353
 
310
354
  login_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid})
@@ -343,7 +387,7 @@ class RoborockApiClient:
343
387
  raise NotImplementedError("Pass_login_v3 has not yet been implemented")
344
388
 
345
389
  async def code_login(self, code: int | str) -> UserData:
346
- base_url = await self._get_base_url()
390
+ base_url = await self.base_url
347
391
  header_clientid = self._get_header_client_id()
348
392
 
349
393
  login_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid})
@@ -376,7 +420,7 @@ class RoborockApiClient:
376
420
  return UserData.from_dict(user_data)
377
421
 
378
422
  async def _get_home_id(self, user_data: UserData):
379
- base_url = await self._get_base_url()
423
+ base_url = await self.base_url
380
424
  header_clientid = self._get_header_client_id()
381
425
  home_id_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid})
382
426
  home_id_response = await home_id_request.request(
@@ -547,7 +591,7 @@ class RoborockApiClient:
547
591
 
548
592
  async def get_products(self, user_data: UserData) -> ProductResponse:
549
593
  """Gets all products and their schemas, good for determining status codes and model numbers."""
550
- base_url = await self._get_base_url()
594
+ base_url = await self.base_url
551
595
  header_clientid = self._get_header_client_id()
552
596
  product_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid})
553
597
  product_response = await product_request.request(
@@ -565,7 +609,7 @@ class RoborockApiClient:
565
609
  raise RoborockException("product result was an unexpected type")
566
610
 
567
611
  async def download_code(self, user_data: UserData, product_id: int):
568
- base_url = await self._get_base_url()
612
+ base_url = await self.base_url
569
613
  header_clientid = self._get_header_client_id()
570
614
  product_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid})
571
615
  request = {"apilevel": 99999, "productids": [product_id], "type": 2}
@@ -578,7 +622,7 @@ class RoborockApiClient:
578
622
  return response["data"][0]["url"]
579
623
 
580
624
  async def download_category_code(self, user_data: UserData):
581
- base_url = await self._get_base_url()
625
+ base_url = await self.base_url
582
626
  header_clientid = self._get_header_client_id()
583
627
  product_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid})
584
628
  response = await product_request.request(