python-roborock 0.41.0__tar.gz → 2.0.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.
- {python_roborock-0.41.0 → python_roborock-2.0.0}/PKG-INFO +1 -1
- {python_roborock-0.41.0 → python_roborock-2.0.0}/pyproject.toml +13 -1
- python_roborock-2.0.0/roborock/api.py +127 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/cloud_api.py +6 -3
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/code_mappings.py +218 -2
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/containers.py +25 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/local_api.py +1 -38
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/protocol.py +21 -5
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/roborock_message.py +44 -2
- python_roborock-2.0.0/roborock/version_1_apis/__init__.py +3 -0
- python_roborock-0.41.0/roborock/api.py → python_roborock-2.0.0/roborock/version_1_apis/roborock_client_v1.py +239 -136
- python_roborock-2.0.0/roborock/version_1_apis/roborock_local_client_v1.py +72 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +5 -2
- python_roborock-2.0.0/roborock/version_a01_apis/__init__.py +2 -0
- python_roborock-2.0.0/roborock/version_a01_apis/roborock_client_a01.py +142 -0
- python_roborock-2.0.0/roborock/version_a01_apis/roborock_mqtt_client_a01.py +68 -0
- python_roborock-0.41.0/roborock/version_1_apis/__init__.py +0 -0
- python_roborock-0.41.0/roborock/version_1_apis/roborock_client_v1.py +0 -227
- python_roborock-0.41.0/roborock/version_1_apis/roborock_local_client_v1.py +0 -32
- {python_roborock-0.41.0 → python_roborock-2.0.0}/LICENSE +0 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/README.md +0 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/__init__.py +0 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/cli.py +0 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/command_cache.py +0 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/const.py +0 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/exceptions.py +0 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/py.typed +0 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/roborock_future.py +0 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/util.py +0 -0
- {python_roborock-0.41.0 → python_roborock-2.0.0}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-roborock"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "2.0.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"
|
|
@@ -49,8 +49,20 @@ pyshark = "^0.6"
|
|
|
49
49
|
branch = "main"
|
|
50
50
|
version_toml = "pyproject.toml:tool.poetry.version"
|
|
51
51
|
build_command = "pip install poetry && poetry build"
|
|
52
|
+
[tool.semantic_release.commit_parser_options]
|
|
53
|
+
allowed_tags = [
|
|
54
|
+
"chore",
|
|
55
|
+
"docs",
|
|
56
|
+
"feat",
|
|
57
|
+
"fix",
|
|
58
|
+
"refactor"
|
|
59
|
+
]
|
|
60
|
+
major_tags= ["refactor"]
|
|
52
61
|
|
|
53
62
|
[tool.ruff]
|
|
54
63
|
ignore = ["F403", "E741"]
|
|
55
64
|
line-length = 120
|
|
56
65
|
select=["E", "F", "UP", "I"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint.per-file-ignores]
|
|
68
|
+
"*/__init__.py" = ["F401"]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""The Roborock api."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import base64
|
|
7
|
+
import logging
|
|
8
|
+
import secrets
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Callable, Coroutine
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .containers import (
|
|
14
|
+
DeviceData,
|
|
15
|
+
)
|
|
16
|
+
from .exceptions import (
|
|
17
|
+
RoborockTimeout,
|
|
18
|
+
UnknownMethodError,
|
|
19
|
+
VacuumError,
|
|
20
|
+
)
|
|
21
|
+
from .roborock_future import RoborockFuture
|
|
22
|
+
from .roborock_message import (
|
|
23
|
+
RoborockMessage,
|
|
24
|
+
)
|
|
25
|
+
from .roborock_typing import RoborockCommand
|
|
26
|
+
from .util import RoborockLoggerAdapter, get_running_loop_or_create_one
|
|
27
|
+
|
|
28
|
+
_LOGGER = logging.getLogger(__name__)
|
|
29
|
+
KEEPALIVE = 60
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RoborockClient:
|
|
33
|
+
def __init__(self, endpoint: str, device_info: DeviceData, queue_timeout: int = 4) -> None:
|
|
34
|
+
self.event_loop = get_running_loop_or_create_one()
|
|
35
|
+
self.device_info = device_info
|
|
36
|
+
self._endpoint = endpoint
|
|
37
|
+
self._nonce = secrets.token_bytes(16)
|
|
38
|
+
self._waiting_queue: dict[int, RoborockFuture] = {}
|
|
39
|
+
self._last_device_msg_in = self.time_func()
|
|
40
|
+
self._last_disconnection = self.time_func()
|
|
41
|
+
self.keep_alive = KEEPALIVE
|
|
42
|
+
self._diagnostic_data: dict[str, dict[str, Any]] = {
|
|
43
|
+
"misc_info": {"Nonce": base64.b64encode(self._nonce).decode("utf-8")}
|
|
44
|
+
}
|
|
45
|
+
self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER)
|
|
46
|
+
self.is_available: bool = True
|
|
47
|
+
self.queue_timeout = queue_timeout
|
|
48
|
+
|
|
49
|
+
def __del__(self) -> None:
|
|
50
|
+
self.release()
|
|
51
|
+
|
|
52
|
+
def release(self):
|
|
53
|
+
self.sync_disconnect()
|
|
54
|
+
|
|
55
|
+
async def async_release(self):
|
|
56
|
+
await self.async_disconnect()
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def diagnostic_data(self) -> dict:
|
|
60
|
+
return self._diagnostic_data
|
|
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
|
+
async def async_connect(self):
|
|
72
|
+
raise NotImplementedError
|
|
73
|
+
|
|
74
|
+
def sync_disconnect(self) -> Any:
|
|
75
|
+
raise NotImplementedError
|
|
76
|
+
|
|
77
|
+
async def async_disconnect(self) -> Any:
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
|
|
80
|
+
def on_message_received(self, messages: list[RoborockMessage]) -> None:
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
|
|
83
|
+
def on_connection_lost(self, exc: Exception | None) -> None:
|
|
84
|
+
self._last_disconnection = self.time_func()
|
|
85
|
+
self._logger.info("Roborock client disconnected")
|
|
86
|
+
if exc is not None:
|
|
87
|
+
self._logger.warning(exc)
|
|
88
|
+
|
|
89
|
+
def should_keepalive(self) -> bool:
|
|
90
|
+
now = self.time_func()
|
|
91
|
+
# noinspection PyUnresolvedReferences
|
|
92
|
+
if now - self._last_disconnection > self.keep_alive**2 and now - self._last_device_msg_in > self.keep_alive:
|
|
93
|
+
return False
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
async def validate_connection(self) -> None:
|
|
97
|
+
if not self.should_keepalive():
|
|
98
|
+
await self.async_disconnect()
|
|
99
|
+
await self.async_connect()
|
|
100
|
+
|
|
101
|
+
async def _wait_response(self, request_id: int, queue: RoborockFuture) -> tuple[Any, VacuumError | None]:
|
|
102
|
+
try:
|
|
103
|
+
(response, err) = await queue.async_get(self.queue_timeout)
|
|
104
|
+
if response == "unknown_method":
|
|
105
|
+
raise UnknownMethodError("Unknown method")
|
|
106
|
+
return response, err
|
|
107
|
+
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
108
|
+
raise RoborockTimeout(f"id={request_id} Timeout after {self.queue_timeout} seconds") from None
|
|
109
|
+
finally:
|
|
110
|
+
self._waiting_queue.pop(request_id, None)
|
|
111
|
+
|
|
112
|
+
def _async_response(
|
|
113
|
+
self, request_id: int, protocol_id: int = 0
|
|
114
|
+
) -> Coroutine[Any, Any, tuple[Any, VacuumError | None]]:
|
|
115
|
+
queue = RoborockFuture(protocol_id)
|
|
116
|
+
self._waiting_queue[request_id] = queue
|
|
117
|
+
return self._wait_response(request_id, queue)
|
|
118
|
+
|
|
119
|
+
async def send_message(self, roborock_message: RoborockMessage):
|
|
120
|
+
raise NotImplementedError
|
|
121
|
+
|
|
122
|
+
async def _send_command(
|
|
123
|
+
self,
|
|
124
|
+
method: RoborockCommand | str,
|
|
125
|
+
params: list | dict | int | None = None,
|
|
126
|
+
):
|
|
127
|
+
raise NotImplementedError
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import base64
|
|
5
5
|
import logging
|
|
6
6
|
import threading
|
|
7
|
+
import typing
|
|
7
8
|
import uuid
|
|
8
9
|
from asyncio import Lock, Task
|
|
9
10
|
from typing import Any
|
|
@@ -11,15 +12,17 @@ from urllib.parse import urlparse
|
|
|
11
12
|
|
|
12
13
|
import paho.mqtt.client as mqtt
|
|
13
14
|
|
|
14
|
-
from .api import KEEPALIVE, RoborockClient
|
|
15
|
+
from .api import KEEPALIVE, RoborockClient
|
|
15
16
|
from .containers import DeviceData, UserData
|
|
16
17
|
from .exceptions import RoborockException, VacuumError
|
|
17
|
-
from .protocol import MessageParser, Utils
|
|
18
|
+
from .protocol import MessageParser, Utils, md5hex
|
|
18
19
|
from .roborock_future import RoborockFuture
|
|
19
20
|
from .roborock_message import RoborockMessage
|
|
20
21
|
from .roborock_typing import RoborockCommand
|
|
21
22
|
from .util import RoborockLoggerAdapter
|
|
22
23
|
|
|
24
|
+
if typing.TYPE_CHECKING:
|
|
25
|
+
pass
|
|
23
26
|
_LOGGER = logging.getLogger(__name__)
|
|
24
27
|
CONNECT_REQUEST_ID = 0
|
|
25
28
|
DISCONNECT_REQUEST_ID = 1
|
|
@@ -78,7 +81,7 @@ class RoborockMqttClient(RoborockClient, mqtt.Client):
|
|
|
78
81
|
connection_queue.resolve((True, None))
|
|
79
82
|
|
|
80
83
|
def on_message(self, *args, **kwargs):
|
|
81
|
-
|
|
84
|
+
client, __, msg = args
|
|
82
85
|
try:
|
|
83
86
|
messages, _ = MessageParser.parse(msg.payload, local_key=self.device_info.device.local_key)
|
|
84
87
|
super().on_message_received(messages)
|
|
@@ -98,8 +98,8 @@ class RoborockDyadStateCode(RoborockEnum):
|
|
|
98
98
|
self_clean_deep_cleaning = 6
|
|
99
99
|
self_clean_rinsing = 7
|
|
100
100
|
self_clean_dehydrating = 8
|
|
101
|
-
drying =
|
|
102
|
-
ventilating =
|
|
101
|
+
drying = 9
|
|
102
|
+
ventilating = 10 # drying
|
|
103
103
|
reserving = 12
|
|
104
104
|
mop_washing_paused = 13
|
|
105
105
|
dusting_mode = 14
|
|
@@ -369,3 +369,219 @@ class RoborockCategory(Enum):
|
|
|
369
369
|
def __missing__(self, key):
|
|
370
370
|
_LOGGER.warning("Missing key %s from category", key)
|
|
371
371
|
return RoborockCategory.UNKNOWN
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class DyadSelfCleanMode(RoborockEnum):
|
|
375
|
+
self_clean = 1
|
|
376
|
+
self_clean_and_dry = 2
|
|
377
|
+
dry = 3
|
|
378
|
+
ventilation = 4
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class DyadSelfCleanLevel(RoborockEnum):
|
|
382
|
+
normal = 1
|
|
383
|
+
deep = 2
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class DyadWarmLevel(RoborockEnum):
|
|
387
|
+
normal = 1
|
|
388
|
+
deep = 2
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class DyadMode(RoborockEnum):
|
|
392
|
+
wash = 1
|
|
393
|
+
wash_and_dry = 2
|
|
394
|
+
dry = 3
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class DyadCleanMode(RoborockEnum):
|
|
398
|
+
auto = 1
|
|
399
|
+
max = 2
|
|
400
|
+
dehydration = 3
|
|
401
|
+
power_saving = 4
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class DyadSuction(RoborockEnum):
|
|
405
|
+
l1 = 1
|
|
406
|
+
l2 = 2
|
|
407
|
+
l3 = 3
|
|
408
|
+
l4 = 4
|
|
409
|
+
l5 = 5
|
|
410
|
+
l6 = 6
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class DyadWaterLevel(RoborockEnum):
|
|
414
|
+
l1 = 1
|
|
415
|
+
l2 = 2
|
|
416
|
+
l3 = 3
|
|
417
|
+
l4 = 4
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class DyadBrushSpeed(RoborockEnum):
|
|
421
|
+
l1 = 1
|
|
422
|
+
l2 = 2
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class DyadCleanser(RoborockEnum):
|
|
426
|
+
none = 0
|
|
427
|
+
normal = 1
|
|
428
|
+
deep = 2
|
|
429
|
+
max = 3
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class DyadError(RoborockEnum):
|
|
433
|
+
none = 0
|
|
434
|
+
dirty_tank_full = 20000 # Dirty tank full. Empty it
|
|
435
|
+
water_level_sensor_stuck = 20001 # Water level sensor is stuck. Clean it.
|
|
436
|
+
clean_tank_empty = 20002 # Clean tank empty. Refill now
|
|
437
|
+
clean_head_entangled = 20003 # Check if the cleaning head is entangled with foreign objects.
|
|
438
|
+
clean_head_too_hot = 20004 # Cleaning head temperature protection. Wait for the temperature to return to normal.
|
|
439
|
+
fan_protection_e5 = 10005 # Fan protection (E5). Restart the vacuum cleaner.
|
|
440
|
+
cleaning_head_blocked = 20005 # Remove blockages from the cleaning head and pipes.
|
|
441
|
+
temperature_protection = 20006 # Temperature protection. Wait for the temperature to return to normal
|
|
442
|
+
fan_protection_e4 = 10004 # Fan protection (E4). Restart the vacuum cleaner.
|
|
443
|
+
fan_protection_e9 = 10009 # Fan protection (E9). Restart the vacuum cleaner.
|
|
444
|
+
battery_temperature_protection_e0 = 10000
|
|
445
|
+
battery_temperature_protection = (
|
|
446
|
+
20007 # Battery temperature protection. Wait for the temperature to return to a normal range.
|
|
447
|
+
)
|
|
448
|
+
battery_temperature_protection_2 = 20008
|
|
449
|
+
power_adapter_error = 20009 # Check if the power adapter is working properly.
|
|
450
|
+
dirty_charging_contacts = 10007 # Disconnection between the device and dock. Wipe charging contacts.
|
|
451
|
+
low_battery = 20017 # Low battery level. Charge before starting self-cleaning.
|
|
452
|
+
battery_under_10 = 20018 # Charge until the battery level exceeds 10% before manually starting self-cleaning.
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class ZeoMode(RoborockEnum):
|
|
456
|
+
wash = 1
|
|
457
|
+
wash_and_dry = 2
|
|
458
|
+
dry = 3
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class ZeoState(RoborockEnum):
|
|
462
|
+
standby = 1
|
|
463
|
+
weighing = 2
|
|
464
|
+
soaking = 3
|
|
465
|
+
washing = 4
|
|
466
|
+
rinsing = 5
|
|
467
|
+
spinning = 6
|
|
468
|
+
drying = 7
|
|
469
|
+
cooling = 8
|
|
470
|
+
under_delay_start = 9
|
|
471
|
+
done = 10
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class ZeoProgram(RoborockEnum):
|
|
475
|
+
standard = 1
|
|
476
|
+
quick = 2
|
|
477
|
+
sanitize = 3
|
|
478
|
+
wool = 4
|
|
479
|
+
air_refresh = 5
|
|
480
|
+
custom = 6
|
|
481
|
+
bedding = 7
|
|
482
|
+
down = 8
|
|
483
|
+
silk = 9
|
|
484
|
+
rinse_and_spin = 10
|
|
485
|
+
spin = 11
|
|
486
|
+
down_clean = 12
|
|
487
|
+
baby_care = 13
|
|
488
|
+
anti_allergen = 14
|
|
489
|
+
sportswear = 15
|
|
490
|
+
night = 16
|
|
491
|
+
new_clothes = 17
|
|
492
|
+
shirts = 18
|
|
493
|
+
synthetics = 19
|
|
494
|
+
underwear = 20
|
|
495
|
+
gentle = 21
|
|
496
|
+
intensive = 22
|
|
497
|
+
cotton_linen = 23
|
|
498
|
+
season = 24
|
|
499
|
+
warming = 25
|
|
500
|
+
bra = 26
|
|
501
|
+
panties = 27
|
|
502
|
+
boiling_wash = 28
|
|
503
|
+
socks = 30
|
|
504
|
+
towels = 31
|
|
505
|
+
anti_mite = 32
|
|
506
|
+
exo_40_60 = 33
|
|
507
|
+
twenty_c = 34
|
|
508
|
+
t_shirts = 35
|
|
509
|
+
stain_removal = 36
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class ZeoSoak(RoborockEnum):
|
|
513
|
+
normal = 0
|
|
514
|
+
low = 1
|
|
515
|
+
medium = 2
|
|
516
|
+
high = 3
|
|
517
|
+
max = 4
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class ZeoTemperature(RoborockEnum):
|
|
521
|
+
normal = 1
|
|
522
|
+
low = 2
|
|
523
|
+
medium = 3
|
|
524
|
+
high = 4
|
|
525
|
+
max = 5
|
|
526
|
+
twenty_c = 6
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class ZeoRinse(RoborockEnum):
|
|
530
|
+
none = 0
|
|
531
|
+
min = 1
|
|
532
|
+
low = 2
|
|
533
|
+
mid = 3
|
|
534
|
+
high = 4
|
|
535
|
+
max = 5
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class ZeoSpin(RoborockEnum):
|
|
539
|
+
none = 1
|
|
540
|
+
very_low = 2
|
|
541
|
+
low = 3
|
|
542
|
+
mid = 4
|
|
543
|
+
high = 5
|
|
544
|
+
very_high = 6
|
|
545
|
+
max = 7
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class ZeoDryingMode(RoborockEnum):
|
|
549
|
+
none = 0
|
|
550
|
+
quick = 1
|
|
551
|
+
iron = 2
|
|
552
|
+
store = 3
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class ZeoDetergentType(RoborockEnum):
|
|
556
|
+
empty = 0
|
|
557
|
+
low = 1
|
|
558
|
+
medium = 2
|
|
559
|
+
high = 3
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
class ZeoSoftenerType(RoborockEnum):
|
|
563
|
+
empty = 0
|
|
564
|
+
low = 1
|
|
565
|
+
medium = 2
|
|
566
|
+
high = 3
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
class ZeoError(RoborockEnum):
|
|
570
|
+
none = 0
|
|
571
|
+
refill_error = 1
|
|
572
|
+
drain_error = 2
|
|
573
|
+
door_lock_error = 3
|
|
574
|
+
water_level_error = 4
|
|
575
|
+
inverter_error = 5
|
|
576
|
+
heating_error = 6
|
|
577
|
+
temperature_error = 7
|
|
578
|
+
communication_error = 10
|
|
579
|
+
drying_error = 11
|
|
580
|
+
drying_error_e_12 = 12
|
|
581
|
+
drying_error_e_13 = 13
|
|
582
|
+
drying_error_e_14 = 14
|
|
583
|
+
drying_error_e_15 = 15
|
|
584
|
+
drying_error_e_16 = 16
|
|
585
|
+
drying_error_water_flow = 17 # Check for normal water flow
|
|
586
|
+
drying_error_restart = 18 # Restart the washer and try again
|
|
587
|
+
spin_error = 19 # re-arrange clothes
|
|
@@ -829,3 +829,28 @@ class RoborockCategoryDetail(RoborockBase):
|
|
|
829
829
|
@dataclass
|
|
830
830
|
class ProductResponse(RoborockBase):
|
|
831
831
|
category_detail_list: list[RoborockCategoryDetail]
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
@dataclass
|
|
835
|
+
class DyadProductInfo(RoborockBase):
|
|
836
|
+
sn: str
|
|
837
|
+
ssid: str
|
|
838
|
+
timezone: str
|
|
839
|
+
posix_timezone: str
|
|
840
|
+
ip: str
|
|
841
|
+
mac: str
|
|
842
|
+
oba: dict
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
@dataclass
|
|
846
|
+
class DyadSndState(RoborockBase):
|
|
847
|
+
sid_in_use: int
|
|
848
|
+
sid_version: int
|
|
849
|
+
location: str
|
|
850
|
+
bom: str
|
|
851
|
+
language: str
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
@dataclass
|
|
855
|
+
class DyadOtaNfo(RoborockBase):
|
|
856
|
+
mqttOtaData: dict
|
|
@@ -8,7 +8,7 @@ import async_timeout
|
|
|
8
8
|
|
|
9
9
|
from . import DeviceData
|
|
10
10
|
from .api import RoborockClient
|
|
11
|
-
from .exceptions import
|
|
11
|
+
from .exceptions import RoborockConnectionException, RoborockException
|
|
12
12
|
from .protocol import MessageParser
|
|
13
13
|
from .roborock_message import RoborockMessage, RoborockMessageProtocol
|
|
14
14
|
from .roborock_typing import RoborockCommand
|
|
@@ -121,40 +121,3 @@ class RoborockLocalClient(RoborockClient, asyncio.Protocol):
|
|
|
121
121
|
self.transport.write(data)
|
|
122
122
|
except Exception as e:
|
|
123
123
|
raise RoborockException(e) from e
|
|
124
|
-
|
|
125
|
-
async def send_message(self, roborock_message: RoborockMessage):
|
|
126
|
-
await self.validate_connection()
|
|
127
|
-
method = roborock_message.get_method()
|
|
128
|
-
params = roborock_message.get_params()
|
|
129
|
-
request_id: int | None
|
|
130
|
-
if not method or not method.startswith("get"):
|
|
131
|
-
request_id = roborock_message.seq
|
|
132
|
-
response_protocol = request_id + 1
|
|
133
|
-
else:
|
|
134
|
-
request_id = roborock_message.get_request_id()
|
|
135
|
-
response_protocol = RoborockMessageProtocol.GENERAL_REQUEST
|
|
136
|
-
if request_id is None:
|
|
137
|
-
raise RoborockException(f"Failed build message {roborock_message}")
|
|
138
|
-
local_key = self.device_info.device.local_key
|
|
139
|
-
msg = MessageParser.build(roborock_message, local_key=local_key)
|
|
140
|
-
if method:
|
|
141
|
-
self._logger.debug(f"id={request_id} Requesting method {method} with {params}")
|
|
142
|
-
# Send the command to the Roborock device
|
|
143
|
-
async_response = asyncio.ensure_future(self._async_response(request_id, response_protocol))
|
|
144
|
-
self._send_msg_raw(msg)
|
|
145
|
-
(response, err) = await async_response
|
|
146
|
-
self._diagnostic_data[method if method is not None else "unknown"] = {
|
|
147
|
-
"params": roborock_message.get_params(),
|
|
148
|
-
"response": response,
|
|
149
|
-
"error": err,
|
|
150
|
-
}
|
|
151
|
-
if err:
|
|
152
|
-
raise CommandVacuumError(method, err) from err
|
|
153
|
-
if roborock_message.protocol == RoborockMessageProtocol.GENERAL_REQUEST:
|
|
154
|
-
self._logger.debug(f"id={request_id} Response from method {roborock_message.get_method()}: {response}")
|
|
155
|
-
if response == "retry":
|
|
156
|
-
retry_id = roborock_message.get_retry_id()
|
|
157
|
-
return self.send_command(
|
|
158
|
-
RoborockCommand.RETRY_REQUEST, {"retry_id": retry_id, "retry_count": 8, "method": method}
|
|
159
|
-
)
|
|
160
|
-
return response
|
|
@@ -13,7 +13,6 @@ from construct import ( # type: ignore
|
|
|
13
13
|
Bytes,
|
|
14
14
|
Checksum,
|
|
15
15
|
ChecksumError,
|
|
16
|
-
Const,
|
|
17
16
|
Construct,
|
|
18
17
|
Container,
|
|
19
18
|
GreedyBytes,
|
|
@@ -36,11 +35,18 @@ from roborock.roborock_message import RoborockMessage
|
|
|
36
35
|
|
|
37
36
|
_LOGGER = logging.getLogger(__name__)
|
|
38
37
|
SALT = b"TXdfu$jyZ#TZHsg4"
|
|
38
|
+
A01_HASH = "726f626f726f636b2d67a6d6da"
|
|
39
39
|
BROADCAST_TOKEN = b"qWKYcdQWrbm9hPqe"
|
|
40
40
|
AP_CONFIG = 1
|
|
41
41
|
SOCK_DISCOVERY = 2
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def md5hex(message: str) -> str:
|
|
45
|
+
md5 = hashlib.md5()
|
|
46
|
+
md5.update(message.encode())
|
|
47
|
+
return md5.hexdigest()
|
|
48
|
+
|
|
49
|
+
|
|
44
50
|
class RoborockProtocol(asyncio.DatagramProtocol):
|
|
45
51
|
def __init__(self, timeout: int = 5):
|
|
46
52
|
self.timeout = timeout
|
|
@@ -199,12 +205,22 @@ class EncryptionAdapter(Construct):
|
|
|
199
205
|
|
|
200
206
|
:param obj: JSON object to encrypt
|
|
201
207
|
"""
|
|
208
|
+
if context.version == b"A01":
|
|
209
|
+
iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24]
|
|
210
|
+
decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
|
|
211
|
+
f = decipher.encrypt(obj)
|
|
212
|
+
return f
|
|
202
213
|
token = self.token_func(context)
|
|
203
214
|
encrypted = Utils.encrypt_ecb(obj, token)
|
|
204
215
|
return encrypted
|
|
205
216
|
|
|
206
217
|
def _decode(self, obj, context, _):
|
|
207
218
|
"""Decrypts the given payload with the token stored in the context."""
|
|
219
|
+
if context.version == b"A01":
|
|
220
|
+
iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24]
|
|
221
|
+
decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
|
|
222
|
+
f = decipher.decrypt(obj)
|
|
223
|
+
return f
|
|
208
224
|
token = self.token_func(context)
|
|
209
225
|
decrypted = Utils.decrypt_ecb(obj, token)
|
|
210
226
|
return decrypted
|
|
@@ -227,9 +243,9 @@ class OptionalChecksum(Checksum):
|
|
|
227
243
|
|
|
228
244
|
class PrefixedStruct(Struct):
|
|
229
245
|
def _parse(self, stream, context, path):
|
|
230
|
-
subcon1 = Peek(Optional(
|
|
246
|
+
subcon1 = Peek(Optional(Bytes(3)))
|
|
231
247
|
peek_version = subcon1.parse_stream(stream, **context)
|
|
232
|
-
if peek_version
|
|
248
|
+
if peek_version not in (b"1.0", b"A01"):
|
|
233
249
|
subcon2 = Bytes(4)
|
|
234
250
|
subcon2.parse_stream(stream, **context)
|
|
235
251
|
return super()._parse(stream, context, path)
|
|
@@ -251,7 +267,7 @@ class PrefixedStruct(Struct):
|
|
|
251
267
|
|
|
252
268
|
_Message = RawCopy(
|
|
253
269
|
Struct(
|
|
254
|
-
"version" /
|
|
270
|
+
"version" / Bytes(3),
|
|
255
271
|
"seq" / Int32ub,
|
|
256
272
|
"random" / Int32ub,
|
|
257
273
|
"timestamp" / Int32ub,
|
|
@@ -280,7 +296,7 @@ _BroadcastMessage = Struct(
|
|
|
280
296
|
"message"
|
|
281
297
|
/ RawCopy(
|
|
282
298
|
Struct(
|
|
283
|
-
"version" /
|
|
299
|
+
"version" / Bytes(3),
|
|
284
300
|
"seq" / Int32ub,
|
|
285
301
|
"protocol" / Int16ub,
|
|
286
302
|
"payload" / EncryptionAdapter(lambda ctx: BROADCAST_TOKEN),
|
|
@@ -57,7 +57,7 @@ class RoborockDyadDataProtocol(RoborockEnum):
|
|
|
57
57
|
COUNTDOWN_TIME = 210
|
|
58
58
|
AUTO_SELF_CLEAN_SET = 212
|
|
59
59
|
AUTO_DRY = 213
|
|
60
|
-
|
|
60
|
+
MESH_LEFT = 214
|
|
61
61
|
BRUSH_LEFT = 215
|
|
62
62
|
ERROR = 216
|
|
63
63
|
MESH_RESET = 218
|
|
@@ -70,7 +70,7 @@ class RoborockDyadDataProtocol(RoborockEnum):
|
|
|
70
70
|
SILENT_MODE = 226
|
|
71
71
|
SILENT_MODE_START_TIME = 227
|
|
72
72
|
SILENT_MODE_END_TIME = 228
|
|
73
|
-
|
|
73
|
+
RECENT_RUN_TIME = 229
|
|
74
74
|
TOTAL_RUN_TIME = 230
|
|
75
75
|
FEATURE_INFO = 235
|
|
76
76
|
RECOVER_SETTINGS = 236
|
|
@@ -87,6 +87,48 @@ class RoborockDyadDataProtocol(RoborockEnum):
|
|
|
87
87
|
RPC_RESPONSE = 10102
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
class RoborockZeoProtocol(RoborockEnum):
|
|
91
|
+
START = 200 # rw
|
|
92
|
+
PAUSE = 201 # rw
|
|
93
|
+
SHUTDOWN = 202 # rw
|
|
94
|
+
STATE = 203 # ro
|
|
95
|
+
MODE = 204 # rw
|
|
96
|
+
PROGRAM = 205 # rw
|
|
97
|
+
CHILD_LOCK = 206 # rw
|
|
98
|
+
TEMP = 207 # rw
|
|
99
|
+
RINSE_TIMES = 208 # rw
|
|
100
|
+
SPIN_LEVEL = 209 # rw
|
|
101
|
+
DRYING_MODE = 210 # rw
|
|
102
|
+
DETERGENT_SET = 211 # rw
|
|
103
|
+
SOFTENER_SET = 212 # rw
|
|
104
|
+
DETERGENT_TYPE = 213 # rw
|
|
105
|
+
SOFTENER_TYPE = 214 # rw
|
|
106
|
+
COUNTDOWN = 217 # rw
|
|
107
|
+
WASHING_LEFT = 218 # ro
|
|
108
|
+
DOORLOCK_STATE = 219 # ro
|
|
109
|
+
ERROR = 220 # ro
|
|
110
|
+
CUSTOM_PARAM_SAVE = 221 # rw
|
|
111
|
+
CUSTOM_PARAM_GET = 222 # ro
|
|
112
|
+
SOUND_SET = 223 # rw
|
|
113
|
+
TIMES_AFTER_CLEAN = 224 # ro
|
|
114
|
+
DEFAULT_SETTING = 225 # rw
|
|
115
|
+
DETERGENT_EMPTY = 226 # ro
|
|
116
|
+
SOFTENER_EMPTY = 227 # ro
|
|
117
|
+
LIGHT_SETTING = 229 # rw
|
|
118
|
+
DETERGENT_VOLUME = 230 # rw
|
|
119
|
+
SOFTENER_VOLUME = 231 # rw
|
|
120
|
+
APP_AUTHORIZATION = 232 # rw
|
|
121
|
+
ID_QUERY = 10000
|
|
122
|
+
F_C = 10001
|
|
123
|
+
SND_STATE = 10004
|
|
124
|
+
PRODUCT_INFO = 10005
|
|
125
|
+
PRIVACY_INFO = 10006
|
|
126
|
+
OTA_NFO = 10007
|
|
127
|
+
WASHING_LOG = 10008
|
|
128
|
+
RPC_REQ = 10101
|
|
129
|
+
RPC_RESp = 10102
|
|
130
|
+
|
|
131
|
+
|
|
90
132
|
ROBOROCK_DATA_STATUS_PROTOCOL = [
|
|
91
133
|
RoborockDataProtocol.ERROR_CODE,
|
|
92
134
|
RoborockDataProtocol.STATE,
|