python-roborock 2.8.4__tar.gz → 2.9.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 (29) hide show
  1. {python_roborock-2.8.4 → python_roborock-2.9.0}/PKG-INFO +46 -2
  2. python_roborock-2.9.0/README.md +76 -0
  3. {python_roborock-2.8.4 → python_roborock-2.9.0}/pyproject.toml +5 -1
  4. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/api.py +10 -16
  5. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/code_mappings.py +27 -0
  6. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/const.py +1 -0
  7. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/containers.py +34 -20
  8. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/roborock_typing.py +6 -0
  9. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/version_1_apis/roborock_client_v1.py +14 -1
  10. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/version_1_apis/roborock_local_client_v1.py +1 -0
  11. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +1 -0
  12. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/web_api.py +31 -11
  13. python_roborock-2.8.4/README.md +0 -32
  14. {python_roborock-2.8.4 → python_roborock-2.9.0}/LICENSE +0 -0
  15. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/__init__.py +0 -0
  16. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/cli.py +0 -0
  17. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/cloud_api.py +0 -0
  18. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/command_cache.py +0 -0
  19. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/exceptions.py +0 -0
  20. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/local_api.py +0 -0
  21. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/protocol.py +0 -0
  22. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/py.typed +0 -0
  23. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/roborock_future.py +0 -0
  24. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/roborock_message.py +0 -0
  25. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/util.py +0 -0
  26. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/version_1_apis/__init__.py +0 -0
  27. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/version_a01_apis/__init__.py +0 -0
  28. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  29. {python_roborock-2.8.4 → python_roborock-2.9.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: python-roborock
3
- Version: 2.8.4
3
+ Version: 2.9.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
@@ -53,6 +53,50 @@ Install this via pip (or your favourite package manager):
53
53
 
54
54
  You can see all of the commands supported [here]("https://python-roborock.readthedocs.io/en/latest/api_commands.html")
55
55
 
56
+ ## Sending Commands
57
+
58
+ Here is an example that requires no manual intervention and can be done all automatically. You can skip some steps by
59
+ caching values or looking at them and grabbing them manually.
60
+ ```python
61
+ import asyncio
62
+
63
+ from roborock import HomeDataProduct, DeviceData, RoborockCommand
64
+ from roborock.version_1_apis import RoborockMqttClientV1, RoborockLocalClientV1
65
+ from roborock.web_api import RoborockApiClient
66
+
67
+ async def main():
68
+ web_api = RoborockApiClient(username="youremailhere")
69
+ # Login via your password
70
+ user_data = await web_api.pass_login(password="pass_here")
71
+ # Or login via a code
72
+ await web_api.request_code()
73
+ code = input("What is the code?")
74
+ user_data = await web_api.code_login(code)
75
+
76
+ # Get home data
77
+ home_data = await web_api.get_home_data_v2(user_data)
78
+
79
+ # Get the device you want
80
+ device = home_data.devices[0]
81
+
82
+ # Get product ids:
83
+ product_info: dict[str, HomeDataProduct] = {
84
+ product.id: product for product in home_data.products
85
+ }
86
+ # Create the Mqtt(aka cloud required) Client
87
+ device_data = DeviceData(device, product_info[device.product_id].model)
88
+ mqtt_client = RoborockMqttClientV1(user_data, device_data)
89
+ networking = await mqtt_client.get_networking()
90
+ local_device_data = DeviceData(device, product_info[device.product_id].model, networking)
91
+ local_client = RoborockLocalClientV1(local_device_data)
92
+ # You can use the send_command to send any command to the device
93
+ status = await local_client.send_command(RoborockCommand.GET_STATUS)
94
+ # Or use existing functions that will give you data classes
95
+ status = await local_client.get_status()
96
+
97
+ asyncio.run(main())
98
+ ```
99
+
56
100
  ## Supported devices
57
101
 
58
102
  You can find what devices are supported
@@ -0,0 +1,76 @@
1
+ # Roborock
2
+
3
+ <p align="center">
4
+ <a href="https://pypi.org/project/python-roborock/">
5
+ <img src="https://img.shields.io/pypi/v/python-roborock.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
6
+ </a>
7
+ <img src="https://img.shields.io/pypi/pyversions/python-roborock.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">
8
+ <img src="https://img.shields.io/pypi/l/python-roborock.svg?style=flat-square" alt="License">
9
+ </p>
10
+
11
+ Roborock library for online and offline control of your vacuums.
12
+
13
+ ## Installation
14
+
15
+ Install this via pip (or your favourite package manager):
16
+
17
+ `pip install python-roborock`
18
+
19
+ ## Functionality
20
+
21
+ You can see all of the commands supported [here]("https://python-roborock.readthedocs.io/en/latest/api_commands.html")
22
+
23
+ ## Sending Commands
24
+
25
+ Here is an example that requires no manual intervention and can be done all automatically. You can skip some steps by
26
+ caching values or looking at them and grabbing them manually.
27
+ ```python
28
+ import asyncio
29
+
30
+ from roborock import HomeDataProduct, DeviceData, RoborockCommand
31
+ from roborock.version_1_apis import RoborockMqttClientV1, RoborockLocalClientV1
32
+ from roborock.web_api import RoborockApiClient
33
+
34
+ async def main():
35
+ web_api = RoborockApiClient(username="youremailhere")
36
+ # Login via your password
37
+ user_data = await web_api.pass_login(password="pass_here")
38
+ # Or login via a code
39
+ await web_api.request_code()
40
+ code = input("What is the code?")
41
+ user_data = await web_api.code_login(code)
42
+
43
+ # Get home data
44
+ home_data = await web_api.get_home_data_v2(user_data)
45
+
46
+ # Get the device you want
47
+ device = home_data.devices[0]
48
+
49
+ # Get product ids:
50
+ product_info: dict[str, HomeDataProduct] = {
51
+ product.id: product for product in home_data.products
52
+ }
53
+ # Create the Mqtt(aka cloud required) Client
54
+ device_data = DeviceData(device, product_info[device.product_id].model)
55
+ mqtt_client = RoborockMqttClientV1(user_data, device_data)
56
+ networking = await mqtt_client.get_networking()
57
+ local_device_data = DeviceData(device, product_info[device.product_id].model, networking)
58
+ local_client = RoborockLocalClientV1(local_device_data)
59
+ # You can use the send_command to send any command to the device
60
+ status = await local_client.send_command(RoborockCommand.GET_STATUS)
61
+ # Or use existing functions that will give you data classes
62
+ status = await local_client.get_status()
63
+
64
+ asyncio.run(main())
65
+ ```
66
+
67
+ ## Supported devices
68
+
69
+ You can find what devices are supported
70
+ [here]("https://python-roborock.readthedocs.io/en/latest/supported_devices.html").
71
+ Please note this may not immediately contain the latest devices.
72
+
73
+
74
+ ## Credits
75
+
76
+ Thanks @rovo89 for https://gist.github.com/rovo89/dff47ed19fca0dfdda77503e66c2b7c7 And thanks @PiotrMachowski for https://github.com/PiotrMachowski/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-roborock"
3
- version = "2.8.4"
3
+ version = "2.9.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"
@@ -45,6 +45,7 @@ mypy = "*"
45
45
  ruff = "*"
46
46
  codespell = "*"
47
47
  pyshark = "^0.6"
48
+ aioresponses = "^0.7.7"
48
49
 
49
50
  [tool.semantic_release]
50
51
  branch = "main"
@@ -67,3 +68,6 @@ select=["E", "F", "UP", "I"]
67
68
 
68
69
  [tool.ruff.lint.per-file-ignores]
69
70
  "*/__init__.py" = ["F401"]
71
+
72
+ [tool.pytest.ini_options]
73
+ asyncio_mode = "auto"
@@ -7,7 +7,7 @@ import base64
7
7
  import logging
8
8
  import secrets
9
9
  import time
10
- from collections.abc import Callable, Coroutine
10
+ from collections.abc import Coroutine
11
11
  from typing import Any
12
12
 
13
13
  from .containers import (
@@ -36,8 +36,8 @@ class RoborockClient:
36
36
  self._endpoint = endpoint
37
37
  self._nonce = secrets.token_bytes(16)
38
38
  self._waiting_queue: dict[int, RoborockFuture] = {}
39
- self._last_device_msg_in = self.time_func()
40
- self._last_disconnection = self.time_func()
39
+ self._last_device_msg_in = time.monotonic()
40
+ self._last_disconnection = time.monotonic()
41
41
  self.keep_alive = KEEPALIVE
42
42
  self._diagnostic_data: dict[str, dict[str, Any]] = {
43
43
  "misc_info": {"Nonce": base64.b64encode(self._nonce).decode("utf-8")}
@@ -59,15 +59,6 @@ class RoborockClient:
59
59
  def diagnostic_data(self) -> dict:
60
60
  return self._diagnostic_data
61
61
 
62
- @property
63
- def time_func(self) -> Callable[[], float]:
64
- try:
65
- # Use monotonic clock if available
66
- time_func = time.monotonic
67
- except AttributeError:
68
- time_func = time.time
69
- return time_func
70
-
71
62
  async def async_connect(self):
72
63
  raise NotImplementedError
73
64
 
@@ -81,13 +72,13 @@ class RoborockClient:
81
72
  raise NotImplementedError
82
73
 
83
74
  def on_connection_lost(self, exc: Exception | None) -> None:
84
- self._last_disconnection = self.time_func()
75
+ self._last_disconnection = time.monotonic()
85
76
  self._logger.info("Roborock client disconnected")
86
77
  if exc is not None:
87
78
  self._logger.warning(exc)
88
79
 
89
80
  def should_keepalive(self) -> bool:
90
- now = self.time_func()
81
+ now = time.monotonic()
91
82
  # noinspection PyUnresolvedReferences
92
83
  if now - self._last_disconnection > self.keep_alive**2 and now - self._last_device_msg_in > self.keep_alive:
93
84
  return False
@@ -116,8 +107,11 @@ class RoborockClient:
116
107
  if request_id in self._waiting_queue:
117
108
  new_id = get_next_int(10000, 32767)
118
109
  _LOGGER.warning(
119
- f"Attempting to create a future with an existing request_id... New id is {new_id}. "
120
- f"Code may not function properly."
110
+ "Attempting to create a future with an existing id %s (%s)... New id is %s. "
111
+ "Code may not function properly.",
112
+ request_id,
113
+ protocol_id,
114
+ new_id,
121
115
  )
122
116
  request_id = new_id
123
117
  self._waiting_queue[request_id] = queue
@@ -254,6 +254,15 @@ class RoborockFanSpeedQRevoMaster(RoborockFanPowerCode):
254
254
  smart_mode = 110
255
255
 
256
256
 
257
+ class RoborockFanSpeedQRevoCurv(RoborockFanPowerCode):
258
+ quiet = 101
259
+ balanced = 102
260
+ turbo = 103
261
+ max = 104
262
+ max_plus = 105
263
+ smart_mode = 110
264
+
265
+
257
266
  class RoborockFanSpeedP10(RoborockFanPowerCode):
258
267
  off = 105
259
268
  quiet = 101
@@ -279,6 +288,14 @@ class RoborockMopModeCode(RoborockEnum):
279
288
  """Describes the mop mode of the vacuum cleaner."""
280
289
 
281
290
 
291
+ class RoborockMopModeQRevoCurv(RoborockMopModeCode):
292
+ standard = 300
293
+ deep = 301
294
+ deep_plus = 303
295
+ fast = 304
296
+ smart_mode = 306
297
+
298
+
282
299
  class RoborockMopModeS7(RoborockMopModeCode):
283
300
  """Describes the mop mode of the vacuum cleaner."""
284
301
 
@@ -351,6 +368,15 @@ class RoborockMopIntensityQRevoMaster(RoborockMopIntensityCode):
351
368
  smart_mode = 209
352
369
 
353
370
 
371
+ class RoborockMopIntensityQRevoCurv(RoborockMopIntensityCode):
372
+ off = 200
373
+ low = 201
374
+ medium = 202
375
+ high = 203
376
+ custom_water_flow = 207
377
+ smart_mode = 209
378
+
379
+
354
380
  class RoborockMopIntensityP10(RoborockMopIntensityCode):
355
381
  """Describes the mop intensity of the vacuum cleaner."""
356
382
 
@@ -431,6 +457,7 @@ class RoborockDockTypeCode(RoborockEnum):
431
457
  s8_maxv_ultra_dock = 10
432
458
  qrevo_master_dock = 14
433
459
  qrevo_s_dock = 15
460
+ qrevo_curv_dock = 17
434
461
 
435
462
 
436
463
  class RoborockDockDustCollectionModeCode(RoborockEnum):
@@ -31,6 +31,7 @@ ROBOROCK_Q7 = "roborock.vacuum.a40"
31
31
  ROBOROCK_Q7_MAX = "roborock.vacuum.a38"
32
32
  ROBOROCK_Q7PLUS = "roborock.vacuum.a40"
33
33
  ROBOROCK_QREVO_MASTER = "roborock.vacuum.a117"
34
+ ROBOROCK_QREVO_CURV = "roborock.vacuum.a135"
34
35
  ROBOROCK_Q8_MAX = "roborock.vacuum.a73"
35
36
  ROBOROCK_G10S_PRO = "roborock.vacuum.a26"
36
37
  ROBOROCK_G10S = "roborock.vacuum.a46"
@@ -20,6 +20,7 @@ from .code_mappings import (
20
20
  RoborockFanPowerCode,
21
21
  RoborockFanSpeedP10,
22
22
  RoborockFanSpeedQ7Max,
23
+ RoborockFanSpeedQRevoCurv,
23
24
  RoborockFanSpeedQRevoMaster,
24
25
  RoborockFanSpeedS6Pure,
25
26
  RoborockFanSpeedS7,
@@ -30,12 +31,14 @@ from .code_mappings import (
30
31
  RoborockMopIntensityCode,
31
32
  RoborockMopIntensityP10,
32
33
  RoborockMopIntensityQ7Max,
34
+ RoborockMopIntensityQRevoCurv,
33
35
  RoborockMopIntensityQRevoMaster,
34
36
  RoborockMopIntensityS5Max,
35
37
  RoborockMopIntensityS6MaxV,
36
38
  RoborockMopIntensityS7,
37
39
  RoborockMopIntensityS8MaxVUltra,
38
40
  RoborockMopModeCode,
41
+ RoborockMopModeQRevoCurv,
39
42
  RoborockMopModeQRevoMaster,
40
43
  RoborockMopModeS7,
41
44
  RoborockMopModeS8MaxVUltra,
@@ -52,6 +55,7 @@ from .const import (
52
55
  ROBOROCK_G10S_PRO,
53
56
  ROBOROCK_P10,
54
57
  ROBOROCK_Q7_MAX,
58
+ ROBOROCK_QREVO_CURV,
55
59
  ROBOROCK_QREVO_MASTER,
56
60
  ROBOROCK_QREVO_MAXV,
57
61
  ROBOROCK_QREVO_PRO,
@@ -581,6 +585,13 @@ class QRevoMasterStatus(Status):
581
585
  mop_mode: RoborockMopModeQRevoMaster | None = None
582
586
 
583
587
 
588
+ @dataclass
589
+ class QRevoCurvStatus(Status):
590
+ fan_power: RoborockFanSpeedQRevoCurv | None = None
591
+ water_box_mode: RoborockMopIntensityQRevoCurv | None = None
592
+ mop_mode: RoborockMopModeQRevoCurv | None = None
593
+
594
+
584
595
  @dataclass
585
596
  class S6MaxVStatus(Status):
586
597
  fan_power: RoborockFanSpeedS7MaxV | None = None
@@ -639,6 +650,7 @@ ModelStatus: dict[str, type[Status]] = {
639
650
  ROBOROCK_S5_MAX: S5MaxStatus,
640
651
  ROBOROCK_Q7_MAX: Q7MaxStatus,
641
652
  ROBOROCK_QREVO_MASTER: QRevoMasterStatus,
653
+ ROBOROCK_QREVO_CURV: QRevoCurvStatus,
642
654
  ROBOROCK_S6: S6PureStatus,
643
655
  ROBOROCK_S6_MAXV: S6MaxVStatus,
644
656
  ROBOROCK_S6_PURE: S6PureStatus,
@@ -881,26 +893,28 @@ class RoborockProductSpec(RoborockBase):
881
893
 
882
894
  @dataclass
883
895
  class RoborockProduct(RoborockBase):
884
- id: int
885
- name: str
886
- model: str
887
- packagename: str
888
- ssid: str
889
- picurl: str
890
- cardpicurl: str
891
- medium_cardpicurl: str
892
- resetwifipicurl: str
893
- resetwifitext: dict
894
- tuyaid: str
895
- status: int
896
- rriotid: str
897
- cardspec: str
898
- pictures: list
899
- nc_mode: str
900
- scope: None
901
- product_tags: list
902
- agreements: list
903
- plugin_pic_url: None
896
+ id: int | None = None
897
+ name: str | None = None
898
+ model: str | None = None
899
+ packagename: str | None = None
900
+ ssid: str | None = None
901
+ picurl: str | None = None
902
+ cardpicurl: str | None = None
903
+ mediumCardpicurl: str | None = None
904
+ resetwifipicurl: str | None = None
905
+ configPicUrl: str | None = None
906
+ pluginPicUrl: str | None = None
907
+ resetwifitext: dict | None = None
908
+ tuyaid: str | None = None
909
+ status: int | None = None
910
+ rriotid: str | None = None
911
+ pictures: list | None = None
912
+ ncMode: str | None = None
913
+ scope: str | None = None
914
+ product_tags: list | None = None
915
+ agreements: list | None = None
916
+ cardspec: str | None = None
917
+ plugin_pic_url: str | None = None
904
918
  products_specification: RoborockProductSpec | None = None
905
919
 
906
920
  def __post_init__(self):
@@ -461,6 +461,11 @@ class DeviceProp(RoborockBase):
461
461
  consumable: Consumable = field(default_factory=Consumable)
462
462
  last_clean_record: CleanRecord | None = None
463
463
  dock_summary: DockSummary | None = None
464
+ dust_collection_mode_name: str | None = None
465
+
466
+ def __post_init__(self) -> None:
467
+ if self.dock_summary and self.dock_summary.dust_collection_mode and self.dock_summary.dust_collection_mode.mode:
468
+ self.dust_collection_mode_name = self.dock_summary.dust_collection_mode.mode.name
464
469
 
465
470
  def update(self, device_prop: DeviceProp) -> None:
466
471
  if device_prop.status:
@@ -473,3 +478,4 @@ class DeviceProp(RoborockBase):
473
478
  self.last_clean_record = device_prop.last_clean_record
474
479
  if device_prop.dock_summary:
475
480
  self.dock_summary = device_prop.dock_summary
481
+ self.__post_init__()
@@ -71,6 +71,7 @@ WASH_N_FILL_DOCK = [
71
71
  RoborockDockTypeCode.p10_pro_dock,
72
72
  RoborockDockTypeCode.s8_maxv_ultra_dock,
73
73
  RoborockDockTypeCode.qrevo_s_dock,
74
+ RoborockDockTypeCode.qrevo_curv_dock,
74
75
  ]
75
76
  RT = TypeVar("RT", bound=RoborockBase)
76
77
  EVICT_TIME = 60
@@ -361,7 +362,7 @@ class RoborockClientV1(RoborockClient):
361
362
 
362
363
  def on_message_received(self, messages: list[RoborockMessage]) -> None:
363
364
  try:
364
- self._last_device_msg_in = self.time_func()
365
+ self._last_device_msg_in = time.monotonic()
365
366
  for data in messages:
366
367
  protocol = data.protocol
367
368
  if data.payload and protocol in [
@@ -391,6 +392,8 @@ class RoborockClientV1(RoborockClient):
391
392
  if isinstance(result, list) and len(result) == 1:
392
393
  result = result[0]
393
394
  queue.resolve((result, None))
395
+ else:
396
+ self._logger.debug("Received response for unknown request id %s", request_id)
394
397
  else:
395
398
  try:
396
399
  data_protocol = RoborockDataProtocol(int(data_point_number))
@@ -418,6 +421,12 @@ class RoborockClientV1(RoborockClient):
418
421
  consumable = Consumable.from_dict(value)
419
422
  for listener in self.listener_model.protocol_handlers.get(data_protocol, []):
420
423
  listener(consumable)
424
+ else:
425
+ self._logger.warning(
426
+ f"Unknown data protocol {data_point_number}, please create an "
427
+ f"issue on the python-roborock repository"
428
+ )
429
+ self._logger.info(data)
421
430
  return
422
431
  except ValueError:
423
432
  self._logger.warning(
@@ -443,10 +452,14 @@ class RoborockClientV1(RoborockClient):
443
452
  if isinstance(decompressed, list):
444
453
  decompressed = decompressed[0]
445
454
  queue.resolve((decompressed, None))
455
+ else:
456
+ self._logger.debug("Received response for unknown request id %s", request_id)
446
457
  else:
447
458
  queue = self._waiting_queue.get(data.seq)
448
459
  if queue:
449
460
  queue.resolve((data.payload, None))
461
+ else:
462
+ self._logger.debug("Received response for unknown request id %s", data.seq)
450
463
  except Exception as ex:
451
464
  self._logger.exception(ex)
452
465
 
@@ -18,6 +18,7 @@ class RoborockLocalClientV1(RoborockLocalClient, RoborockClientV1):
18
18
  ) -> RoborockMessage:
19
19
  secured = True if method in COMMANDS_SECURED else False
20
20
  request_id, timestamp, payload = self._get_payload(method, params, secured)
21
+ self._logger.debug("Building message id %s for method %s", request_id, method)
21
22
  request_protocol = RoborockMessageProtocol.GENERAL_REQUEST
22
23
  message_retry: MessageRetry | None = None
23
24
  if method == RoborockCommand.RETRY_REQUEST and isinstance(params, dict):
@@ -74,6 +74,7 @@ class RoborockMqttClientV1(RoborockMqttClient, RoborockClientV1):
74
74
  # When we have more custom commands do something more complicated here
75
75
  return await self._get_calibration_points()
76
76
  request_id, timestamp, payload = self._get_payload(method, params, True)
77
+ self._logger.debug("Building message id %s for method %s", request_id, method)
77
78
  request_protocol = RoborockMessageProtocol.RPC_REQUEST
78
79
  roborock_message = RoborockMessage(timestamp=timestamp, protocol=request_protocol, payload=payload)
79
80
  return await self.send_message(roborock_message)
@@ -54,7 +54,7 @@ class RoborockApiClient:
54
54
  raise RoborockMissingParameters(
55
55
  "You are missing parameters for this request, are you sure you " "entered your username?"
56
56
  )
57
- raise RoborockUrlException(response.get("error"))
57
+ raise RoborockUrlException(f"error code: {response_code} msg: {response.get('error')}")
58
58
  response_data = response.get("data")
59
59
  if response_data is None:
60
60
  raise RoborockUrlException("response does not have 'data'")
@@ -146,7 +146,7 @@ class RoborockApiClient:
146
146
  """
147
147
  raise NotImplementedError("Pass_login_v3 has not yet been implemented")
148
148
 
149
- async def code_login(self, code) -> UserData:
149
+ async def code_login(self, code: int | str) -> UserData:
150
150
  base_url = await self._get_base_url()
151
151
  header_clientid = self._get_header_client_id()
152
152
 
@@ -276,7 +276,7 @@ class RoborockApiClient:
276
276
  product_request = PreparedRequest(base_url, {"header_clientid": header_clientid})
277
277
  product_response = await product_request.request(
278
278
  "get",
279
- "/api/v3/product",
279
+ "/api/v4/product",
280
280
  headers={"Authorization": user_data.token},
281
281
  )
282
282
  if product_response is None:
@@ -288,24 +288,44 @@ class RoborockApiClient:
288
288
  return ProductResponse.from_dict(result)
289
289
  raise RoborockException("product result was an unexpected type")
290
290
 
291
+ async def download_code(self, user_data: UserData, product_id: int):
292
+ base_url = await self._get_base_url()
293
+ header_clientid = self._get_header_client_id()
294
+ product_request = PreparedRequest(base_url, {"header_clientid": header_clientid})
295
+ request = {"apilevel": 99999, "productids": [product_id], "type": 2}
296
+ response = await product_request.request(
297
+ "post",
298
+ "/api/v1/appplugin",
299
+ json=request,
300
+ headers={"Authorization": user_data.token, "Content-Type": "application/json"},
301
+ )
302
+ return response["data"][0]["url"]
303
+
304
+ async def download_category_code(self, user_data: UserData):
305
+ base_url = await self._get_base_url()
306
+ header_clientid = self._get_header_client_id()
307
+ product_request = PreparedRequest(base_url, {"header_clientid": header_clientid})
308
+ response = await product_request.request(
309
+ "get",
310
+ "api/v1/plugins?apiLevel=99999&type=2",
311
+ headers={
312
+ "Authorization": user_data.token,
313
+ },
314
+ )
315
+ return {r["category"]: r["url"] for r in response["data"]["categoryPluginList"]}
316
+
291
317
 
292
318
  class PreparedRequest:
293
319
  def __init__(self, base_url: str, base_headers: dict | None = None) -> None:
294
320
  self.base_url = base_url
295
321
  self.base_headers = base_headers or {}
296
322
 
297
- async def request(self, method: str, url: str, params=None, data=None, headers=None) -> dict:
323
+ async def request(self, method: str, url: str, params=None, data=None, headers=None, json=None) -> dict:
298
324
  _url = "/".join(s.strip("/") for s in [self.base_url, url])
299
325
  _headers = {**self.base_headers, **(headers or {})}
300
326
  async with aiohttp.ClientSession() as session:
301
327
  try:
302
- async with session.request(
303
- method,
304
- _url,
305
- params=params,
306
- data=data,
307
- headers=_headers,
308
- ) as resp:
328
+ async with session.request(method, _url, params=params, data=data, headers=_headers, json=json) as resp:
309
329
  return await resp.json()
310
330
  except ContentTypeError as err:
311
331
  """If we get an error, lets log everything for debugging."""
@@ -1,32 +0,0 @@
1
- # Roborock
2
-
3
- <p align="center">
4
- <a href="https://pypi.org/project/python-roborock/">
5
- <img src="https://img.shields.io/pypi/v/python-roborock.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
6
- </a>
7
- <img src="https://img.shields.io/pypi/pyversions/python-roborock.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">
8
- <img src="https://img.shields.io/pypi/l/python-roborock.svg?style=flat-square" alt="License">
9
- </p>
10
-
11
- Roborock library for online and offline control of your vacuums.
12
-
13
- ## Installation
14
-
15
- Install this via pip (or your favourite package manager):
16
-
17
- `pip install python-roborock`
18
-
19
- ## Functionality
20
-
21
- You can see all of the commands supported [here]("https://python-roborock.readthedocs.io/en/latest/api_commands.html")
22
-
23
- ## Supported devices
24
-
25
- You can find what devices are supported
26
- [here]("https://python-roborock.readthedocs.io/en/latest/supported_devices.html").
27
- Please note this may not immediately contain the latest devices.
28
-
29
-
30
- ## Credits
31
-
32
- Thanks @rovo89 for https://gist.github.com/rovo89/dff47ed19fca0dfdda77503e66c2b7c7 And thanks @PiotrMachowski for https://github.com/PiotrMachowski/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor
File without changes