python-roborock 2.25.0__tar.gz → 2.26.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-2.25.0 → python_roborock-2.26.0}/PKG-INFO +1 -1
- {python_roborock-2.25.0 → python_roborock-2.26.0}/pyproject.toml +1 -1
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/code_mappings.py +61 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/const.py +2 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/containers.py +24 -0
- python_roborock-2.26.0/roborock/protocols/a01_protocol.py +54 -0
- python_roborock-2.26.0/roborock/version_a01_apis/roborock_client_a01.py +159 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +7 -18
- python_roborock-2.25.0/roborock/version_a01_apis/roborock_client_a01.py +0 -153
- {python_roborock-2.25.0 → python_roborock-2.26.0}/LICENSE +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/README.md +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/__init__.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/api.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/cli.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/cloud_api.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/command_cache.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/devices/README.md +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/devices/device.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/devices/device_manager.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/devices/local_channel.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/devices/mqtt_channel.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/exceptions.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/local_api.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/protocol.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/py.typed +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/roborock_future.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/roborock_message.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/util.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_1_apis/__init__.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_a01_apis/__init__.py +0 -0
- {python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/web_api.py +0 -0
|
@@ -300,6 +300,28 @@ class RoborockFanSpeedS8MaxVUltra(RoborockFanPowerCode):
|
|
|
300
300
|
smart_mode = 110
|
|
301
301
|
|
|
302
302
|
|
|
303
|
+
class RoborockFanSpeedSaros10(RoborockFanPowerCode):
|
|
304
|
+
off = 105
|
|
305
|
+
quiet = 101
|
|
306
|
+
balanced = 102
|
|
307
|
+
turbo = 103
|
|
308
|
+
max = 104
|
|
309
|
+
custom = 106
|
|
310
|
+
max_plus = 108
|
|
311
|
+
smart_mode = 110
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class RoborockFanSpeedSaros10R(RoborockFanPowerCode):
|
|
315
|
+
off = 105
|
|
316
|
+
quiet = 101
|
|
317
|
+
balanced = 102
|
|
318
|
+
turbo = 103
|
|
319
|
+
max = 104
|
|
320
|
+
custom = 106
|
|
321
|
+
max_plus = 108
|
|
322
|
+
smart_mode = 110
|
|
323
|
+
|
|
324
|
+
|
|
303
325
|
class RoborockMopModeCode(RoborockEnum):
|
|
304
326
|
"""Describes the mop mode of the vacuum cleaner."""
|
|
305
327
|
|
|
@@ -341,6 +363,15 @@ class RoborockMopModeS8MaxVUltra(RoborockMopModeCode):
|
|
|
341
363
|
smart_mode = 306
|
|
342
364
|
|
|
343
365
|
|
|
366
|
+
class RoborockMopModeSaros10R(RoborockMopModeCode):
|
|
367
|
+
standard = 300
|
|
368
|
+
deep = 301
|
|
369
|
+
custom = 302
|
|
370
|
+
deep_plus = 303
|
|
371
|
+
fast = 304
|
|
372
|
+
smart_mode = 306
|
|
373
|
+
|
|
374
|
+
|
|
344
375
|
class RoborockMopModeQRevoMaster(RoborockMopModeCode):
|
|
345
376
|
standard = 300
|
|
346
377
|
deep = 301
|
|
@@ -359,6 +390,15 @@ class RoborockMopModeQRevoMaxV(RoborockMopModeCode):
|
|
|
359
390
|
smart_mode = 306
|
|
360
391
|
|
|
361
392
|
|
|
393
|
+
class RoborockMopModeSaros10(RoborockMopModeCode):
|
|
394
|
+
standard = 300
|
|
395
|
+
deep = 301
|
|
396
|
+
custom = 302
|
|
397
|
+
deep_plus = 303
|
|
398
|
+
fast = 304
|
|
399
|
+
smart_mode = 306
|
|
400
|
+
|
|
401
|
+
|
|
362
402
|
class RoborockMopIntensityCode(RoborockEnum):
|
|
363
403
|
"""Describes the mop intensity of the vacuum cleaner."""
|
|
364
404
|
|
|
@@ -438,6 +478,27 @@ class RoborockMopIntensityS8MaxVUltra(RoborockMopIntensityCode):
|
|
|
438
478
|
custom_water_flow = 207
|
|
439
479
|
|
|
440
480
|
|
|
481
|
+
class RoborockMopIntensitySaros10(RoborockMopIntensityCode):
|
|
482
|
+
off = 200
|
|
483
|
+
mild = 201
|
|
484
|
+
standard = 202
|
|
485
|
+
intense = 203
|
|
486
|
+
extreme = 208
|
|
487
|
+
custom = 204
|
|
488
|
+
smart_mode = 209
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
class RoborockMopIntensitySaros10R(RoborockMopIntensityCode):
|
|
492
|
+
off = 200
|
|
493
|
+
low = 201
|
|
494
|
+
medium = 202
|
|
495
|
+
high = 203
|
|
496
|
+
custom = 204
|
|
497
|
+
extreme = 250
|
|
498
|
+
vac_followed_by_mop = 235
|
|
499
|
+
smart_mode = 209
|
|
500
|
+
|
|
501
|
+
|
|
441
502
|
class RoborockMopIntensityS5Max(RoborockMopIntensityCode):
|
|
442
503
|
"""Describes the mop intensity of the vacuum cleaner."""
|
|
443
504
|
|
|
@@ -50,6 +50,8 @@ ROBOROCK_S8_MAXV_ULTRA = "roborock.vacuum.a97"
|
|
|
50
50
|
ROBOROCK_QREVO_S = "roborock.vacuum.a104"
|
|
51
51
|
ROBOROCK_QREVO_PRO = "roborock.vacuum.a101"
|
|
52
52
|
ROBOROCK_QREVO_MAXV = "roborock.vacuum.a87"
|
|
53
|
+
ROBOROCK_SAROS_10R = "roborock.vacuum.a144"
|
|
54
|
+
ROBOROCK_SAROS_10 = "roborock.vacuum.a147"
|
|
53
55
|
|
|
54
56
|
ROBOROCK_DYAD_AIR = "roborock.wetdryvac.a107"
|
|
55
57
|
ROBOROCK_DYAD_PRO_COMBO = "roborock.wetdryvac.a83"
|
|
@@ -28,6 +28,8 @@ from .code_mappings import (
|
|
|
28
28
|
RoborockFanSpeedS7,
|
|
29
29
|
RoborockFanSpeedS7MaxV,
|
|
30
30
|
RoborockFanSpeedS8MaxVUltra,
|
|
31
|
+
RoborockFanSpeedSaros10,
|
|
32
|
+
RoborockFanSpeedSaros10R,
|
|
31
33
|
RoborockFinishReason,
|
|
32
34
|
RoborockInCleaning,
|
|
33
35
|
RoborockMopIntensityCode,
|
|
@@ -40,6 +42,8 @@ from .code_mappings import (
|
|
|
40
42
|
RoborockMopIntensityS6MaxV,
|
|
41
43
|
RoborockMopIntensityS7,
|
|
42
44
|
RoborockMopIntensityS8MaxVUltra,
|
|
45
|
+
RoborockMopIntensitySaros10,
|
|
46
|
+
RoborockMopIntensitySaros10R,
|
|
43
47
|
RoborockMopModeCode,
|
|
44
48
|
RoborockMopModeQRevoCurv,
|
|
45
49
|
RoborockMopModeQRevoMaster,
|
|
@@ -47,6 +51,8 @@ from .code_mappings import (
|
|
|
47
51
|
RoborockMopModeS7,
|
|
48
52
|
RoborockMopModeS8MaxVUltra,
|
|
49
53
|
RoborockMopModeS8ProUltra,
|
|
54
|
+
RoborockMopModeSaros10,
|
|
55
|
+
RoborockMopModeSaros10R,
|
|
50
56
|
RoborockStartType,
|
|
51
57
|
RoborockStateCode,
|
|
52
58
|
)
|
|
@@ -74,6 +80,8 @@ from .const import (
|
|
|
74
80
|
ROBOROCK_S8,
|
|
75
81
|
ROBOROCK_S8_MAXV_ULTRA,
|
|
76
82
|
ROBOROCK_S8_PRO_ULTRA,
|
|
83
|
+
ROBOROCK_SAROS_10,
|
|
84
|
+
ROBOROCK_SAROS_10R,
|
|
77
85
|
SENSOR_DIRTY_REPLACE_TIME,
|
|
78
86
|
SIDE_BRUSH_REPLACE_TIME,
|
|
79
87
|
STRAINER_REPLACE_TIME,
|
|
@@ -678,6 +686,20 @@ class S8MaxvUltraStatus(Status):
|
|
|
678
686
|
mop_mode: RoborockMopModeS8MaxVUltra | None = None
|
|
679
687
|
|
|
680
688
|
|
|
689
|
+
@dataclass
|
|
690
|
+
class Saros10RStatus(Status):
|
|
691
|
+
fan_power: RoborockFanSpeedSaros10R | None = None
|
|
692
|
+
water_box_mode: RoborockMopIntensitySaros10R | None = None
|
|
693
|
+
mop_mode: RoborockMopModeSaros10R | None = None
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
@dataclass
|
|
697
|
+
class Saros10Status(Status):
|
|
698
|
+
fan_power: RoborockFanSpeedSaros10 | None = None
|
|
699
|
+
water_box_mode: RoborockMopIntensitySaros10 | None = None
|
|
700
|
+
mop_mode: RoborockMopModeSaros10 | None = None
|
|
701
|
+
|
|
702
|
+
|
|
681
703
|
ModelStatus: dict[str, type[Status]] = {
|
|
682
704
|
ROBOROCK_S4_MAX: S4MaxStatus,
|
|
683
705
|
ROBOROCK_S5_MAX: S5MaxStatus,
|
|
@@ -701,6 +723,8 @@ ModelStatus: dict[str, type[Status]] = {
|
|
|
701
723
|
ROBOROCK_QREVO_MAXV: QRevoMaxVStatus,
|
|
702
724
|
ROBOROCK_QREVO_PRO: P10Status,
|
|
703
725
|
ROBOROCK_S8_MAXV_ULTRA: S8MaxvUltraStatus,
|
|
726
|
+
ROBOROCK_SAROS_10R: Saros10RStatus,
|
|
727
|
+
ROBOROCK_SAROS_10: Saros10Status,
|
|
704
728
|
}
|
|
705
729
|
|
|
706
730
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Roborock A01 Protocol encoding and decoding."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from Crypto.Cipher import AES
|
|
8
|
+
from Crypto.Util.Padding import pad, unpad
|
|
9
|
+
|
|
10
|
+
from roborock.exceptions import RoborockException
|
|
11
|
+
from roborock.roborock_message import (
|
|
12
|
+
RoborockDyadDataProtocol,
|
|
13
|
+
RoborockMessage,
|
|
14
|
+
RoborockMessageProtocol,
|
|
15
|
+
RoborockZeoProtocol,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_LOGGER = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
A01_VERSION = b"A01"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def encode_mqtt_payload(data: dict[RoborockDyadDataProtocol | RoborockZeoProtocol, Any]) -> RoborockMessage:
|
|
24
|
+
"""Encode payload for A01 commands over MQTT."""
|
|
25
|
+
dps_data = {"dps": data}
|
|
26
|
+
payload = pad(json.dumps(dps_data).encode("utf-8"), AES.block_size)
|
|
27
|
+
return RoborockMessage(
|
|
28
|
+
protocol=RoborockMessageProtocol.RPC_REQUEST,
|
|
29
|
+
version=A01_VERSION,
|
|
30
|
+
payload=payload,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def decode_rpc_response(message: RoborockMessage) -> dict[int, Any]:
|
|
35
|
+
"""Decode a V1 RPC_RESPONSE message."""
|
|
36
|
+
if not message.payload:
|
|
37
|
+
raise RoborockException("Invalid A01 message format: missing payload")
|
|
38
|
+
try:
|
|
39
|
+
unpadded = unpad(message.payload, AES.block_size)
|
|
40
|
+
except ValueError as err:
|
|
41
|
+
raise RoborockException(f"Unable to unpad A01 payload: {err}")
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
payload = json.loads(unpadded.decode())
|
|
45
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
46
|
+
raise RoborockException(f"Invalid A01 message payload: {e} for {message.payload!r}") from e
|
|
47
|
+
|
|
48
|
+
datapoints = payload.get("dps", {})
|
|
49
|
+
if not isinstance(datapoints, dict):
|
|
50
|
+
raise RoborockException(f"Invalid A01 message format: 'dps' should be a dictionary for {message.payload!r}")
|
|
51
|
+
try:
|
|
52
|
+
return {int(key): value for key, value in datapoints.items()}
|
|
53
|
+
except ValueError:
|
|
54
|
+
raise RoborockException(f"Invalid A01 message format: 'dps' key should be an integer for {message.payload!r}")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from datetime import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from roborock import DeviceData
|
|
8
|
+
from roborock.api import RoborockClient
|
|
9
|
+
from roborock.code_mappings import (
|
|
10
|
+
DyadBrushSpeed,
|
|
11
|
+
DyadCleanMode,
|
|
12
|
+
DyadError,
|
|
13
|
+
DyadSelfCleanLevel,
|
|
14
|
+
DyadSelfCleanMode,
|
|
15
|
+
DyadSuction,
|
|
16
|
+
DyadWarmLevel,
|
|
17
|
+
DyadWaterLevel,
|
|
18
|
+
RoborockDyadStateCode,
|
|
19
|
+
ZeoDetergentType,
|
|
20
|
+
ZeoDryingMode,
|
|
21
|
+
ZeoError,
|
|
22
|
+
ZeoMode,
|
|
23
|
+
ZeoProgram,
|
|
24
|
+
ZeoRinse,
|
|
25
|
+
ZeoSoftenerType,
|
|
26
|
+
ZeoSpin,
|
|
27
|
+
ZeoState,
|
|
28
|
+
ZeoTemperature,
|
|
29
|
+
)
|
|
30
|
+
from roborock.containers import DyadProductInfo, DyadSndState, RoborockCategory
|
|
31
|
+
from roborock.exceptions import RoborockException
|
|
32
|
+
from roborock.protocols.a01_protocol import decode_rpc_response
|
|
33
|
+
from roborock.roborock_message import (
|
|
34
|
+
RoborockDyadDataProtocol,
|
|
35
|
+
RoborockMessage,
|
|
36
|
+
RoborockMessageProtocol,
|
|
37
|
+
RoborockZeoProtocol,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
_LOGGER = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
DYAD_PROTOCOL_ENTRIES: dict[RoborockDyadDataProtocol, Callable] = {
|
|
44
|
+
RoborockDyadDataProtocol.STATUS: lambda val: RoborockDyadStateCode(val).name,
|
|
45
|
+
RoborockDyadDataProtocol.SELF_CLEAN_MODE: lambda val: DyadSelfCleanMode(val).name,
|
|
46
|
+
RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: lambda val: DyadSelfCleanLevel(val).name,
|
|
47
|
+
RoborockDyadDataProtocol.WARM_LEVEL: lambda val: DyadWarmLevel(val).name,
|
|
48
|
+
RoborockDyadDataProtocol.CLEAN_MODE: lambda val: DyadCleanMode(val).name,
|
|
49
|
+
RoborockDyadDataProtocol.SUCTION: lambda val: DyadSuction(val).name,
|
|
50
|
+
RoborockDyadDataProtocol.WATER_LEVEL: lambda val: DyadWaterLevel(val).name,
|
|
51
|
+
RoborockDyadDataProtocol.BRUSH_SPEED: lambda val: DyadBrushSpeed(val).name,
|
|
52
|
+
RoborockDyadDataProtocol.POWER: lambda val: int(val),
|
|
53
|
+
RoborockDyadDataProtocol.AUTO_DRY: lambda val: bool(val),
|
|
54
|
+
RoborockDyadDataProtocol.MESH_LEFT: lambda val: int(360000 - val * 60),
|
|
55
|
+
RoborockDyadDataProtocol.BRUSH_LEFT: lambda val: int(360000 - val * 60),
|
|
56
|
+
RoborockDyadDataProtocol.ERROR: lambda val: DyadError(val).name,
|
|
57
|
+
RoborockDyadDataProtocol.VOLUME_SET: lambda val: int(val),
|
|
58
|
+
RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: lambda val: bool(val),
|
|
59
|
+
RoborockDyadDataProtocol.AUTO_DRY_MODE: lambda val: bool(val),
|
|
60
|
+
RoborockDyadDataProtocol.SILENT_DRY_DURATION: lambda val: int(val), # in minutes
|
|
61
|
+
RoborockDyadDataProtocol.SILENT_MODE: lambda val: bool(val),
|
|
62
|
+
RoborockDyadDataProtocol.SILENT_MODE_START_TIME: lambda val: time(
|
|
63
|
+
hour=int(val / 60), minute=val % 60
|
|
64
|
+
), # in minutes since 00:00
|
|
65
|
+
RoborockDyadDataProtocol.SILENT_MODE_END_TIME: lambda val: time(
|
|
66
|
+
hour=int(val / 60), minute=val % 60
|
|
67
|
+
), # in minutes since 00:00
|
|
68
|
+
RoborockDyadDataProtocol.RECENT_RUN_TIME: lambda val: [
|
|
69
|
+
int(v) for v in val.split(",")
|
|
70
|
+
], # minutes of cleaning in past few days.
|
|
71
|
+
RoborockDyadDataProtocol.TOTAL_RUN_TIME: lambda val: int(val),
|
|
72
|
+
RoborockDyadDataProtocol.SND_STATE: lambda val: DyadSndState.from_dict(val),
|
|
73
|
+
RoborockDyadDataProtocol.PRODUCT_INFO: lambda val: DyadProductInfo.from_dict(val),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ZEO_PROTOCOL_ENTRIES: dict[RoborockZeoProtocol, Callable] = {
|
|
77
|
+
# ro
|
|
78
|
+
RoborockZeoProtocol.STATE: lambda val: ZeoState(val).name,
|
|
79
|
+
RoborockZeoProtocol.COUNTDOWN: lambda val: int(val),
|
|
80
|
+
RoborockZeoProtocol.WASHING_LEFT: lambda val: int(val),
|
|
81
|
+
RoborockZeoProtocol.ERROR: lambda val: ZeoError(val).name,
|
|
82
|
+
RoborockZeoProtocol.TIMES_AFTER_CLEAN: lambda val: int(val),
|
|
83
|
+
RoborockZeoProtocol.DETERGENT_EMPTY: lambda val: bool(val),
|
|
84
|
+
RoborockZeoProtocol.SOFTENER_EMPTY: lambda val: bool(val),
|
|
85
|
+
# rw
|
|
86
|
+
RoborockZeoProtocol.MODE: lambda val: ZeoMode(val).name,
|
|
87
|
+
RoborockZeoProtocol.PROGRAM: lambda val: ZeoProgram(val).name,
|
|
88
|
+
RoborockZeoProtocol.TEMP: lambda val: ZeoTemperature(val).name,
|
|
89
|
+
RoborockZeoProtocol.RINSE_TIMES: lambda val: ZeoRinse(val).name,
|
|
90
|
+
RoborockZeoProtocol.SPIN_LEVEL: lambda val: ZeoSpin(val).name,
|
|
91
|
+
RoborockZeoProtocol.DRYING_MODE: lambda val: ZeoDryingMode(val).name,
|
|
92
|
+
RoborockZeoProtocol.DETERGENT_TYPE: lambda val: ZeoDetergentType(val).name,
|
|
93
|
+
RoborockZeoProtocol.SOFTENER_TYPE: lambda val: ZeoSoftenerType(val).name,
|
|
94
|
+
RoborockZeoProtocol.SOUND_SET: lambda val: bool(val),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def convert_dyad_value(protocol: int, value: Any) -> Any:
|
|
99
|
+
"""Convert a dyad protocol value to its corresponding type."""
|
|
100
|
+
protocol_value = RoborockDyadDataProtocol(protocol)
|
|
101
|
+
if (converter := DYAD_PROTOCOL_ENTRIES.get(protocol_value)) is not None:
|
|
102
|
+
return converter(value)
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def convert_zeo_value(protocol: int, value: Any) -> Any:
|
|
107
|
+
"""Convert a zeo protocol value to its corresponding type."""
|
|
108
|
+
protocol_value = RoborockZeoProtocol(protocol)
|
|
109
|
+
if (converter := ZEO_PROTOCOL_ENTRIES.get(protocol_value)) is not None:
|
|
110
|
+
return converter(value)
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class RoborockClientA01(RoborockClient, ABC):
|
|
115
|
+
"""Roborock client base class for A01 devices."""
|
|
116
|
+
|
|
117
|
+
value_converter: Callable[[int, Any], Any] | None = None
|
|
118
|
+
|
|
119
|
+
def __init__(self, device_info: DeviceData, category: RoborockCategory):
|
|
120
|
+
"""Initialize the Roborock client."""
|
|
121
|
+
super().__init__(device_info)
|
|
122
|
+
if category == RoborockCategory.WET_DRY_VAC:
|
|
123
|
+
self.value_converter = convert_dyad_value
|
|
124
|
+
elif category == RoborockCategory.WASHING_MACHINE:
|
|
125
|
+
self.value_converter = convert_zeo_value
|
|
126
|
+
else:
|
|
127
|
+
_LOGGER.debug("Device category %s is not (yet) supported", category)
|
|
128
|
+
self.value_converter = None
|
|
129
|
+
|
|
130
|
+
def on_message_received(self, messages: list[RoborockMessage]) -> None:
|
|
131
|
+
if self.value_converter is None:
|
|
132
|
+
return
|
|
133
|
+
for message in messages:
|
|
134
|
+
protocol = message.protocol
|
|
135
|
+
if message.payload and protocol in [
|
|
136
|
+
RoborockMessageProtocol.RPC_RESPONSE,
|
|
137
|
+
RoborockMessageProtocol.GENERAL_REQUEST,
|
|
138
|
+
]:
|
|
139
|
+
try:
|
|
140
|
+
data_points = decode_rpc_response(message)
|
|
141
|
+
except RoborockException as err:
|
|
142
|
+
self._logger.debug("Failed to decode message: %s", err)
|
|
143
|
+
continue
|
|
144
|
+
for data_point_number, data_point in data_points.items():
|
|
145
|
+
self._logger.debug("received msg with dps, protocol: %s, %s", data_point_number, protocol)
|
|
146
|
+
if converted_response := self.value_converter(data_point_number, data_point):
|
|
147
|
+
queue = self._waiting_queue.get(int(data_point_number))
|
|
148
|
+
if queue and queue.protocol == protocol:
|
|
149
|
+
queue.set_result(converted_response)
|
|
150
|
+
else:
|
|
151
|
+
self._logger.debug(
|
|
152
|
+
"Received unknown data point %s for protocol %s, ignoring", data_point_number, protocol
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@abstractmethod
|
|
156
|
+
async def update_values(
|
|
157
|
+
self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]
|
|
158
|
+
) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, Any]:
|
|
159
|
+
"""This should handle updating for each given protocol."""
|
|
@@ -4,11 +4,12 @@ import logging
|
|
|
4
4
|
import typing
|
|
5
5
|
|
|
6
6
|
from Crypto.Cipher import AES
|
|
7
|
-
from Crypto.Util.Padding import
|
|
7
|
+
from Crypto.Util.Padding import unpad
|
|
8
8
|
|
|
9
9
|
from roborock.cloud_api import RoborockMqttClient
|
|
10
10
|
from roborock.containers import DeviceData, RoborockCategory, UserData
|
|
11
11
|
from roborock.exceptions import RoborockException
|
|
12
|
+
from roborock.protocols.a01_protocol import encode_mqtt_payload
|
|
12
13
|
from roborock.roborock_message import (
|
|
13
14
|
RoborockDyadDataProtocol,
|
|
14
15
|
RoborockMessage,
|
|
@@ -43,7 +44,6 @@ class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01):
|
|
|
43
44
|
response_protocol = RoborockMessageProtocol.RPC_RESPONSE
|
|
44
45
|
|
|
45
46
|
m = self._encoder(roborock_message)
|
|
46
|
-
# self._logger.debug(f"id={request_id} Requesting method {method} with {params}")
|
|
47
47
|
payload = json.loads(unpad(roborock_message.payload, AES.block_size))
|
|
48
48
|
futures = []
|
|
49
49
|
if "10000" in payload["dps"]:
|
|
@@ -56,7 +56,6 @@ class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01):
|
|
|
56
56
|
for i, dps in enumerate(json.loads(payload["dps"]["10000"])):
|
|
57
57
|
response = responses[i]
|
|
58
58
|
if isinstance(response, BaseException):
|
|
59
|
-
self._logger.warning("Timed out get req for %s after %s s", dps, self.queue_timeout)
|
|
60
59
|
dps_responses[dps] = None
|
|
61
60
|
else:
|
|
62
61
|
dps_responses[dps] = response
|
|
@@ -65,24 +64,14 @@ class RoborockMqttClientA01(RoborockMqttClient, RoborockClientA01):
|
|
|
65
64
|
async def update_values(
|
|
66
65
|
self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]
|
|
67
66
|
) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, typing.Any]:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
RoborockMessage(
|
|
71
|
-
protocol=RoborockMessageProtocol.RPC_REQUEST,
|
|
72
|
-
version=b"A01",
|
|
73
|
-
payload=pad(json.dumps(payload).encode("utf-8"), AES.block_size),
|
|
74
|
-
)
|
|
67
|
+
message = encode_mqtt_payload(
|
|
68
|
+
{RoborockDyadDataProtocol.ID_QUERY: str([int(protocol) for protocol in dyad_data_protocols])}
|
|
75
69
|
)
|
|
70
|
+
return await self.send_message(message)
|
|
76
71
|
|
|
77
72
|
async def set_value(
|
|
78
73
|
self, protocol: RoborockDyadDataProtocol | RoborockZeoProtocol, value: typing.Any
|
|
79
74
|
) -> dict[int, typing.Any]:
|
|
80
75
|
"""Set a value for a specific protocol on the A01 device."""
|
|
81
|
-
|
|
82
|
-
return await self.send_message(
|
|
83
|
-
RoborockMessage(
|
|
84
|
-
protocol=RoborockMessageProtocol.RPC_REQUEST,
|
|
85
|
-
version=b"A01",
|
|
86
|
-
payload=pad(json.dumps(payload).encode("utf-8"), AES.block_size),
|
|
87
|
-
)
|
|
88
|
-
)
|
|
76
|
+
message = encode_mqtt_payload({protocol: value})
|
|
77
|
+
return await self.send_message(message)
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import dataclasses
|
|
2
|
-
import json
|
|
3
|
-
import logging
|
|
4
|
-
import typing
|
|
5
|
-
from abc import ABC, abstractmethod
|
|
6
|
-
from collections.abc import Callable
|
|
7
|
-
from datetime import time
|
|
8
|
-
|
|
9
|
-
from Crypto.Cipher import AES
|
|
10
|
-
from Crypto.Util.Padding import unpad
|
|
11
|
-
|
|
12
|
-
from roborock import DeviceData
|
|
13
|
-
from roborock.api import RoborockClient
|
|
14
|
-
from roborock.code_mappings import (
|
|
15
|
-
DyadBrushSpeed,
|
|
16
|
-
DyadCleanMode,
|
|
17
|
-
DyadError,
|
|
18
|
-
DyadSelfCleanLevel,
|
|
19
|
-
DyadSelfCleanMode,
|
|
20
|
-
DyadSuction,
|
|
21
|
-
DyadWarmLevel,
|
|
22
|
-
DyadWaterLevel,
|
|
23
|
-
RoborockDyadStateCode,
|
|
24
|
-
ZeoDetergentType,
|
|
25
|
-
ZeoDryingMode,
|
|
26
|
-
ZeoError,
|
|
27
|
-
ZeoMode,
|
|
28
|
-
ZeoProgram,
|
|
29
|
-
ZeoRinse,
|
|
30
|
-
ZeoSoftenerType,
|
|
31
|
-
ZeoSpin,
|
|
32
|
-
ZeoState,
|
|
33
|
-
ZeoTemperature,
|
|
34
|
-
)
|
|
35
|
-
from roborock.containers import DyadProductInfo, DyadSndState, RoborockCategory
|
|
36
|
-
from roborock.roborock_message import (
|
|
37
|
-
RoborockDyadDataProtocol,
|
|
38
|
-
RoborockMessage,
|
|
39
|
-
RoborockMessageProtocol,
|
|
40
|
-
RoborockZeoProtocol,
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
_LOGGER = logging.getLogger(__name__)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@dataclasses.dataclass
|
|
47
|
-
class A01ProtocolCacheEntry:
|
|
48
|
-
post_process_fn: Callable
|
|
49
|
-
value: typing.Any | None = None
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# Right now this cache is not active, it was too much complexity for the initial addition of dyad.
|
|
53
|
-
protocol_entries = {
|
|
54
|
-
RoborockDyadDataProtocol.STATUS: A01ProtocolCacheEntry(lambda val: RoborockDyadStateCode(val).name),
|
|
55
|
-
RoborockDyadDataProtocol.SELF_CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadSelfCleanMode(val).name),
|
|
56
|
-
RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: A01ProtocolCacheEntry(lambda val: DyadSelfCleanLevel(val).name),
|
|
57
|
-
RoborockDyadDataProtocol.WARM_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWarmLevel(val).name),
|
|
58
|
-
RoborockDyadDataProtocol.CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadCleanMode(val).name),
|
|
59
|
-
RoborockDyadDataProtocol.SUCTION: A01ProtocolCacheEntry(lambda val: DyadSuction(val).name),
|
|
60
|
-
RoborockDyadDataProtocol.WATER_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWaterLevel(val).name),
|
|
61
|
-
RoborockDyadDataProtocol.BRUSH_SPEED: A01ProtocolCacheEntry(lambda val: DyadBrushSpeed(val).name),
|
|
62
|
-
RoborockDyadDataProtocol.POWER: A01ProtocolCacheEntry(lambda val: int(val)),
|
|
63
|
-
RoborockDyadDataProtocol.AUTO_DRY: A01ProtocolCacheEntry(lambda val: bool(val)),
|
|
64
|
-
RoborockDyadDataProtocol.MESH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)),
|
|
65
|
-
RoborockDyadDataProtocol.BRUSH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)),
|
|
66
|
-
RoborockDyadDataProtocol.ERROR: A01ProtocolCacheEntry(lambda val: DyadError(val).name),
|
|
67
|
-
RoborockDyadDataProtocol.VOLUME_SET: A01ProtocolCacheEntry(lambda val: int(val)),
|
|
68
|
-
RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: A01ProtocolCacheEntry(lambda val: bool(val)),
|
|
69
|
-
RoborockDyadDataProtocol.AUTO_DRY_MODE: A01ProtocolCacheEntry(lambda val: bool(val)),
|
|
70
|
-
RoborockDyadDataProtocol.SILENT_DRY_DURATION: A01ProtocolCacheEntry(lambda val: int(val)), # in minutes
|
|
71
|
-
RoborockDyadDataProtocol.SILENT_MODE: A01ProtocolCacheEntry(lambda val: bool(val)),
|
|
72
|
-
RoborockDyadDataProtocol.SILENT_MODE_START_TIME: A01ProtocolCacheEntry(
|
|
73
|
-
lambda val: time(hour=int(val / 60), minute=val % 60)
|
|
74
|
-
), # in minutes since 00:00
|
|
75
|
-
RoborockDyadDataProtocol.SILENT_MODE_END_TIME: A01ProtocolCacheEntry(
|
|
76
|
-
lambda val: time(hour=int(val / 60), minute=val % 60)
|
|
77
|
-
), # in minutes since 00:00
|
|
78
|
-
RoborockDyadDataProtocol.RECENT_RUN_TIME: A01ProtocolCacheEntry(
|
|
79
|
-
lambda val: [int(v) for v in val.split(",")]
|
|
80
|
-
), # minutes of cleaning in past few days.
|
|
81
|
-
RoborockDyadDataProtocol.TOTAL_RUN_TIME: A01ProtocolCacheEntry(lambda val: int(val)),
|
|
82
|
-
RoborockDyadDataProtocol.SND_STATE: A01ProtocolCacheEntry(lambda val: DyadSndState.from_dict(val)),
|
|
83
|
-
RoborockDyadDataProtocol.PRODUCT_INFO: A01ProtocolCacheEntry(lambda val: DyadProductInfo.from_dict(val)),
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
zeo_data_protocol_entries = {
|
|
87
|
-
# ro
|
|
88
|
-
RoborockZeoProtocol.STATE: A01ProtocolCacheEntry(lambda val: ZeoState(val).name),
|
|
89
|
-
RoborockZeoProtocol.COUNTDOWN: A01ProtocolCacheEntry(lambda val: int(val)),
|
|
90
|
-
RoborockZeoProtocol.WASHING_LEFT: A01ProtocolCacheEntry(lambda val: int(val)),
|
|
91
|
-
RoborockZeoProtocol.ERROR: A01ProtocolCacheEntry(lambda val: ZeoError(val).name),
|
|
92
|
-
RoborockZeoProtocol.TIMES_AFTER_CLEAN: A01ProtocolCacheEntry(lambda val: int(val)),
|
|
93
|
-
RoborockZeoProtocol.DETERGENT_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)),
|
|
94
|
-
RoborockZeoProtocol.SOFTENER_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)),
|
|
95
|
-
# rw
|
|
96
|
-
RoborockZeoProtocol.MODE: A01ProtocolCacheEntry(lambda val: ZeoMode(val).name),
|
|
97
|
-
RoborockZeoProtocol.PROGRAM: A01ProtocolCacheEntry(lambda val: ZeoProgram(val).name),
|
|
98
|
-
RoborockZeoProtocol.TEMP: A01ProtocolCacheEntry(lambda val: ZeoTemperature(val).name),
|
|
99
|
-
RoborockZeoProtocol.RINSE_TIMES: A01ProtocolCacheEntry(lambda val: ZeoRinse(val).name),
|
|
100
|
-
RoborockZeoProtocol.SPIN_LEVEL: A01ProtocolCacheEntry(lambda val: ZeoSpin(val).name),
|
|
101
|
-
RoborockZeoProtocol.DRYING_MODE: A01ProtocolCacheEntry(lambda val: ZeoDryingMode(val).name),
|
|
102
|
-
RoborockZeoProtocol.DETERGENT_TYPE: A01ProtocolCacheEntry(lambda val: ZeoDetergentType(val).name),
|
|
103
|
-
RoborockZeoProtocol.SOFTENER_TYPE: A01ProtocolCacheEntry(lambda val: ZeoSoftenerType(val).name),
|
|
104
|
-
RoborockZeoProtocol.SOUND_SET: A01ProtocolCacheEntry(lambda val: bool(val)),
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
class RoborockClientA01(RoborockClient, ABC):
|
|
109
|
-
"""Roborock client base class for A01 devices."""
|
|
110
|
-
|
|
111
|
-
def __init__(self, device_info: DeviceData, category: RoborockCategory):
|
|
112
|
-
"""Initialize the Roborock client."""
|
|
113
|
-
super().__init__(device_info)
|
|
114
|
-
self.category = category
|
|
115
|
-
|
|
116
|
-
def on_message_received(self, messages: list[RoborockMessage]) -> None:
|
|
117
|
-
for message in messages:
|
|
118
|
-
protocol = message.protocol
|
|
119
|
-
if message.payload and protocol in [
|
|
120
|
-
RoborockMessageProtocol.RPC_RESPONSE,
|
|
121
|
-
RoborockMessageProtocol.GENERAL_REQUEST,
|
|
122
|
-
]:
|
|
123
|
-
payload = message.payload
|
|
124
|
-
try:
|
|
125
|
-
payload = unpad(payload, AES.block_size)
|
|
126
|
-
except Exception as err:
|
|
127
|
-
self._logger.debug("Failed to unpad payload: %s", err)
|
|
128
|
-
continue
|
|
129
|
-
payload_json = json.loads(payload.decode())
|
|
130
|
-
for data_point_number, data_point in payload_json.get("dps").items():
|
|
131
|
-
data_point_protocol: RoborockDyadDataProtocol | RoborockZeoProtocol
|
|
132
|
-
self._logger.debug("received msg with dps, protocol: %s, %s", data_point_number, protocol)
|
|
133
|
-
entries: dict
|
|
134
|
-
if self.category == RoborockCategory.WET_DRY_VAC:
|
|
135
|
-
data_point_protocol = RoborockDyadDataProtocol(int(data_point_number))
|
|
136
|
-
entries = protocol_entries
|
|
137
|
-
elif self.category == RoborockCategory.WASHING_MACHINE:
|
|
138
|
-
data_point_protocol = RoborockZeoProtocol(int(data_point_number))
|
|
139
|
-
entries = zeo_data_protocol_entries
|
|
140
|
-
else:
|
|
141
|
-
continue
|
|
142
|
-
if data_point_protocol in entries:
|
|
143
|
-
# Auto convert into data struct we want.
|
|
144
|
-
converted_response = entries[data_point_protocol].post_process_fn(data_point)
|
|
145
|
-
queue = self._waiting_queue.get(int(data_point_number))
|
|
146
|
-
if queue and queue.protocol == protocol:
|
|
147
|
-
queue.set_result(converted_response)
|
|
148
|
-
|
|
149
|
-
@abstractmethod
|
|
150
|
-
async def update_values(
|
|
151
|
-
self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]
|
|
152
|
-
) -> dict[RoborockDyadDataProtocol | RoborockZeoProtocol, typing.Any]:
|
|
153
|
-
"""This should handle updating for each given protocol."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_1_apis/roborock_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.25.0 → python_roborock-2.26.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|