python-roborock 2.25.1__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.1 → python_roborock-2.26.0}/PKG-INFO +1 -1
- {python_roborock-2.25.1 → python_roborock-2.26.0}/pyproject.toml +1 -1
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/code_mappings.py +30 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/const.py +1 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/containers.py +12 -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.1 → python_roborock-2.26.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +7 -18
- python_roborock-2.25.1/roborock/version_a01_apis/roborock_client_a01.py +0 -153
- {python_roborock-2.25.1 → python_roborock-2.26.0}/LICENSE +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/README.md +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/__init__.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/api.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/cli.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/cloud_api.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/command_cache.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/devices/README.md +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/devices/device.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/devices/device_manager.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/devices/local_channel.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/devices/mqtt_channel.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/exceptions.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/local_api.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/protocol.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/py.typed +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/roborock_future.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/roborock_message.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/util.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/version_1_apis/__init__.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/version_a01_apis/__init__.py +0 -0
- {python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/web_api.py +0 -0
|
@@ -300,6 +300,17 @@ 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
|
+
|
|
303
314
|
class RoborockFanSpeedSaros10R(RoborockFanPowerCode):
|
|
304
315
|
off = 105
|
|
305
316
|
quiet = 101
|
|
@@ -379,6 +390,15 @@ class RoborockMopModeQRevoMaxV(RoborockMopModeCode):
|
|
|
379
390
|
smart_mode = 306
|
|
380
391
|
|
|
381
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
|
+
|
|
382
402
|
class RoborockMopIntensityCode(RoborockEnum):
|
|
383
403
|
"""Describes the mop intensity of the vacuum cleaner."""
|
|
384
404
|
|
|
@@ -458,6 +478,16 @@ class RoborockMopIntensityS8MaxVUltra(RoborockMopIntensityCode):
|
|
|
458
478
|
custom_water_flow = 207
|
|
459
479
|
|
|
460
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
|
+
|
|
461
491
|
class RoborockMopIntensitySaros10R(RoborockMopIntensityCode):
|
|
462
492
|
off = 200
|
|
463
493
|
low = 201
|
|
@@ -51,6 +51,7 @@ ROBOROCK_QREVO_S = "roborock.vacuum.a104"
|
|
|
51
51
|
ROBOROCK_QREVO_PRO = "roborock.vacuum.a101"
|
|
52
52
|
ROBOROCK_QREVO_MAXV = "roborock.vacuum.a87"
|
|
53
53
|
ROBOROCK_SAROS_10R = "roborock.vacuum.a144"
|
|
54
|
+
ROBOROCK_SAROS_10 = "roborock.vacuum.a147"
|
|
54
55
|
|
|
55
56
|
ROBOROCK_DYAD_AIR = "roborock.wetdryvac.a107"
|
|
56
57
|
ROBOROCK_DYAD_PRO_COMBO = "roborock.wetdryvac.a83"
|
|
@@ -28,6 +28,7 @@ from .code_mappings import (
|
|
|
28
28
|
RoborockFanSpeedS7,
|
|
29
29
|
RoborockFanSpeedS7MaxV,
|
|
30
30
|
RoborockFanSpeedS8MaxVUltra,
|
|
31
|
+
RoborockFanSpeedSaros10,
|
|
31
32
|
RoborockFanSpeedSaros10R,
|
|
32
33
|
RoborockFinishReason,
|
|
33
34
|
RoborockInCleaning,
|
|
@@ -41,6 +42,7 @@ from .code_mappings import (
|
|
|
41
42
|
RoborockMopIntensityS6MaxV,
|
|
42
43
|
RoborockMopIntensityS7,
|
|
43
44
|
RoborockMopIntensityS8MaxVUltra,
|
|
45
|
+
RoborockMopIntensitySaros10,
|
|
44
46
|
RoborockMopIntensitySaros10R,
|
|
45
47
|
RoborockMopModeCode,
|
|
46
48
|
RoborockMopModeQRevoCurv,
|
|
@@ -49,6 +51,7 @@ from .code_mappings import (
|
|
|
49
51
|
RoborockMopModeS7,
|
|
50
52
|
RoborockMopModeS8MaxVUltra,
|
|
51
53
|
RoborockMopModeS8ProUltra,
|
|
54
|
+
RoborockMopModeSaros10,
|
|
52
55
|
RoborockMopModeSaros10R,
|
|
53
56
|
RoborockStartType,
|
|
54
57
|
RoborockStateCode,
|
|
@@ -77,6 +80,7 @@ from .const import (
|
|
|
77
80
|
ROBOROCK_S8,
|
|
78
81
|
ROBOROCK_S8_MAXV_ULTRA,
|
|
79
82
|
ROBOROCK_S8_PRO_ULTRA,
|
|
83
|
+
ROBOROCK_SAROS_10,
|
|
80
84
|
ROBOROCK_SAROS_10R,
|
|
81
85
|
SENSOR_DIRTY_REPLACE_TIME,
|
|
82
86
|
SIDE_BRUSH_REPLACE_TIME,
|
|
@@ -689,6 +693,13 @@ class Saros10RStatus(Status):
|
|
|
689
693
|
mop_mode: RoborockMopModeSaros10R | None = None
|
|
690
694
|
|
|
691
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
|
+
|
|
692
703
|
ModelStatus: dict[str, type[Status]] = {
|
|
693
704
|
ROBOROCK_S4_MAX: S4MaxStatus,
|
|
694
705
|
ROBOROCK_S5_MAX: S5MaxStatus,
|
|
@@ -713,6 +724,7 @@ ModelStatus: dict[str, type[Status]] = {
|
|
|
713
724
|
ROBOROCK_QREVO_PRO: P10Status,
|
|
714
725
|
ROBOROCK_S8_MAXV_ULTRA: S8MaxvUltraStatus,
|
|
715
726
|
ROBOROCK_SAROS_10R: Saros10RStatus,
|
|
727
|
+
ROBOROCK_SAROS_10: Saros10Status,
|
|
716
728
|
}
|
|
717
729
|
|
|
718
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.1 → python_roborock-2.26.0}/roborock/version_1_apis/roborock_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.25.1 → python_roborock-2.26.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|