python-roborock 2.8.5__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.5 → python_roborock-2.9.0}/PKG-INFO +45 -1
  2. python_roborock-2.9.0/README.md +76 -0
  3. {python_roborock-2.8.5 → python_roborock-2.9.0}/pyproject.toml +5 -1
  4. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/code_mappings.py +27 -0
  5. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/const.py +1 -0
  6. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/containers.py +34 -20
  7. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/roborock_typing.py +6 -0
  8. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/version_1_apis/roborock_client_v1.py +7 -0
  9. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/web_api.py +31 -11
  10. python_roborock-2.8.5/README.md +0 -32
  11. {python_roborock-2.8.5 → python_roborock-2.9.0}/LICENSE +0 -0
  12. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/__init__.py +0 -0
  13. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/api.py +0 -0
  14. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/cli.py +0 -0
  15. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/cloud_api.py +0 -0
  16. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/command_cache.py +0 -0
  17. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/exceptions.py +0 -0
  18. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/local_api.py +0 -0
  19. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/protocol.py +0 -0
  20. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/py.typed +0 -0
  21. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/roborock_future.py +0 -0
  22. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/roborock_message.py +0 -0
  23. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/util.py +0 -0
  24. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/version_1_apis/__init__.py +0 -0
  25. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  26. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  27. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/version_a01_apis/__init__.py +0 -0
  28. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  29. {python_roborock-2.8.5 → python_roborock-2.9.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-roborock
3
- Version: 2.8.5
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.5"
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"
@@ -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
@@ -420,6 +421,12 @@ class RoborockClientV1(RoborockClient):
420
421
  consumable = Consumable.from_dict(value)
421
422
  for listener in self.listener_model.protocol_handlers.get(data_protocol, []):
422
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)
423
430
  return
424
431
  except ValueError:
425
432
  self._logger.warning(
@@ -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