python-roborock 2.27.0__tar.gz → 2.28.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.27.0 → python_roborock-2.28.0}/PKG-INFO +1 -1
- {python_roborock-2.27.0 → python_roborock-2.28.0}/pyproject.toml +1 -1
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/code_mappings.py +63 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/containers.py +8 -138
- python_roborock-2.28.0/roborock/device_features.py +365 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/devices/device.py +1 -1
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/devices/local_channel.py +6 -1
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/devices/mqtt_channel.py +1 -1
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/devices/v1_channel.py +18 -59
- python_roborock-2.28.0/roborock/devices/v1_rpc_channel.py +148 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/LICENSE +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/README.md +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/__init__.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/api.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/cli.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/cloud_api.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/command_cache.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/const.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/devices/README.md +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/devices/device_manager.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/exceptions.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/local_api.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/protocol.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/py.typed +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/roborock_future.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/roborock_message.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/util.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/version_1_apis/__init__.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/version_a01_apis/__init__.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
- {python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
from collections import namedtuple
|
|
4
5
|
from enum import Enum, IntEnum
|
|
5
6
|
|
|
6
7
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -50,6 +51,68 @@ class RoborockEnum(IntEnum):
|
|
|
50
51
|
return cls.as_dict().items()
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
ProductInfo = namedtuple("ProductInfo", ["nickname", "short_models"])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RoborockProductNickname(Enum):
|
|
58
|
+
# Coral Series
|
|
59
|
+
CORAL = ProductInfo(nickname="Coral", short_models=("a20", "a21"))
|
|
60
|
+
CORALPRO = ProductInfo(nickname="CoralPro", short_models=("a143", "a144"))
|
|
61
|
+
|
|
62
|
+
# Pearl Series
|
|
63
|
+
PEARL = ProductInfo(nickname="Pearl", short_models=("a74", "a75"))
|
|
64
|
+
PEARLC = ProductInfo(nickname="PearlC", short_models=("a103", "a104"))
|
|
65
|
+
PEARLE = ProductInfo(nickname="PearlE", short_models=("a167", "a168"))
|
|
66
|
+
PEARLELITE = ProductInfo(nickname="PearlELite", short_models=("a169", "a170"))
|
|
67
|
+
PEARLPLUS = ProductInfo(nickname="PearlPlus", short_models=("a86", "a87"))
|
|
68
|
+
PEARLPLUSS = ProductInfo(nickname="PearlPlusS", short_models=("a116", "a117", "a136"))
|
|
69
|
+
PEARLS = ProductInfo(nickname="PearlS", short_models=("a100", "a101"))
|
|
70
|
+
PEARLSLITE = ProductInfo(nickname="PearlSLite", short_models=("a122", "a123"))
|
|
71
|
+
|
|
72
|
+
# Ruby Series
|
|
73
|
+
RUBYPLUS = ProductInfo(nickname="RubyPlus", short_models=("t4", "s4"))
|
|
74
|
+
RUBYSC = ProductInfo(nickname="RubySC", short_models=("p5", "a08"))
|
|
75
|
+
RUBYSE = ProductInfo(nickname="RubySE", short_models=("a19",))
|
|
76
|
+
RUBYSLITE = ProductInfo(nickname="RubySLite", short_models=("p6", "s5e", "a05"))
|
|
77
|
+
|
|
78
|
+
# Tanos Series
|
|
79
|
+
TANOS = ProductInfo(nickname="Tanos", short_models=("t6", "s6"))
|
|
80
|
+
TANOSE = ProductInfo(nickname="TanosE", short_models=("t7", "a11"))
|
|
81
|
+
TANOSS = ProductInfo(nickname="TanosS", short_models=("a14", "a15"))
|
|
82
|
+
TANOSSC = ProductInfo(nickname="TanosSC", short_models=("a39", "a40"))
|
|
83
|
+
TANOSSE = ProductInfo(nickname="TanosSE", short_models=("a33", "a34"))
|
|
84
|
+
TANOSSMAX = ProductInfo(nickname="TanosSMax", short_models=("a52",))
|
|
85
|
+
TANOSSLITE = ProductInfo(nickname="TanosSLite", short_models=("a37", "a38"))
|
|
86
|
+
TANOSSPLUS = ProductInfo(nickname="TanosSPlus", short_models=("a23", "a24"))
|
|
87
|
+
TANOSV = ProductInfo(nickname="TanosV", short_models=("t7p", "a09", "a10"))
|
|
88
|
+
|
|
89
|
+
# Topaz Series
|
|
90
|
+
TOPAZS = ProductInfo(nickname="TopazS", short_models=("a29", "a30", "a76"))
|
|
91
|
+
TOPAZSC = ProductInfo(nickname="TopazSC", short_models=("a64", "a65"))
|
|
92
|
+
TOPAZSPLUS = ProductInfo(nickname="TopazSPlus", short_models=("a46", "a47", "a66"))
|
|
93
|
+
TOPAZSPOWER = ProductInfo(nickname="TopazSPower", short_models=("a62",))
|
|
94
|
+
TOPAZSV = ProductInfo(nickname="TopazSV", short_models=("a26", "a27"))
|
|
95
|
+
|
|
96
|
+
# Ultron Series
|
|
97
|
+
ULTRON = ProductInfo(nickname="Ultron", short_models=("a50", "a51"))
|
|
98
|
+
ULTRONE = ProductInfo(nickname="UltronE", short_models=("a72", "a84"))
|
|
99
|
+
ULTRONLITE = ProductInfo(nickname="UltronLite", short_models=("a73", "a85"))
|
|
100
|
+
ULTRONSC = ProductInfo(nickname="UltronSC", short_models=("a94", "a95"))
|
|
101
|
+
ULTRONSE = ProductInfo(nickname="UltronSE", short_models=("a124", "a125", "a139", "a140"))
|
|
102
|
+
ULTRONSPLUS = ProductInfo(nickname="UltronSPlus", short_models=("a68", "a69", "a70"))
|
|
103
|
+
ULTRONSV = ProductInfo(nickname="UltronSV", short_models=("a96", "a97"))
|
|
104
|
+
|
|
105
|
+
# Verdelite Series
|
|
106
|
+
VERDELITE = ProductInfo(nickname="Verdelite", short_models=("a146", "a147"))
|
|
107
|
+
|
|
108
|
+
# Vivian Series
|
|
109
|
+
VIVIAN = ProductInfo(nickname="Vivian", short_models=("a134", "a135", "a155", "a156"))
|
|
110
|
+
VIVIANC = ProductInfo(nickname="VivianC", short_models=("a158", "a159"))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
SHORT_MODEL_TO_ENUM = {model: product for product in RoborockProductNickname for model in product.value.short_models}
|
|
114
|
+
|
|
115
|
+
|
|
53
116
|
class RoborockStateCode(RoborockEnum):
|
|
54
117
|
unknown = 0
|
|
55
118
|
starting = 1
|
|
@@ -11,6 +11,7 @@ from functools import cached_property
|
|
|
11
11
|
from typing import Any, NamedTuple, get_args, get_origin
|
|
12
12
|
|
|
13
13
|
from .code_mappings import (
|
|
14
|
+
SHORT_MODEL_TO_ENUM,
|
|
14
15
|
RoborockCategory,
|
|
15
16
|
RoborockCleanType,
|
|
16
17
|
RoborockDockDustCollectionModeCode,
|
|
@@ -53,6 +54,7 @@ from .code_mappings import (
|
|
|
53
54
|
RoborockMopModeS8ProUltra,
|
|
54
55
|
RoborockMopModeSaros10,
|
|
55
56
|
RoborockMopModeSaros10R,
|
|
57
|
+
RoborockProductNickname,
|
|
56
58
|
RoborockStartType,
|
|
57
59
|
RoborockStateCode,
|
|
58
60
|
)
|
|
@@ -87,6 +89,7 @@ from .const import (
|
|
|
87
89
|
STRAINER_REPLACE_TIME,
|
|
88
90
|
ROBOROCK_G20S_Ultra,
|
|
89
91
|
)
|
|
92
|
+
from .device_features import DeviceFeatures
|
|
90
93
|
from .exceptions import RoborockException
|
|
91
94
|
|
|
92
95
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -306,144 +309,6 @@ class HomeDataDevice(RoborockBase):
|
|
|
306
309
|
silent_ota_switch: bool | None = None
|
|
307
310
|
setting: Any | None = None
|
|
308
311
|
f: bool | None = None
|
|
309
|
-
device_features: DeviceFeatures | None = None
|
|
310
|
-
|
|
311
|
-
# seemingly not just str like I thought - example: '0000000000002000' and '0000000000002F63'
|
|
312
|
-
|
|
313
|
-
# def __post_init__(self):
|
|
314
|
-
# if self.feature_set is not None and self.new_feature_set is not None and self.new_feature_set != "":
|
|
315
|
-
# self.device_features = build_device_features(self.feature_set, self.new_feature_set)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
@dataclass
|
|
319
|
-
class DeviceFeatures(RoborockBase):
|
|
320
|
-
map_carpet_add_supported: bool
|
|
321
|
-
show_clean_finish_reason_supported: bool
|
|
322
|
-
resegment_supported: bool
|
|
323
|
-
video_monitor_supported: bool
|
|
324
|
-
any_state_transit_goto_supported: bool
|
|
325
|
-
fw_filter_obstacle_supported: bool
|
|
326
|
-
video_setting_supported: bool
|
|
327
|
-
ignore_unknown_map_object_supported: bool
|
|
328
|
-
set_child_supported: bool
|
|
329
|
-
carpet_supported: bool
|
|
330
|
-
mop_path_supported: bool
|
|
331
|
-
multi_map_segment_timer_supported: bool
|
|
332
|
-
custom_water_box_distance_supported: bool
|
|
333
|
-
wash_then_charge_cmd_supported: bool
|
|
334
|
-
room_name_supported: bool
|
|
335
|
-
current_map_restore_enabled: bool
|
|
336
|
-
photo_upload_supported: bool
|
|
337
|
-
shake_mop_set_supported: bool
|
|
338
|
-
map_beautify_internal_debug_supported: bool
|
|
339
|
-
new_data_for_clean_history: bool
|
|
340
|
-
new_data_for_clean_history_detail: bool
|
|
341
|
-
flow_led_setting_supported: bool
|
|
342
|
-
dust_collection_setting_supported: bool
|
|
343
|
-
rpc_retry_supported: bool
|
|
344
|
-
avoid_collision_supported: bool
|
|
345
|
-
support_set_switch_map_mode: bool
|
|
346
|
-
support_smart_scene: bool
|
|
347
|
-
support_floor_edit: bool
|
|
348
|
-
support_furniture: bool
|
|
349
|
-
support_room_tag: bool
|
|
350
|
-
support_quick_map_builder: bool
|
|
351
|
-
support_smart_global_clean_with_custom_mode: bool
|
|
352
|
-
record_allowed: bool
|
|
353
|
-
careful_slow_map_supported: bool
|
|
354
|
-
egg_mode_supported: bool
|
|
355
|
-
unsave_map_reason_supported: bool
|
|
356
|
-
carpet_show_on_map: bool
|
|
357
|
-
supported_valley_electricity: bool
|
|
358
|
-
drying_supported: bool
|
|
359
|
-
download_test_voice_supported: bool
|
|
360
|
-
support_backup_map: bool
|
|
361
|
-
support_custom_mode_in_cleaning: bool
|
|
362
|
-
support_remote_control_in_call: bool
|
|
363
|
-
support_set_volume_in_call: bool
|
|
364
|
-
support_clean_estimate: bool
|
|
365
|
-
support_custom_dnd: bool
|
|
366
|
-
carpet_deep_clean_supported: bool
|
|
367
|
-
stuck_zone_supported: bool
|
|
368
|
-
custom_door_sill_supported: bool
|
|
369
|
-
clean_route_fast_mode_supported: bool
|
|
370
|
-
cliff_zone_supported: bool
|
|
371
|
-
smart_door_sill_supported: bool
|
|
372
|
-
support_floor_direction: bool
|
|
373
|
-
wifi_manage_supported: bool
|
|
374
|
-
back_charge_auto_wash_supported: bool
|
|
375
|
-
support_incremental_map: bool
|
|
376
|
-
offline_map_supported: bool
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def build_device_features(feature_set: str, new_feature_set: str) -> DeviceFeatures:
|
|
380
|
-
new_feature_set_int = int(new_feature_set)
|
|
381
|
-
feature_set_int = int(feature_set)
|
|
382
|
-
new_feature_set_divided = int(new_feature_set_int / (2**32))
|
|
383
|
-
# Convert last 8 digits of new feature set into hexadecimal number
|
|
384
|
-
converted_new_feature_set = int("0x" + new_feature_set[-8:], 16)
|
|
385
|
-
new_feature_set_mod_8: bool = len(new_feature_set) % 8 == 0
|
|
386
|
-
return DeviceFeatures(
|
|
387
|
-
map_carpet_add_supported=bool(1073741824 & new_feature_set_int),
|
|
388
|
-
show_clean_finish_reason_supported=bool(1 & new_feature_set_int),
|
|
389
|
-
resegment_supported=bool(4 & new_feature_set_int),
|
|
390
|
-
video_monitor_supported=bool(8 & new_feature_set_int),
|
|
391
|
-
any_state_transit_goto_supported=bool(16 & new_feature_set_int),
|
|
392
|
-
fw_filter_obstacle_supported=bool(32 & new_feature_set_int),
|
|
393
|
-
video_setting_supported=bool(64 & new_feature_set_int),
|
|
394
|
-
ignore_unknown_map_object_supported=bool(128 & new_feature_set_int),
|
|
395
|
-
set_child_supported=bool(256 & new_feature_set_int),
|
|
396
|
-
carpet_supported=bool(512 & new_feature_set_int),
|
|
397
|
-
mop_path_supported=bool(2048 & new_feature_set_int),
|
|
398
|
-
multi_map_segment_timer_supported=bool(feature_set_int and 4096 & new_feature_set_int),
|
|
399
|
-
custom_water_box_distance_supported=bool(new_feature_set_int and 2147483648 & new_feature_set_int),
|
|
400
|
-
wash_then_charge_cmd_supported=bool((new_feature_set_divided >> 5) & 1),
|
|
401
|
-
room_name_supported=bool(16384 & new_feature_set_int),
|
|
402
|
-
current_map_restore_enabled=bool(8192 & new_feature_set_int),
|
|
403
|
-
photo_upload_supported=bool(65536 & new_feature_set_int),
|
|
404
|
-
shake_mop_set_supported=bool(262144 & new_feature_set_int),
|
|
405
|
-
map_beautify_internal_debug_supported=bool(2097152 & new_feature_set_int),
|
|
406
|
-
new_data_for_clean_history=bool(4194304 & new_feature_set_int),
|
|
407
|
-
new_data_for_clean_history_detail=bool(8388608 & new_feature_set_int),
|
|
408
|
-
flow_led_setting_supported=bool(16777216 & new_feature_set_int),
|
|
409
|
-
dust_collection_setting_supported=bool(33554432 & new_feature_set_int),
|
|
410
|
-
rpc_retry_supported=bool(67108864 & new_feature_set_int),
|
|
411
|
-
avoid_collision_supported=bool(134217728 & new_feature_set_int),
|
|
412
|
-
support_set_switch_map_mode=bool(268435456 & new_feature_set_int),
|
|
413
|
-
support_smart_scene=bool(new_feature_set_divided & 2),
|
|
414
|
-
support_floor_edit=bool(new_feature_set_divided & 8),
|
|
415
|
-
support_furniture=bool((new_feature_set_divided >> 4) & 1),
|
|
416
|
-
support_room_tag=bool((new_feature_set_divided >> 6) & 1),
|
|
417
|
-
support_quick_map_builder=bool((new_feature_set_divided >> 7) & 1),
|
|
418
|
-
support_smart_global_clean_with_custom_mode=bool((new_feature_set_divided >> 8) & 1),
|
|
419
|
-
record_allowed=bool(1024 & new_feature_set_int),
|
|
420
|
-
careful_slow_map_supported=bool((new_feature_set_divided >> 9) & 1),
|
|
421
|
-
egg_mode_supported=bool((new_feature_set_divided >> 10) & 1),
|
|
422
|
-
unsave_map_reason_supported=bool((new_feature_set_divided >> 14) & 1),
|
|
423
|
-
carpet_show_on_map=bool((new_feature_set_divided >> 12) & 1),
|
|
424
|
-
supported_valley_electricity=bool((new_feature_set_divided >> 13) & 1),
|
|
425
|
-
# This one could actually be incorrect
|
|
426
|
-
# ((t.robotNewFeatures / 2 ** 32) >> 15) & 1 && (module422.DMM.isTopazSV_CE || 'cn' == t.deviceLocation));
|
|
427
|
-
drying_supported=bool((new_feature_set_divided >> 15) & 1),
|
|
428
|
-
download_test_voice_supported=bool((new_feature_set_divided >> 16) & 1),
|
|
429
|
-
support_backup_map=bool((new_feature_set_divided >> 17) & 1),
|
|
430
|
-
support_custom_mode_in_cleaning=bool((new_feature_set_divided >> 18) & 1),
|
|
431
|
-
support_remote_control_in_call=bool((new_feature_set_divided >> 19) & 1),
|
|
432
|
-
support_set_volume_in_call=new_feature_set_mod_8 and bool(1 & converted_new_feature_set),
|
|
433
|
-
support_clean_estimate=new_feature_set_mod_8 and bool(2 & converted_new_feature_set),
|
|
434
|
-
support_custom_dnd=new_feature_set_mod_8 and bool(4 & converted_new_feature_set),
|
|
435
|
-
carpet_deep_clean_supported=bool(8 & converted_new_feature_set),
|
|
436
|
-
stuck_zone_supported=new_feature_set_mod_8 and bool(16 & converted_new_feature_set),
|
|
437
|
-
custom_door_sill_supported=new_feature_set_mod_8 and bool(32 & converted_new_feature_set),
|
|
438
|
-
clean_route_fast_mode_supported=bool(256 & converted_new_feature_set),
|
|
439
|
-
cliff_zone_supported=new_feature_set_mod_8 and bool(512 & converted_new_feature_set),
|
|
440
|
-
smart_door_sill_supported=new_feature_set_mod_8 and bool(1024 & converted_new_feature_set),
|
|
441
|
-
support_floor_direction=new_feature_set_mod_8 and bool(2048 & converted_new_feature_set),
|
|
442
|
-
wifi_manage_supported=bool(128 & converted_new_feature_set),
|
|
443
|
-
back_charge_auto_wash_supported=bool(4096 & converted_new_feature_set),
|
|
444
|
-
support_incremental_map=bool(8192 & converted_new_feature_set),
|
|
445
|
-
offline_map_supported=bool(16384 & converted_new_feature_set),
|
|
446
|
-
)
|
|
447
312
|
|
|
448
313
|
|
|
449
314
|
@dataclass
|
|
@@ -890,6 +755,11 @@ class DeviceData(RoborockBase):
|
|
|
890
755
|
device: HomeDataDevice
|
|
891
756
|
model: str
|
|
892
757
|
host: str | None = None
|
|
758
|
+
product_nickname: RoborockProductNickname | None = None
|
|
759
|
+
device_features: DeviceFeatures | None = None
|
|
760
|
+
|
|
761
|
+
def __post_init__(self):
|
|
762
|
+
self.product_nickname = SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
|
|
893
763
|
|
|
894
764
|
|
|
895
765
|
@dataclass
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field, fields
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from roborock import RoborockProductNickname
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NewFeatureStrBit(IntEnum):
|
|
11
|
+
TWO_KEY_REAL_TIME_VIDEO = 32
|
|
12
|
+
TWO_KEY_RTV_IN_CHARGING = 33
|
|
13
|
+
DIRTY_REPLENISH_CLEAN = 34
|
|
14
|
+
AUTO_DELIVERY_FIELD_IN_GLOBAL_STATUS = 35
|
|
15
|
+
AVOID_COLLISION_MODE = 36
|
|
16
|
+
VOICE_CONTROL = 37
|
|
17
|
+
NEW_ENDPOINT = 38
|
|
18
|
+
PUMPING_WATER = 39
|
|
19
|
+
CORNER_MOP_STRETCH = 40
|
|
20
|
+
HOT_WASH_TOWEL = 41
|
|
21
|
+
FLOOR_DIR_CLEAN_ANY_TIME = 42
|
|
22
|
+
PET_SUPPLIES_DEEP_CLEAN = 43
|
|
23
|
+
MOP_SHAKE_WATER_MAX = 45
|
|
24
|
+
EXACT_CUSTOM_MODE = 47
|
|
25
|
+
VIDEO_PATROL = 48
|
|
26
|
+
CARPET_CUSTOM_CLEAN = 49
|
|
27
|
+
PET_SNAPSHOT = 50
|
|
28
|
+
CUSTOM_CLEAN_MODE_COUNT = 51
|
|
29
|
+
NEW_AI_RECOGNITION = 52
|
|
30
|
+
AUTO_COLLECTION_2 = 53
|
|
31
|
+
RIGHT_BRUSH_STRETCH = 54
|
|
32
|
+
SMART_CLEAN_MODE_SET = 55
|
|
33
|
+
DIRTY_OBJECT_DETECT = 56
|
|
34
|
+
NO_NEED_CARPET_PRESS_SET = 57
|
|
35
|
+
VOICE_CONTROL_LED = 58
|
|
36
|
+
WATER_LEAK_CHECK = 60
|
|
37
|
+
MIN_BATTERY_15_TO_CLEAN_TASK = 62
|
|
38
|
+
GAP_DEEP_CLEAN = 63
|
|
39
|
+
OBJECT_DETECT_CHECK = 64
|
|
40
|
+
IDENTIFY_ROOM = 66
|
|
41
|
+
MATTER = 67
|
|
42
|
+
WORKDAY_HOLIDAY = 69
|
|
43
|
+
CLEAN_DIRECT_STATUS = 70
|
|
44
|
+
MAP_ERASER = 71
|
|
45
|
+
OPTIMIZE_BATTERY = 72
|
|
46
|
+
ACTIVATE_VIDEO_CHARGING_AND_STANDBY = 73
|
|
47
|
+
CARPET_LONG_HAIRED = 75
|
|
48
|
+
CLEAN_HISTORY_TIME_LINE = 76
|
|
49
|
+
MAX_ZONE_OPENED = 77
|
|
50
|
+
EXHIBITION_FUNCTION = 78
|
|
51
|
+
LDS_LIFTING = 79
|
|
52
|
+
AUTO_TEAR_DOWN_MOP = 80
|
|
53
|
+
SMALL_SIDE_MOP = 81
|
|
54
|
+
SUPPORT_SIDE_BRUSH_UP_DOWN = 82
|
|
55
|
+
DRY_INTERVAL_TIMER = 83
|
|
56
|
+
UVC_STERILIZE = 84
|
|
57
|
+
MIDWAY_BACK_TO_DOCK = 85
|
|
58
|
+
SUPPORT_MAIN_BRUSH_UP_DOWN = 86
|
|
59
|
+
EGG_DANCE_MODE = 87
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class DeviceFeatures:
|
|
64
|
+
"""Represents the features supported by a Roborock device."""
|
|
65
|
+
|
|
66
|
+
# Features from robot_new_features (lower 32 bits)
|
|
67
|
+
is_show_clean_finish_reason_supported: bool = field(metadata={"robot_new_features": 1})
|
|
68
|
+
is_re_segment_supported: bool = field(metadata={"robot_new_features": 4})
|
|
69
|
+
is_video_monitor_supported: bool = field(metadata={"robot_new_features": 8})
|
|
70
|
+
is_any_state_transit_goto_supported: bool = field(metadata={"robot_new_features": 16})
|
|
71
|
+
is_fw_filter_obstacle_supported: bool = field(metadata={"robot_new_features": 32})
|
|
72
|
+
is_video_setting_supported: bool = field(metadata={"robot_new_features": 64})
|
|
73
|
+
is_ignore_unknown_map_object_supported: bool = field(metadata={"robot_new_features": 128})
|
|
74
|
+
is_set_child_supported: bool = field(metadata={"robot_new_features": 256})
|
|
75
|
+
is_carpet_supported: bool = field(metadata={"robot_new_features": 512})
|
|
76
|
+
is_record_allowed: bool = field(metadata={"robot_new_features": 1024})
|
|
77
|
+
is_mop_path_supported: bool = field(metadata={"robot_new_features": 2048})
|
|
78
|
+
is_multi_map_segment_timer_supported: bool = field(metadata={"robot_new_features": 4096})
|
|
79
|
+
is_current_map_restore_enabled: bool = field(metadata={"robot_new_features": 8192})
|
|
80
|
+
is_room_name_supported: bool = field(metadata={"robot_new_features": 16384})
|
|
81
|
+
is_shake_mop_set_supported: bool = field(metadata={"robot_new_features": 262144})
|
|
82
|
+
is_map_beautify_internal_debug_supported: bool = field(metadata={"robot_new_features": 2097152})
|
|
83
|
+
is_new_data_for_clean_history: bool = field(metadata={"robot_new_features": 4194304})
|
|
84
|
+
is_new_data_for_clean_history_detail: bool = field(metadata={"robot_new_features": 8388608})
|
|
85
|
+
is_flow_led_setting_supported: bool = field(metadata={"robot_new_features": 16777216})
|
|
86
|
+
is_dust_collection_setting_supported: bool = field(metadata={"robot_new_features": 33554432})
|
|
87
|
+
is_rpc_retry_supported: bool = field(metadata={"robot_new_features": 67108864})
|
|
88
|
+
is_avoid_collision_supported: bool = field(metadata={"robot_new_features": 134217728})
|
|
89
|
+
is_support_set_switch_map_mode: bool = field(metadata={"robot_new_features": 268435456})
|
|
90
|
+
is_map_carpet_add_support: bool = field(metadata={"robot_new_features": 1073741824})
|
|
91
|
+
is_custom_water_box_distance_supported: bool = field(metadata={"robot_new_features": 2147483648})
|
|
92
|
+
|
|
93
|
+
# Features from robot_new_features (upper 32 bits)
|
|
94
|
+
is_support_smart_scene: bool = field(metadata={"upper_32_bits": 1})
|
|
95
|
+
is_support_floor_edit: bool = field(metadata={"upper_32_bits": 3})
|
|
96
|
+
is_support_furniture: bool = field(metadata={"upper_32_bits": 4})
|
|
97
|
+
is_wash_then_charge_cmd_supported: bool = field(metadata={"upper_32_bits": 5})
|
|
98
|
+
is_support_room_tag: bool = field(metadata={"upper_32_bits": 6})
|
|
99
|
+
is_support_quick_map_builder: bool = field(metadata={"upper_32_bits": 7})
|
|
100
|
+
is_support_smart_global_clean_with_custom_mode: bool = field(metadata={"upper_32_bits": 8})
|
|
101
|
+
is_careful_slow_mop_supported: bool = field(metadata={"upper_32_bits": 9})
|
|
102
|
+
is_egg_mode_supported_from_new_features: bool = field(metadata={"upper_32_bits": 10})
|
|
103
|
+
is_carpet_show_on_map: bool = field(metadata={"upper_32_bits": 12})
|
|
104
|
+
is_supported_valley_electricity: bool = field(metadata={"upper_32_bits": 13})
|
|
105
|
+
is_unsave_map_reason_supported: bool = field(metadata={"upper_32_bits": 14})
|
|
106
|
+
is_supported_drying: bool = field(metadata={"upper_32_bits": 15})
|
|
107
|
+
is_supported_download_test_voice: bool = field(metadata={"upper_32_bits": 16})
|
|
108
|
+
is_support_backup_map: bool = field(metadata={"upper_32_bits": 17})
|
|
109
|
+
is_support_custom_mode_in_cleaning: bool = field(metadata={"upper_32_bits": 18})
|
|
110
|
+
is_support_remote_control_in_call: bool = field(metadata={"upper_32_bits": 19})
|
|
111
|
+
|
|
112
|
+
# Features from new_feature_info_str (masking last 8 chars / 32 bits)
|
|
113
|
+
is_support_set_volume_in_call: bool = field(metadata={"new_feature_str_mask": (1, 8)})
|
|
114
|
+
is_support_clean_estimate: bool = field(metadata={"new_feature_str_mask": (2, 8)})
|
|
115
|
+
is_support_custom_dnd: bool = field(metadata={"new_feature_str_mask": (4, 8)})
|
|
116
|
+
is_carpet_deep_clean_supported: bool = field(metadata={"new_feature_str_mask": (8, 8)})
|
|
117
|
+
is_support_stuck_zone: bool = field(metadata={"new_feature_str_mask": (16, 8)})
|
|
118
|
+
is_support_custom_door_sill: bool = field(metadata={"new_feature_str_mask": (32, 8)})
|
|
119
|
+
is_wifi_manage_supported: bool = field(metadata={"new_feature_str_mask": (128, 8)})
|
|
120
|
+
is_clean_route_fast_mode_supported: bool = field(metadata={"new_feature_str_mask": (256, 8)})
|
|
121
|
+
is_support_cliff_zone: bool = field(metadata={"new_feature_str_mask": (512, 8)})
|
|
122
|
+
is_support_smart_door_sill: bool = field(metadata={"new_feature_str_mask": (1024, 8)})
|
|
123
|
+
is_support_floor_direction: bool = field(metadata={"new_feature_str_mask": (2048, 8)})
|
|
124
|
+
is_back_charge_auto_wash_supported: bool = field(metadata={"new_feature_str_mask": (4096, 8)})
|
|
125
|
+
is_support_incremental_map: bool = field(metadata={"new_feature_str_mask": (4194304, 8)})
|
|
126
|
+
is_offline_map_supported: bool = field(metadata={"new_feature_str_mask": (16384, 8)})
|
|
127
|
+
is_super_deep_wash_supported: bool = field(metadata={"new_feature_str_mask": (32768, 8)})
|
|
128
|
+
is_ces2022_supported: bool = field(metadata={"new_feature_str_mask": (65536, 8)})
|
|
129
|
+
is_dss_believable: bool = field(metadata={"new_feature_str_mask": (131072, 8)})
|
|
130
|
+
is_main_brush_up_down_supported_from_str: bool = field(metadata={"new_feature_str_mask": (262144, 8)})
|
|
131
|
+
is_goto_pure_clean_path_supported: bool = field(metadata={"new_feature_str_mask": (524288, 8)})
|
|
132
|
+
is_water_up_down_drain_supported: bool = field(metadata={"new_feature_str_mask": (1048576, 8)})
|
|
133
|
+
is_setting_carpet_first_supported: bool = field(metadata={"new_feature_str_mask": (8388608, 8)})
|
|
134
|
+
is_clean_route_deep_slow_plus_supported: bool = field(metadata={"new_feature_str_mask": (16777216, 8)})
|
|
135
|
+
is_dynamically_skip_clean_zone_supported: bool = field(metadata={"new_feature_str_mask": (33554432, 8)})
|
|
136
|
+
is_dynamically_add_clean_zones_supported: bool = field(metadata={"new_feature_str_mask": (67108864, 8)})
|
|
137
|
+
is_left_water_drain_supported: bool = field(metadata={"new_feature_str_mask": (134217728, 8)})
|
|
138
|
+
is_clean_count_setting_supported: bool = field(metadata={"new_feature_str_mask": (1073741824, 8)})
|
|
139
|
+
is_corner_clean_mode_supported: bool = field(metadata={"new_feature_str_mask": (2147483648, 8)})
|
|
140
|
+
|
|
141
|
+
# Features from new_feature_info_str (by bit index)
|
|
142
|
+
is_two_key_real_time_video_supported: bool = field(
|
|
143
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.TWO_KEY_REAL_TIME_VIDEO}
|
|
144
|
+
)
|
|
145
|
+
is_two_key_rtv_in_charging_supported: bool = field(
|
|
146
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.TWO_KEY_RTV_IN_CHARGING}
|
|
147
|
+
)
|
|
148
|
+
is_dirty_replenish_clean_supported: bool = field(
|
|
149
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.DIRTY_REPLENISH_CLEAN}
|
|
150
|
+
)
|
|
151
|
+
is_auto_delivery_field_in_global_status_supported: bool = field(
|
|
152
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.AUTO_DELIVERY_FIELD_IN_GLOBAL_STATUS}
|
|
153
|
+
)
|
|
154
|
+
is_avoid_collision_mode_supported: bool = field(
|
|
155
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.AVOID_COLLISION_MODE}
|
|
156
|
+
)
|
|
157
|
+
is_voice_control_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.VOICE_CONTROL})
|
|
158
|
+
is_new_endpoint_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.NEW_ENDPOINT})
|
|
159
|
+
is_pumping_water_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.PUMPING_WATER})
|
|
160
|
+
is_corner_mop_stretch_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.CORNER_MOP_STRETCH})
|
|
161
|
+
is_hot_wash_towel_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.HOT_WASH_TOWEL})
|
|
162
|
+
is_floor_dir_clean_any_time_supported: bool = field(
|
|
163
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.FLOOR_DIR_CLEAN_ANY_TIME}
|
|
164
|
+
)
|
|
165
|
+
is_pet_supplies_deep_clean_supported: bool = field(
|
|
166
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.PET_SUPPLIES_DEEP_CLEAN}
|
|
167
|
+
)
|
|
168
|
+
is_mop_shake_water_max_supported: bool = field(
|
|
169
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.MOP_SHAKE_WATER_MAX}
|
|
170
|
+
)
|
|
171
|
+
is_exact_custom_mode_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.EXACT_CUSTOM_MODE})
|
|
172
|
+
is_video_patrol_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.VIDEO_PATROL})
|
|
173
|
+
is_carpet_custom_clean_supported: bool = field(
|
|
174
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.CARPET_CUSTOM_CLEAN}
|
|
175
|
+
)
|
|
176
|
+
is_pet_snapshot_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.PET_SNAPSHOT})
|
|
177
|
+
is_custom_clean_mode_count_supported: bool = field(
|
|
178
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.CUSTOM_CLEAN_MODE_COUNT}
|
|
179
|
+
)
|
|
180
|
+
is_new_ai_recognition_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.NEW_AI_RECOGNITION})
|
|
181
|
+
is_auto_collection_2_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.AUTO_COLLECTION_2})
|
|
182
|
+
is_right_brush_stretch_supported: bool = field(
|
|
183
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.RIGHT_BRUSH_STRETCH}
|
|
184
|
+
)
|
|
185
|
+
is_smart_clean_mode_set_supported: bool = field(
|
|
186
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.SMART_CLEAN_MODE_SET}
|
|
187
|
+
)
|
|
188
|
+
is_dirty_object_detect_supported: bool = field(
|
|
189
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.DIRTY_OBJECT_DETECT}
|
|
190
|
+
)
|
|
191
|
+
is_no_need_carpet_press_set_supported: bool = field(
|
|
192
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.NO_NEED_CARPET_PRESS_SET}
|
|
193
|
+
)
|
|
194
|
+
is_voice_control_led_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.VOICE_CONTROL_LED})
|
|
195
|
+
is_water_leak_check_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.WATER_LEAK_CHECK})
|
|
196
|
+
is_min_battery_15_to_clean_task_supported: bool = field(
|
|
197
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.MIN_BATTERY_15_TO_CLEAN_TASK}
|
|
198
|
+
)
|
|
199
|
+
is_gap_deep_clean_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.GAP_DEEP_CLEAN})
|
|
200
|
+
is_object_detect_check_supported: bool = field(
|
|
201
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.OBJECT_DETECT_CHECK}
|
|
202
|
+
)
|
|
203
|
+
is_identify_room_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.IDENTIFY_ROOM})
|
|
204
|
+
is_matter_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.MATTER})
|
|
205
|
+
is_workday_holiday_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.WORKDAY_HOLIDAY})
|
|
206
|
+
is_clean_direct_status_supported: bool = field(
|
|
207
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.CLEAN_DIRECT_STATUS}
|
|
208
|
+
)
|
|
209
|
+
is_map_eraser_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.MAP_ERASER})
|
|
210
|
+
is_optimize_battery_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.OPTIMIZE_BATTERY})
|
|
211
|
+
is_activate_video_charging_and_standby_supported: bool = field(
|
|
212
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.ACTIVATE_VIDEO_CHARGING_AND_STANDBY}
|
|
213
|
+
)
|
|
214
|
+
is_carpet_long_haired_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.CARPET_LONG_HAIRED})
|
|
215
|
+
is_clean_history_time_line_supported: bool = field(
|
|
216
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.CLEAN_HISTORY_TIME_LINE}
|
|
217
|
+
)
|
|
218
|
+
is_max_zone_opened_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.MAX_ZONE_OPENED})
|
|
219
|
+
is_exhibition_function_supported: bool = field(
|
|
220
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.EXHIBITION_FUNCTION}
|
|
221
|
+
)
|
|
222
|
+
is_lds_lifting_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.LDS_LIFTING})
|
|
223
|
+
is_auto_tear_down_mop_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.AUTO_TEAR_DOWN_MOP})
|
|
224
|
+
is_small_side_mop_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.SMALL_SIDE_MOP})
|
|
225
|
+
is_support_side_brush_up_down_supported: bool = field(
|
|
226
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.SUPPORT_SIDE_BRUSH_UP_DOWN}
|
|
227
|
+
)
|
|
228
|
+
is_dry_interval_timer_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.DRY_INTERVAL_TIMER})
|
|
229
|
+
is_uvc_sterilize_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.UVC_STERILIZE})
|
|
230
|
+
is_midway_back_to_dock_supported: bool = field(
|
|
231
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.MIDWAY_BACK_TO_DOCK}
|
|
232
|
+
)
|
|
233
|
+
is_support_main_brush_up_down_supported: bool = field(
|
|
234
|
+
metadata={"new_feature_str_bit": NewFeatureStrBit.SUPPORT_MAIN_BRUSH_UP_DOWN}
|
|
235
|
+
)
|
|
236
|
+
is_egg_dance_mode_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.EGG_DANCE_MODE})
|
|
237
|
+
|
|
238
|
+
# Features from feature_info list
|
|
239
|
+
is_led_status_switch_supported: bool = field(metadata={"robot_features": 119})
|
|
240
|
+
is_multi_floor_supported: bool = field(metadata={"robot_features": 120})
|
|
241
|
+
is_support_fetch_timer_summary: bool = field(metadata={"robot_features": 122})
|
|
242
|
+
is_order_clean_supported: bool = field(metadata={"robot_features": 123})
|
|
243
|
+
is_analysis_supported: bool = field(metadata={"robot_features": 124})
|
|
244
|
+
is_remote_supported: bool = field(metadata={"robot_features": 125})
|
|
245
|
+
is_support_voice_control_debug: bool = field(metadata={"robot_features": 130})
|
|
246
|
+
|
|
247
|
+
# Features from model whitelists/blacklists or other flags
|
|
248
|
+
is_mop_forbidden_supported: bool = field(
|
|
249
|
+
metadata={
|
|
250
|
+
"model_whitelist": [
|
|
251
|
+
RoborockProductNickname.TANOSV,
|
|
252
|
+
RoborockProductNickname.TOPAZSV,
|
|
253
|
+
RoborockProductNickname.TANOS,
|
|
254
|
+
RoborockProductNickname.TANOSE,
|
|
255
|
+
RoborockProductNickname.TANOSSLITE,
|
|
256
|
+
RoborockProductNickname.TANOSS,
|
|
257
|
+
RoborockProductNickname.TANOSSPLUS,
|
|
258
|
+
RoborockProductNickname.TANOSSMAX,
|
|
259
|
+
RoborockProductNickname.ULTRON,
|
|
260
|
+
RoborockProductNickname.ULTRONLITE,
|
|
261
|
+
RoborockProductNickname.PEARL,
|
|
262
|
+
RoborockProductNickname.RUBYSLITE,
|
|
263
|
+
]
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
is_soft_clean_mode_supported: bool = field(
|
|
267
|
+
metadata={
|
|
268
|
+
"model_whitelist": [
|
|
269
|
+
RoborockProductNickname.TANOSV,
|
|
270
|
+
RoborockProductNickname.TANOSE,
|
|
271
|
+
RoborockProductNickname.TANOS,
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
is_custom_mode_supported: bool = field(metadata={"model_blacklist": [RoborockProductNickname.TANOS]})
|
|
276
|
+
is_support_custom_carpet: bool = field(metadata={"model_whitelist": [RoborockProductNickname.ULTRONLITE]})
|
|
277
|
+
is_show_general_obstacle_supported: bool = field(metadata={"model_whitelist": [RoborockProductNickname.TANOSSPLUS]})
|
|
278
|
+
is_show_obstacle_photo_supported: bool = field(
|
|
279
|
+
metadata={
|
|
280
|
+
"model_whitelist": [
|
|
281
|
+
RoborockProductNickname.TANOSSPLUS,
|
|
282
|
+
RoborockProductNickname.TANOSSMAX,
|
|
283
|
+
RoborockProductNickname.ULTRON,
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
is_rubber_brush_carpet_supported: bool = field(metadata={"model_whitelist": [RoborockProductNickname.ULTRONLITE]})
|
|
288
|
+
is_carpet_pressure_use_origin_paras_supported: bool = field(
|
|
289
|
+
metadata={"model_whitelist": [RoborockProductNickname.ULTRONLITE]}
|
|
290
|
+
)
|
|
291
|
+
is_support_mop_back_pwm_set: bool = field(metadata={"model_whitelist": [RoborockProductNickname.PEARL]})
|
|
292
|
+
is_collect_dust_mode_supported: bool = field(metadata={"model_blacklist": [RoborockProductNickname.PEARL]})
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def from_feature_flags(
|
|
296
|
+
cls,
|
|
297
|
+
new_feature_info: int,
|
|
298
|
+
new_feature_info_str: str,
|
|
299
|
+
feature_info: list[int],
|
|
300
|
+
product_nickname: RoborockProductNickname | None,
|
|
301
|
+
) -> DeviceFeatures:
|
|
302
|
+
"""Creates a DeviceFeatures instance from raw feature flags.
|
|
303
|
+
:param new_feature_info: A int from get_init_status (sometimes can be found in homedata, but it is not always)
|
|
304
|
+
:param new_feature_info_str: A hex string from get_init_status or home_data.
|
|
305
|
+
:param feature_info: A list of ints from get_init_status
|
|
306
|
+
:param product_nickname: The product nickname of the device."""
|
|
307
|
+
# For any future reverse engineerining:
|
|
308
|
+
# RobotNewFeatures = new_feature_info
|
|
309
|
+
# newFeatureInfoStr = new_feature_info_str
|
|
310
|
+
# feature_info =robotFeatures
|
|
311
|
+
kwargs: dict[str, Any] = {}
|
|
312
|
+
|
|
313
|
+
for f in fields(cls):
|
|
314
|
+
# Default all features to False.
|
|
315
|
+
kwargs[f.name] = False
|
|
316
|
+
if not f.metadata:
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
if (mask := f.metadata.get("robot_new_features")) is not None:
|
|
320
|
+
kwargs[f.name] = bool(mask & new_feature_info)
|
|
321
|
+
elif (bit_index := f.metadata.get("upper_32_bits")) is not None:
|
|
322
|
+
# Check bits in the upper 32-bit integer of new_feature_info
|
|
323
|
+
if new_feature_info:
|
|
324
|
+
kwargs[f.name] = bool(((new_feature_info >> 32) >> bit_index) & 1)
|
|
325
|
+
elif (mask_info := f.metadata.get("new_feature_str_mask")) is not None:
|
|
326
|
+
# Check bitmask against a slice of the hex string
|
|
327
|
+
if new_feature_info_str:
|
|
328
|
+
try:
|
|
329
|
+
mask, slice_count = mask_info
|
|
330
|
+
if len(new_feature_info_str) >= slice_count:
|
|
331
|
+
last_chars = new_feature_info_str[-slice_count:]
|
|
332
|
+
value = int(last_chars, 16)
|
|
333
|
+
kwargs[f.name] = bool(mask & value)
|
|
334
|
+
except (ValueError, IndexError):
|
|
335
|
+
pass # Keep it False
|
|
336
|
+
elif (bit := f.metadata.get("new_feature_str_bit")) is not None:
|
|
337
|
+
# Check a specific bit in the hex string using its index
|
|
338
|
+
if new_feature_info_str:
|
|
339
|
+
try:
|
|
340
|
+
# Bit index defines which character and which bit inside it to check
|
|
341
|
+
char_index_from_end = 1 + bit.value // 4
|
|
342
|
+
if char_index_from_end <= len(new_feature_info_str):
|
|
343
|
+
char_hex = new_feature_info_str[-char_index_from_end]
|
|
344
|
+
nibble = int(char_hex, 16)
|
|
345
|
+
bit_in_nibble = bit.value % 4
|
|
346
|
+
kwargs[f.name] = bool((nibble >> bit_in_nibble) & 1)
|
|
347
|
+
except (ValueError, IndexError):
|
|
348
|
+
pass # Keep it False
|
|
349
|
+
elif (feature_id := f.metadata.get("robot_features")) is not None:
|
|
350
|
+
kwargs[f.name] = feature_id in feature_info
|
|
351
|
+
elif (whitelist := f.metadata.get("model_whitelist")) is not None:
|
|
352
|
+
# If product_nickname is None, assume it is not in the whitelist
|
|
353
|
+
kwargs[f.name] = product_nickname in whitelist or product_nickname is None
|
|
354
|
+
elif (blacklist := f.metadata.get("model_blacklist")) is not None:
|
|
355
|
+
# If product_nickname is None, assume it is not in the blacklist.
|
|
356
|
+
if product_nickname is None:
|
|
357
|
+
kwargs[f.name] = True
|
|
358
|
+
else:
|
|
359
|
+
kwargs[f.name] = product_nickname not in blacklist
|
|
360
|
+
|
|
361
|
+
return cls(**kwargs)
|
|
362
|
+
|
|
363
|
+
def get_supported_features(self) -> list[str]:
|
|
364
|
+
"""Returns a list of supported features (Primarily used for logging purposes)."""
|
|
365
|
+
return [k for k, v in vars(self).items() if v]
|
|
@@ -117,4 +117,4 @@ class RoborockDevice:
|
|
|
117
117
|
This is a placeholder command and will likely be changed/moved in the future.
|
|
118
118
|
"""
|
|
119
119
|
status_type: type[Status] = ModelStatus.get(self._product_info.model, S7MaxVStatus)
|
|
120
|
-
return await self._v1_channel.
|
|
120
|
+
return await self._v1_channel.rpc_channel.send_command(RoborockCommand.GET_STATUS, response_type=status_type)
|
|
@@ -50,6 +50,11 @@ class LocalChannel:
|
|
|
50
50
|
self._encoder: Encoder = create_local_encoder(local_key)
|
|
51
51
|
self._queue_lock = asyncio.Lock()
|
|
52
52
|
|
|
53
|
+
@property
|
|
54
|
+
def is_connected(self) -> bool:
|
|
55
|
+
"""Check if the channel is currently connected."""
|
|
56
|
+
return self._is_connected
|
|
57
|
+
|
|
53
58
|
async def connect(self) -> None:
|
|
54
59
|
"""Connect to the device."""
|
|
55
60
|
if self._is_connected:
|
|
@@ -113,7 +118,7 @@ class LocalChannel:
|
|
|
113
118
|
else:
|
|
114
119
|
_LOGGER.debug("Received message with no waiting handler: request_id=%s", request_id)
|
|
115
120
|
|
|
116
|
-
async def
|
|
121
|
+
async def send_message(self, message: RoborockMessage, timeout: float = 10.0) -> RoborockMessage:
|
|
117
122
|
"""Send a command message and wait for the response message."""
|
|
118
123
|
if not self._transport or not self._is_connected:
|
|
119
124
|
raise RoborockConnectionException("Not connected to device")
|
|
@@ -80,7 +80,7 @@ class MqttChannel:
|
|
|
80
80
|
else:
|
|
81
81
|
_LOGGER.debug("Received message with no waiting handler: request_id=%s", request_id)
|
|
82
82
|
|
|
83
|
-
async def
|
|
83
|
+
async def send_message(self, message: RoborockMessage, timeout: float = 10.0) -> RoborockMessage:
|
|
84
84
|
"""Send a command message and wait for the response message.
|
|
85
85
|
|
|
86
86
|
Returns the raw response message - caller is responsible for parsing.
|
|
@@ -6,25 +6,21 @@ handling both MQTT and local connections with automatic fallback.
|
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
8
|
from collections.abc import Callable
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import TypeVar
|
|
10
10
|
|
|
11
11
|
from roborock.containers import HomeDataDevice, NetworkInfo, RoborockBase, UserData
|
|
12
12
|
from roborock.exceptions import RoborockException
|
|
13
13
|
from roborock.mqtt.session import MqttParams, MqttSession
|
|
14
14
|
from roborock.protocols.v1_protocol import (
|
|
15
|
-
CommandType,
|
|
16
|
-
ParamsType,
|
|
17
15
|
SecurityData,
|
|
18
|
-
create_mqtt_payload_encoder,
|
|
19
16
|
create_security_data,
|
|
20
|
-
decode_rpc_response,
|
|
21
|
-
encode_local_payload,
|
|
22
17
|
)
|
|
23
18
|
from roborock.roborock_message import RoborockMessage
|
|
24
19
|
from roborock.roborock_typing import RoborockCommand
|
|
25
20
|
|
|
26
21
|
from .local_channel import LocalChannel, LocalSession, create_local_session
|
|
27
22
|
from .mqtt_channel import MqttChannel
|
|
23
|
+
from .v1_rpc_channel import V1RpcChannel, create_combined_rpc_channel, create_mqtt_rpc_channel
|
|
28
24
|
|
|
29
25
|
_LOGGER = logging.getLogger(__name__)
|
|
30
26
|
|
|
@@ -58,9 +54,10 @@ class V1Channel:
|
|
|
58
54
|
"""
|
|
59
55
|
self._device_uid = device_uid
|
|
60
56
|
self._mqtt_channel = mqtt_channel
|
|
61
|
-
self.
|
|
57
|
+
self._mqtt_rpc_channel = create_mqtt_rpc_channel(mqtt_channel, security_data)
|
|
62
58
|
self._local_session = local_session
|
|
63
59
|
self._local_channel: LocalChannel | None = None
|
|
60
|
+
self._combined_rpc_channel: V1RpcChannel | None = None
|
|
64
61
|
self._mqtt_unsub: Callable[[], None] | None = None
|
|
65
62
|
self._local_unsub: Callable[[], None] | None = None
|
|
66
63
|
self._callback: Callable[[RoborockMessage], None] | None = None
|
|
@@ -76,6 +73,16 @@ class V1Channel:
|
|
|
76
73
|
"""Return whether MQTT connection is available."""
|
|
77
74
|
return self._mqtt_unsub is not None
|
|
78
75
|
|
|
76
|
+
@property
|
|
77
|
+
def rpc_channel(self) -> V1RpcChannel:
|
|
78
|
+
"""Return the combined RPC channel prefers local with a fallback to MQTT."""
|
|
79
|
+
return self._combined_rpc_channel or self._mqtt_rpc_channel
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def mqtt_rpc_channel(self) -> V1RpcChannel:
|
|
83
|
+
"""Return the MQTT RPC channel."""
|
|
84
|
+
return self._mqtt_rpc_channel
|
|
85
|
+
|
|
79
86
|
async def subscribe(self, callback: Callable[[RoborockMessage], None]) -> Callable[[], None]:
|
|
80
87
|
"""Subscribe to all messages from the device.
|
|
81
88
|
|
|
@@ -119,7 +126,9 @@ class V1Channel:
|
|
|
119
126
|
This is a cloud only command used to get the local device's IP address.
|
|
120
127
|
"""
|
|
121
128
|
try:
|
|
122
|
-
return await self.
|
|
129
|
+
return await self._mqtt_rpc_channel.send_command(
|
|
130
|
+
RoborockCommand.GET_NETWORK_INFO, response_type=NetworkInfo
|
|
131
|
+
)
|
|
123
132
|
except RoborockException as e:
|
|
124
133
|
raise RoborockException(f"Network info failed for device {self._device_uid}") from e
|
|
125
134
|
|
|
@@ -136,59 +145,9 @@ class V1Channel:
|
|
|
136
145
|
except RoborockException as e:
|
|
137
146
|
self._local_channel = None
|
|
138
147
|
raise RoborockException(f"Error connecting to local device {self._device_uid}: {e}") from e
|
|
139
|
-
|
|
148
|
+
self._combined_rpc_channel = create_combined_rpc_channel(self._local_channel, self._mqtt_rpc_channel)
|
|
140
149
|
return await self._local_channel.subscribe(self._on_local_message)
|
|
141
150
|
|
|
142
|
-
async def send_decoded_command(
|
|
143
|
-
self,
|
|
144
|
-
method: CommandType,
|
|
145
|
-
*,
|
|
146
|
-
response_type: type[_T],
|
|
147
|
-
params: ParamsType = None,
|
|
148
|
-
) -> _T:
|
|
149
|
-
"""Send a command using the best available transport.
|
|
150
|
-
|
|
151
|
-
Will prefer local connection if available, falling back to MQTT.
|
|
152
|
-
"""
|
|
153
|
-
connection = "local" if self.is_local_connected else "mqtt"
|
|
154
|
-
_LOGGER.debug("Sending command (%s): %s, params=%s", connection, method, params)
|
|
155
|
-
if self._local_channel:
|
|
156
|
-
return await self._send_local_decoded_command(method, response_type=response_type, params=params)
|
|
157
|
-
return await self._send_mqtt_decoded_command(method, response_type=response_type, params=params)
|
|
158
|
-
|
|
159
|
-
async def _send_mqtt_raw_command(self, method: CommandType, params: ParamsType | None = None) -> dict[str, Any]:
|
|
160
|
-
"""Send a raw command and return a raw unparsed response."""
|
|
161
|
-
message = self._mqtt_payload_encoder(method, params)
|
|
162
|
-
_LOGGER.debug("Sending MQTT message for device %s: %s", self._device_uid, message)
|
|
163
|
-
response = await self._mqtt_channel.send_command(message)
|
|
164
|
-
return decode_rpc_response(response)
|
|
165
|
-
|
|
166
|
-
async def _send_mqtt_decoded_command(
|
|
167
|
-
self, method: CommandType, *, response_type: type[_T], params: ParamsType | None = None
|
|
168
|
-
) -> _T:
|
|
169
|
-
"""Send a command over MQTT and decode the response."""
|
|
170
|
-
decoded_response = await self._send_mqtt_raw_command(method, params)
|
|
171
|
-
return response_type.from_dict(decoded_response)
|
|
172
|
-
|
|
173
|
-
async def _send_local_raw_command(self, method: CommandType, params: ParamsType | None = None) -> dict[str, Any]:
|
|
174
|
-
"""Send a raw command over local connection."""
|
|
175
|
-
if not self._local_channel:
|
|
176
|
-
raise RoborockException("Local channel is not connected")
|
|
177
|
-
|
|
178
|
-
message = encode_local_payload(method, params)
|
|
179
|
-
_LOGGER.debug("Sending local message for device %s: %s", self._device_uid, message)
|
|
180
|
-
response = await self._local_channel.send_command(message)
|
|
181
|
-
return decode_rpc_response(response)
|
|
182
|
-
|
|
183
|
-
async def _send_local_decoded_command(
|
|
184
|
-
self, method: CommandType, *, response_type: type[_T], params: ParamsType | None = None
|
|
185
|
-
) -> _T:
|
|
186
|
-
"""Send a command over local connection and decode the response."""
|
|
187
|
-
if not self._local_channel:
|
|
188
|
-
raise RoborockException("Local channel is not connected")
|
|
189
|
-
decoded_response = await self._send_local_raw_command(method, params)
|
|
190
|
-
return response_type.from_dict(decoded_response)
|
|
191
|
-
|
|
192
151
|
def _on_mqtt_message(self, message: RoborockMessage) -> None:
|
|
193
152
|
"""Handle incoming MQTT messages."""
|
|
194
153
|
_LOGGER.debug("V1Channel received MQTT message from device %s: %s", self._device_uid, message)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""V1 Rpc Channel for Roborock devices.
|
|
2
|
+
|
|
3
|
+
This is a wrapper around the V1 channel that provides a higher level interface
|
|
4
|
+
for sending typed commands and receiving typed responses. This also provides
|
|
5
|
+
a simple interface for sending commands and receiving responses over both MQTT
|
|
6
|
+
and local connections, preferring local when available.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any, Protocol, TypeVar, overload
|
|
12
|
+
|
|
13
|
+
from roborock.containers import RoborockBase
|
|
14
|
+
from roborock.protocols.v1_protocol import (
|
|
15
|
+
CommandType,
|
|
16
|
+
ParamsType,
|
|
17
|
+
SecurityData,
|
|
18
|
+
create_mqtt_payload_encoder,
|
|
19
|
+
decode_rpc_response,
|
|
20
|
+
encode_local_payload,
|
|
21
|
+
)
|
|
22
|
+
from roborock.roborock_message import RoborockMessage
|
|
23
|
+
|
|
24
|
+
from .local_channel import LocalChannel
|
|
25
|
+
from .mqtt_channel import MqttChannel
|
|
26
|
+
|
|
27
|
+
_LOGGER = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_T = TypeVar("_T", bound=RoborockBase)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class V1RpcChannel(Protocol):
|
|
34
|
+
"""Protocol for V1 RPC channels.
|
|
35
|
+
|
|
36
|
+
This is a wrapper around a raw channel that provides a high-level interface
|
|
37
|
+
for sending commands and receiving responses.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@overload
|
|
41
|
+
async def send_command(
|
|
42
|
+
self,
|
|
43
|
+
method: CommandType,
|
|
44
|
+
*,
|
|
45
|
+
params: ParamsType = None,
|
|
46
|
+
) -> Any:
|
|
47
|
+
"""Send a command and return a decoded response."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
@overload
|
|
51
|
+
async def send_command(
|
|
52
|
+
self,
|
|
53
|
+
method: CommandType,
|
|
54
|
+
*,
|
|
55
|
+
response_type: type[_T],
|
|
56
|
+
params: ParamsType = None,
|
|
57
|
+
) -> _T:
|
|
58
|
+
"""Send a command and return a parsed response RoborockBase type."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BaseV1RpcChannel(V1RpcChannel):
|
|
63
|
+
"""Base implementation that provides the typed response logic."""
|
|
64
|
+
|
|
65
|
+
async def send_command(
|
|
66
|
+
self,
|
|
67
|
+
method: CommandType,
|
|
68
|
+
*,
|
|
69
|
+
response_type: type[_T] | None = None,
|
|
70
|
+
params: ParamsType = None,
|
|
71
|
+
) -> _T | Any:
|
|
72
|
+
"""Send a command and return either a decoded or parsed response."""
|
|
73
|
+
decoded_response = await self._send_raw_command(method, params=params)
|
|
74
|
+
|
|
75
|
+
if response_type is not None:
|
|
76
|
+
return response_type.from_dict(decoded_response)
|
|
77
|
+
return decoded_response
|
|
78
|
+
|
|
79
|
+
async def _send_raw_command(
|
|
80
|
+
self,
|
|
81
|
+
method: CommandType,
|
|
82
|
+
*,
|
|
83
|
+
params: ParamsType = None,
|
|
84
|
+
) -> Any:
|
|
85
|
+
"""Send a raw command and return the decoded response. Must be implemented by subclasses."""
|
|
86
|
+
raise NotImplementedError
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CombinedV1RpcChannel(BaseV1RpcChannel):
|
|
90
|
+
"""A V1 RPC channel that can use both local and MQTT channels, preferring local when available."""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self, local_channel: LocalChannel, local_rpc_channel: V1RpcChannel, mqtt_channel: V1RpcChannel
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Initialize the combined channel with local and MQTT channels."""
|
|
96
|
+
self._local_channel = local_channel
|
|
97
|
+
self._local_rpc_channel = local_rpc_channel
|
|
98
|
+
self._mqtt_rpc_channel = mqtt_channel
|
|
99
|
+
|
|
100
|
+
async def _send_raw_command(
|
|
101
|
+
self,
|
|
102
|
+
method: CommandType,
|
|
103
|
+
*,
|
|
104
|
+
params: ParamsType = None,
|
|
105
|
+
) -> Any:
|
|
106
|
+
"""Send a command and return a parsed response RoborockBase type."""
|
|
107
|
+
if self._local_channel.is_connected:
|
|
108
|
+
return await self._local_rpc_channel.send_command(method, params=params)
|
|
109
|
+
return await self._mqtt_rpc_channel.send_command(method, params=params)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PayloadEncodedV1RpcChannel(BaseV1RpcChannel):
|
|
113
|
+
"""Protocol for V1 channels that send encoded commands."""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
name: str,
|
|
118
|
+
channel: MqttChannel | LocalChannel,
|
|
119
|
+
payload_encoder: Callable[[CommandType, ParamsType], RoborockMessage],
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Initialize the channel with a raw channel and an encoder function."""
|
|
122
|
+
self._name = name
|
|
123
|
+
self._channel = channel
|
|
124
|
+
self._payload_encoder = payload_encoder
|
|
125
|
+
|
|
126
|
+
async def _send_raw_command(
|
|
127
|
+
self,
|
|
128
|
+
method: CommandType,
|
|
129
|
+
*,
|
|
130
|
+
params: ParamsType = None,
|
|
131
|
+
) -> Any:
|
|
132
|
+
"""Send a command and return a parsed response RoborockBase type."""
|
|
133
|
+
_LOGGER.debug("Sending command (%s): %s, params=%s", self._name, method, params)
|
|
134
|
+
message = self._payload_encoder(method, params)
|
|
135
|
+
response = await self._channel.send_message(message)
|
|
136
|
+
return decode_rpc_response(response)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def create_mqtt_rpc_channel(mqtt_channel: MqttChannel, security_data: SecurityData) -> V1RpcChannel:
|
|
140
|
+
"""Create a V1 RPC channel using an MQTT channel."""
|
|
141
|
+
payload_encoder = create_mqtt_payload_encoder(security_data)
|
|
142
|
+
return PayloadEncodedV1RpcChannel("mqtt", mqtt_channel, payload_encoder)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def create_combined_rpc_channel(local_channel: LocalChannel, mqtt_rpc_channel: V1RpcChannel) -> V1RpcChannel:
|
|
146
|
+
"""Create a V1 RPC channel that combines local and MQTT channels."""
|
|
147
|
+
local_rpc_channel = PayloadEncodedV1RpcChannel("local", local_channel, encode_local_payload)
|
|
148
|
+
return CombinedV1RpcChannel(local_channel, local_rpc_channel, mqtt_rpc_channel)
|
|
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.27.0 → python_roborock-2.28.0}/roborock/version_1_apis/roborock_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.27.0 → python_roborock-2.28.0}/roborock/version_a01_apis/roborock_client_a01.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|