Skip to content

Commit e825ff5

Browse files
authored
feat: Add zeo support and fix some a01 weirdness (#200)
* major: add A01 * chore: add init * chore: fix commitlint? * chore: fix commitlint * chore: fix commitlint * chore: change refactor to be major tag * refactor: add A01 * feat: add a01 BREAKING CHANGE: You must now specify what version api you want to use with clients. * feat: add initial zeo support * fix: fix A01 support * fix: allow messages to fail * fix: lint * feat: add more zeo things
1 parent 94cc275 commit e825ff5

File tree

6 files changed

+274
-52
lines changed

6 files changed

+274
-52
lines changed

roborock/api.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@
1212

1313
from .containers import (
1414
DeviceData,
15-
ModelStatus,
16-
S7MaxVStatus,
17-
Status,
1815
)
1916
from .exceptions import (
2017
RoborockTimeout,
@@ -48,16 +45,10 @@ def __init__(self, endpoint: str, device_info: DeviceData, queue_timeout: int =
4845
self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER)
4946
self.is_available: bool = True
5047
self.queue_timeout = queue_timeout
51-
self._status_type: type[Status] = ModelStatus.get(self.device_info.model, S7MaxVStatus)
5248

5349
def __del__(self) -> None:
5450
self.release()
5551

56-
@property
57-
def status_type(self) -> type[Status]:
58-
"""Gets the status type for this device"""
59-
return self._status_type
60-
6152
def release(self):
6253
self.sync_disconnect()
6354

roborock/code_mappings.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,3 +450,138 @@ class DyadError(RoborockEnum):
450450
dirty_charging_contacts = 10007 # Disconnection between the device and dock. Wipe charging contacts.
451451
low_battery = 20017 # Low battery level. Charge before starting self-cleaning.
452452
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

roborock/protocol.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
_LOGGER = logging.getLogger(__name__)
3737
SALT = b"TXdfu$jyZ#TZHsg4"
3838
A01_HASH = "726f626f726f636b2d67a6d6da"
39-
A01_AES_DECIPHER = "ELSYN0wTI4AUm7C4"
4039
BROADCAST_TOKEN = b"qWKYcdQWrbm9hPqe"
4140
AP_CONFIG = 1
4241
SOCK_DISCOVERY = 2
@@ -208,7 +207,7 @@ def _encode(self, obj, context, _):
208207
"""
209208
if context.version == b"A01":
210209
iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24]
211-
decipher = AES.new(bytes(A01_AES_DECIPHER, "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
210+
decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
212211
f = decipher.encrypt(obj)
213212
return f
214213
token = self.token_func(context)
@@ -219,7 +218,7 @@ def _decode(self, obj, context, _):
219218
"""Decrypts the given payload with the token stored in the context."""
220219
if context.version == b"A01":
221220
iv = md5hex(format(context.random, "08x") + A01_HASH)[8:24]
222-
decipher = AES.new(bytes(A01_AES_DECIPHER, "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
221+
decipher = AES.new(bytes(context.search("local_key"), "utf-8"), AES.MODE_CBC, bytes(iv, "utf-8"))
223222
f = decipher.decrypt(obj)
224223
return f
225224
token = self.token_func(context)

roborock/roborock_message.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,48 @@ class RoborockDyadDataProtocol(RoborockEnum):
8787
RPC_RESPONSE = 10102
8888

8989

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+
90132
ROBOROCK_DATA_STATUS_PROTOCOL = [
91133
RoborockDataProtocol.ERROR_CODE,
92134
RoborockDataProtocol.STATE,

roborock/version_a01_apis/roborock_client_a01.py

Lines changed: 74 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,59 +19,92 @@
1919
DyadWarmLevel,
2020
DyadWaterLevel,
2121
RoborockDyadStateCode,
22+
ZeoDetergentType,
23+
ZeoDryingMode,
24+
ZeoError,
25+
ZeoMode,
26+
ZeoProgram,
27+
ZeoRinse,
28+
ZeoSoftenerType,
29+
ZeoSpin,
30+
ZeoState,
31+
ZeoTemperature,
2232
)
23-
from roborock.containers import DyadProductInfo, DyadSndState
33+
from roborock.containers import DyadProductInfo, DyadSndState, RoborockCategory
2434
from roborock.roborock_message import (
2535
RoborockDyadDataProtocol,
2636
RoborockMessage,
2737
RoborockMessageProtocol,
38+
RoborockZeoProtocol,
2839
)
2940

3041

3142
@dataclasses.dataclass
32-
class DyadProtocolCacheEntry:
43+
class A01ProtocolCacheEntry:
3344
post_process_fn: Callable
3445
value: typing.Any | None = None
3546

3647

3748
# Right now this cache is not active, it was too much complexity for the initial addition of dyad.
3849
protocol_entries = {
39-
RoborockDyadDataProtocol.STATUS: DyadProtocolCacheEntry(lambda val: RoborockDyadStateCode(val).name),
40-
RoborockDyadDataProtocol.SELF_CLEAN_MODE: DyadProtocolCacheEntry(lambda val: DyadSelfCleanMode(val).name),
41-
RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: DyadProtocolCacheEntry(lambda val: DyadSelfCleanLevel(val).name),
42-
RoborockDyadDataProtocol.WARM_LEVEL: DyadProtocolCacheEntry(lambda val: DyadWarmLevel(val).name),
43-
RoborockDyadDataProtocol.CLEAN_MODE: DyadProtocolCacheEntry(lambda val: DyadCleanMode(val).name),
44-
RoborockDyadDataProtocol.SUCTION: DyadProtocolCacheEntry(lambda val: DyadSuction(val).name),
45-
RoborockDyadDataProtocol.WATER_LEVEL: DyadProtocolCacheEntry(lambda val: DyadWaterLevel(val).name),
46-
RoborockDyadDataProtocol.BRUSH_SPEED: DyadProtocolCacheEntry(lambda val: DyadBrushSpeed(val).name),
47-
RoborockDyadDataProtocol.POWER: DyadProtocolCacheEntry(lambda val: int(val)),
48-
RoborockDyadDataProtocol.AUTO_DRY: DyadProtocolCacheEntry(lambda val: bool(val)),
49-
RoborockDyadDataProtocol.MESH_LEFT: DyadProtocolCacheEntry(lambda val: int(360000 - val * 60)),
50-
RoborockDyadDataProtocol.BRUSH_LEFT: DyadProtocolCacheEntry(lambda val: int(360000 - val * 60)),
51-
RoborockDyadDataProtocol.ERROR: DyadProtocolCacheEntry(lambda val: DyadError(val).name),
52-
RoborockDyadDataProtocol.VOLUME_SET: DyadProtocolCacheEntry(lambda val: int(val)),
53-
RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: DyadProtocolCacheEntry(lambda val: bool(val)),
54-
RoborockDyadDataProtocol.AUTO_DRY_MODE: DyadProtocolCacheEntry(lambda val: bool(val)),
55-
RoborockDyadDataProtocol.SILENT_DRY_DURATION: DyadProtocolCacheEntry(lambda val: int(val)), # in minutes
56-
RoborockDyadDataProtocol.SILENT_MODE: DyadProtocolCacheEntry(lambda val: bool(val)),
57-
RoborockDyadDataProtocol.SILENT_MODE_START_TIME: DyadProtocolCacheEntry(
50+
RoborockDyadDataProtocol.STATUS: A01ProtocolCacheEntry(lambda val: RoborockDyadStateCode(val).name),
51+
RoborockDyadDataProtocol.SELF_CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadSelfCleanMode(val).name),
52+
RoborockDyadDataProtocol.SELF_CLEAN_LEVEL: A01ProtocolCacheEntry(lambda val: DyadSelfCleanLevel(val).name),
53+
RoborockDyadDataProtocol.WARM_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWarmLevel(val).name),
54+
RoborockDyadDataProtocol.CLEAN_MODE: A01ProtocolCacheEntry(lambda val: DyadCleanMode(val).name),
55+
RoborockDyadDataProtocol.SUCTION: A01ProtocolCacheEntry(lambda val: DyadSuction(val).name),
56+
RoborockDyadDataProtocol.WATER_LEVEL: A01ProtocolCacheEntry(lambda val: DyadWaterLevel(val).name),
57+
RoborockDyadDataProtocol.BRUSH_SPEED: A01ProtocolCacheEntry(lambda val: DyadBrushSpeed(val).name),
58+
RoborockDyadDataProtocol.POWER: A01ProtocolCacheEntry(lambda val: int(val)),
59+
RoborockDyadDataProtocol.AUTO_DRY: A01ProtocolCacheEntry(lambda val: bool(val)),
60+
RoborockDyadDataProtocol.MESH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)),
61+
RoborockDyadDataProtocol.BRUSH_LEFT: A01ProtocolCacheEntry(lambda val: int(360000 - val * 60)),
62+
RoborockDyadDataProtocol.ERROR: A01ProtocolCacheEntry(lambda val: DyadError(val).name),
63+
RoborockDyadDataProtocol.VOLUME_SET: A01ProtocolCacheEntry(lambda val: int(val)),
64+
RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: A01ProtocolCacheEntry(lambda val: bool(val)),
65+
RoborockDyadDataProtocol.AUTO_DRY_MODE: A01ProtocolCacheEntry(lambda val: bool(val)),
66+
RoborockDyadDataProtocol.SILENT_DRY_DURATION: A01ProtocolCacheEntry(lambda val: int(val)), # in minutes
67+
RoborockDyadDataProtocol.SILENT_MODE: A01ProtocolCacheEntry(lambda val: bool(val)),
68+
RoborockDyadDataProtocol.SILENT_MODE_START_TIME: A01ProtocolCacheEntry(
5869
lambda val: time(hour=int(val / 60), minute=val % 60)
5970
), # in minutes since 00:00
60-
RoborockDyadDataProtocol.SILENT_MODE_END_TIME: DyadProtocolCacheEntry(
71+
RoborockDyadDataProtocol.SILENT_MODE_END_TIME: A01ProtocolCacheEntry(
6172
lambda val: time(hour=int(val / 60), minute=val % 60)
6273
), # in minutes since 00:00
63-
RoborockDyadDataProtocol.RECENT_RUN_TIME: DyadProtocolCacheEntry(
74+
RoborockDyadDataProtocol.RECENT_RUN_TIME: A01ProtocolCacheEntry(
6475
lambda val: [int(v) for v in val.split(",")]
6576
), # minutes of cleaning in past few days.
66-
RoborockDyadDataProtocol.TOTAL_RUN_TIME: DyadProtocolCacheEntry(lambda val: int(val)),
67-
RoborockDyadDataProtocol.SND_STATE: DyadProtocolCacheEntry(lambda val: DyadSndState.from_dict(val)),
68-
RoborockDyadDataProtocol.PRODUCT_INFO: DyadProtocolCacheEntry(lambda val: DyadProductInfo.from_dict(val)),
77+
RoborockDyadDataProtocol.TOTAL_RUN_TIME: A01ProtocolCacheEntry(lambda val: int(val)),
78+
RoborockDyadDataProtocol.SND_STATE: A01ProtocolCacheEntry(lambda val: DyadSndState.from_dict(val)),
79+
RoborockDyadDataProtocol.PRODUCT_INFO: A01ProtocolCacheEntry(lambda val: DyadProductInfo.from_dict(val)),
80+
}
81+
82+
zeo_data_protocol_entries = {
83+
# ro
84+
RoborockZeoProtocol.STATE: A01ProtocolCacheEntry(lambda val: ZeoState(val).name),
85+
RoborockZeoProtocol.COUNTDOWN: A01ProtocolCacheEntry(lambda val: int(val)),
86+
RoborockZeoProtocol.WASHING_LEFT: A01ProtocolCacheEntry(lambda val: int(val)),
87+
RoborockZeoProtocol.ERROR: A01ProtocolCacheEntry(lambda val: ZeoError(val).name),
88+
RoborockZeoProtocol.TIMES_AFTER_CLEAN: A01ProtocolCacheEntry(lambda val: int(val)),
89+
RoborockZeoProtocol.DETERGENT_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)),
90+
RoborockZeoProtocol.SOFTENER_EMPTY: A01ProtocolCacheEntry(lambda val: bool(val)),
91+
# rw
92+
RoborockZeoProtocol.MODE: A01ProtocolCacheEntry(lambda val: ZeoMode(val).name),
93+
RoborockZeoProtocol.PROGRAM: A01ProtocolCacheEntry(lambda val: ZeoProgram(val).name),
94+
RoborockZeoProtocol.TEMP: A01ProtocolCacheEntry(lambda val: ZeoTemperature(val).name),
95+
RoborockZeoProtocol.RINSE_TIMES: A01ProtocolCacheEntry(lambda val: ZeoRinse(val).name),
96+
RoborockZeoProtocol.SPIN_LEVEL: A01ProtocolCacheEntry(lambda val: ZeoSpin(val).name),
97+
RoborockZeoProtocol.DRYING_MODE: A01ProtocolCacheEntry(lambda val: ZeoDryingMode(val).name),
98+
RoborockZeoProtocol.DETERGENT_TYPE: A01ProtocolCacheEntry(lambda val: ZeoDetergentType(val).name),
99+
RoborockZeoProtocol.SOFTENER_TYPE: A01ProtocolCacheEntry(lambda val: ZeoSoftenerType(val).name),
100+
RoborockZeoProtocol.SOUND_SET: A01ProtocolCacheEntry(lambda val: bool(val)),
69101
}
70102

71103

72104
class RoborockClientA01(RoborockClient):
73-
def __init__(self, endpoint: str, device_info: DeviceData):
74-
super().__init__(endpoint, device_info)
105+
def __init__(self, endpoint: str, device_info: DeviceData, category: RoborockCategory, queue_timeout: int = 4):
106+
super().__init__(endpoint, device_info, queue_timeout)
107+
self.category = category
75108

76109
def on_message_received(self, messages: list[RoborockMessage]) -> None:
77110
for message in messages:
@@ -87,14 +120,23 @@ def on_message_received(self, messages: list[RoborockMessage]) -> None:
87120
continue
88121
payload_json = json.loads(payload.decode())
89122
for data_point_number, data_point in payload_json.get("dps").items():
90-
data_point_protocol = RoborockDyadDataProtocol(int(data_point_number))
91-
if data_point_protocol in protocol_entries:
123+
data_point_protocol: RoborockDyadDataProtocol | RoborockZeoProtocol
124+
entries: dict
125+
if self.category == RoborockCategory.WET_DRY_VAC:
126+
data_point_protocol = RoborockDyadDataProtocol(int(data_point_number))
127+
entries = protocol_entries
128+
elif self.category == RoborockCategory.WASHING_MACHINE:
129+
data_point_protocol = RoborockZeoProtocol(int(data_point_number))
130+
entries = zeo_data_protocol_entries
131+
else:
132+
continue
133+
if data_point_protocol in entries:
92134
# Auto convert into data struct we want.
93-
converted_response = protocol_entries[data_point_protocol].post_process_fn(data_point)
135+
converted_response = entries[data_point_protocol].post_process_fn(data_point)
94136
queue = self._waiting_queue.get(int(data_point_number))
95137
if queue and queue.protocol == protocol:
96138
queue.resolve((converted_response, None))
97139

98-
async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol]):
140+
async def update_values(self, dyad_data_protocols: list[RoborockDyadDataProtocol | RoborockZeoProtocol]):
99141
"""This should handle updating for each given protocol."""
100142
raise NotImplementedError

0 commit comments

Comments
 (0)