python-roborock 2.44.0__tar.gz → 2.45.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 (59) hide show
  1. {python_roborock-2.44.0 → python_roborock-2.45.0}/PKG-INFO +1 -1
  2. {python_roborock-2.44.0 → python_roborock-2.45.0}/pyproject.toml +1 -1
  3. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/device_manager.py +6 -2
  4. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/web_api.py +108 -0
  5. {python_roborock-2.44.0 → python_roborock-2.45.0}/LICENSE +0 -0
  6. {python_roborock-2.44.0 → python_roborock-2.45.0}/README.md +0 -0
  7. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/__init__.py +0 -0
  8. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/api.py +0 -0
  9. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/b01_containers.py +0 -0
  10. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/broadcast_protocol.py +0 -0
  11. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/callbacks.py +0 -0
  12. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/clean_modes.py +0 -0
  13. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/cli.py +0 -0
  14. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/cloud_api.py +0 -0
  15. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/code_mappings.py +0 -0
  16. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/command_cache.py +0 -0
  17. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/const.py +0 -0
  18. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/containers.py +0 -0
  19. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/device_features.py +0 -0
  20. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/README.md +0 -0
  21. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/__init__.py +0 -0
  22. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/a01_channel.py +0 -0
  23. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/b01_channel.py +0 -0
  24. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/cache.py +0 -0
  25. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/channel.py +0 -0
  26. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/device.py +0 -0
  27. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/local_channel.py +0 -0
  28. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/mqtt_channel.py +0 -0
  29. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/traits/b01/__init__.py +0 -0
  30. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/traits/b01/props.py +0 -0
  31. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/traits/clean_summary.py +0 -0
  32. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/traits/dnd.py +0 -0
  33. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/traits/dyad.py +0 -0
  34. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/traits/sound_volume.py +0 -0
  35. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/traits/status.py +0 -0
  36. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/traits/trait.py +0 -0
  37. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/traits/zeo.py +0 -0
  38. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/v1_channel.py +0 -0
  39. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/devices/v1_rpc_channel.py +0 -0
  40. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/exceptions.py +0 -0
  41. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/mqtt/__init__.py +0 -0
  42. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/mqtt/roborock_session.py +0 -0
  43. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/mqtt/session.py +0 -0
  44. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/protocol.py +0 -0
  45. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/protocols/a01_protocol.py +0 -0
  46. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/protocols/b01_protocol.py +0 -0
  47. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/protocols/v1_protocol.py +0 -0
  48. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/py.typed +0 -0
  49. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/roborock_future.py +0 -0
  50. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/roborock_message.py +0 -0
  51. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/roborock_typing.py +0 -0
  52. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/util.py +0 -0
  53. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/version_1_apis/__init__.py +0 -0
  54. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  55. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  56. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  57. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/version_a01_apis/__init__.py +0 -0
  58. {python_roborock-2.44.0 → python_roborock-2.45.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  59. {python_roborock-2.44.0 → python_roborock-2.45.0}/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.44.0
3
+ Version: 2.45.0
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.44.0"
3
+ version = "2.45.0"
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"
@@ -5,6 +5,8 @@ import enum
5
5
  import logging
6
6
  from collections.abc import Awaitable, Callable
7
7
 
8
+ import aiohttp
9
+
8
10
  from roborock.code_mappings import RoborockCategory
9
11
  from roborock.containers import (
10
12
  HomeData,
@@ -113,7 +115,9 @@ class DeviceManager:
113
115
  await asyncio.gather(*tasks)
114
116
 
115
117
 
116
- def create_home_data_api(email: str, user_data: UserData) -> HomeDataApi:
118
+ def create_home_data_api(
119
+ email: str, user_data: UserData, base_url: str | None = None, session: aiohttp.ClientSession | None = None
120
+ ) -> HomeDataApi:
117
121
  """Create a home data API wrapper.
118
122
 
119
123
  This function creates a wrapper around the Roborock API client to fetch
@@ -122,7 +126,7 @@ def create_home_data_api(email: str, user_data: UserData) -> HomeDataApi:
122
126
 
123
127
  # Note: This will auto discover the API base URL. This can be improved
124
128
  # by caching this next to `UserData` if needed to avoid unnecessary API calls.
125
- client = RoborockApiClient(email)
129
+ client = RoborockApiClient(username=email, base_url=base_url, session=session)
126
130
 
127
131
  async def home_data_api() -> HomeData:
128
132
  return await client.get_home_data_v3(user_data)
@@ -6,6 +6,7 @@ import hmac
6
6
  import logging
7
7
  import math
8
8
  import secrets
9
+ import string
9
10
  import time
10
11
 
11
12
  import aiohttp
@@ -190,6 +191,113 @@ class RoborockApiClient:
190
191
  else:
191
192
  raise RoborockException(f"{code_response.get('msg')} - response code: {code_response.get('code')}")
192
193
 
194
+ async def request_code_v4(self) -> None:
195
+ """Request a code using the v4 endpoint."""
196
+ try:
197
+ self._login_limiter.try_acquire("login")
198
+ except BucketFullException as ex:
199
+ _LOGGER.info(ex.meta_info)
200
+ raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex
201
+ base_url = await self._get_base_url()
202
+ header_clientid = self._get_header_client_id()
203
+ code_request = PreparedRequest(
204
+ base_url,
205
+ self.session,
206
+ {
207
+ "header_clientid": header_clientid,
208
+ "Content-Type": "application/x-www-form-urlencoded",
209
+ "header_clientlang": "en",
210
+ },
211
+ )
212
+
213
+ code_response = await code_request.request(
214
+ "post",
215
+ "/api/v4/email/code/send",
216
+ params={"email": self._username, "type": "login", "platform": ""},
217
+ )
218
+ if code_response is None:
219
+ raise RoborockException("Failed to get a response from send email code")
220
+ response_code = code_response.get("code")
221
+ if response_code != 200:
222
+ _LOGGER.info("Request code failed for %s with the following context: %s", self._username, code_response)
223
+ if response_code == 2008:
224
+ raise RoborockAccountDoesNotExist("Account does not exist - check your login and try again.")
225
+ elif response_code == 9002:
226
+ raise RoborockTooFrequentCodeRequests("You have attempted to request too many codes. Try again later")
227
+ else:
228
+ raise RoborockException(f"{code_response.get('msg')} - response code: {code_response.get('code')}")
229
+
230
+ async def sign_key_v3(self, s: str) -> str:
231
+ """Sign a randomly generated string."""
232
+ base_url = await self._get_base_url()
233
+ header_clientid = self._get_header_client_id()
234
+ code_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid})
235
+
236
+ code_response = await code_request.request(
237
+ "post",
238
+ "/api/v3/key/sign",
239
+ params={"s": s},
240
+ )
241
+
242
+ if not code_response or "data" not in code_response or "k" not in code_response["data"]:
243
+ raise RoborockException("Failed to get a response from sign key")
244
+ response_code = code_response.get("code")
245
+
246
+ if response_code != 200:
247
+ _LOGGER.info("Request code failed for %s with the following context: %s", self._username, code_response)
248
+ raise RoborockException(f"{code_response.get('msg')} - response code: {code_response.get('code')}")
249
+
250
+ return code_response["data"]["k"]
251
+
252
+ async def code_login_v4(self, code: int | str, country: str, country_code: int) -> UserData:
253
+ """
254
+ Login via code authentication.
255
+ :param code: The code from the email.
256
+ :param country: The two-character representation of the country, i.e. "US"
257
+ :param country_code: the country phone number code i.e. 1 for US.
258
+ """
259
+ base_url = await self._get_base_url()
260
+ header_clientid = self._get_header_client_id()
261
+ x_mercy_ks = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
262
+ x_mercy_k = await self.sign_key_v3(x_mercy_ks)
263
+ login_request = PreparedRequest(
264
+ base_url,
265
+ self.session,
266
+ {"header_clientid": header_clientid, "x-mercy-ks": x_mercy_ks, "x-mercy-k": x_mercy_k},
267
+ )
268
+ login_response = await login_request.request(
269
+ "post",
270
+ "/api/v4/auth/email/login/code",
271
+ params={
272
+ "country": country,
273
+ "countryCode": country_code,
274
+ "email": self._username,
275
+ "code": code,
276
+ # Major and minor version are the user agreement version, we will need to see if this needs to be
277
+ # dynamic https://usiot.roborock.com/api/v3/app/agreement/latest?country=US
278
+ "majorVersion": 14,
279
+ "minorVersion": 0,
280
+ },
281
+ )
282
+ if login_response is None:
283
+ raise RoborockException("Login request response is None")
284
+ response_code = login_response.get("code")
285
+ if response_code != 200:
286
+ _LOGGER.info("Login failed for %s with the following context: %s", self._username, login_response)
287
+ if response_code == 2018:
288
+ raise RoborockInvalidCode("Invalid code - check your code and try again.")
289
+ if response_code == 3009:
290
+ raise RoborockNoUserAgreement("You must accept the user agreement in the Roborock app to continue.")
291
+ if response_code == 3006:
292
+ raise RoborockInvalidUserAgreement(
293
+ "User agreement must be accepted again - or you are attempting to use the Mi Home app account."
294
+ )
295
+ raise RoborockException(f"{login_response.get('msg')} - response code: {response_code}")
296
+ user_data = login_response.get("data")
297
+ if not isinstance(user_data, dict):
298
+ raise RoborockException("Got unexpected data type for user_data")
299
+ return UserData.from_dict(user_data)
300
+
193
301
  async def pass_login(self, password: str) -> UserData:
194
302
  try:
195
303
  self._login_limiter.try_acquire("login")