python-roborock 2.26.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.
Files changed (42) hide show
  1. {python_roborock-2.26.0 → python_roborock-2.28.0}/PKG-INFO +1 -1
  2. {python_roborock-2.26.0 → python_roborock-2.28.0}/pyproject.toml +1 -1
  3. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/cli.py +10 -1
  4. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/code_mappings.py +63 -0
  5. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/containers.py +8 -138
  6. python_roborock-2.28.0/roborock/device_features.py +365 -0
  7. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/devices/device.py +33 -21
  8. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/devices/device_manager.py +11 -4
  9. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/devices/local_channel.py +29 -2
  10. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/devices/mqtt_channel.py +1 -1
  11. python_roborock-2.28.0/roborock/devices/v1_channel.py +171 -0
  12. python_roborock-2.28.0/roborock/devices/v1_rpc_channel.py +148 -0
  13. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/protocols/v1_protocol.py +58 -0
  14. {python_roborock-2.26.0 → python_roborock-2.28.0}/LICENSE +0 -0
  15. {python_roborock-2.26.0 → python_roborock-2.28.0}/README.md +0 -0
  16. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/__init__.py +0 -0
  17. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/api.py +0 -0
  18. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/cloud_api.py +0 -0
  19. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/command_cache.py +0 -0
  20. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/const.py +0 -0
  21. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/devices/README.md +0 -0
  22. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/devices/__init__.py +0 -0
  23. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/exceptions.py +0 -0
  24. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/local_api.py +0 -0
  25. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/mqtt/__init__.py +0 -0
  26. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/mqtt/roborock_session.py +0 -0
  27. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/mqtt/session.py +0 -0
  28. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/protocol.py +0 -0
  29. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/protocols/a01_protocol.py +0 -0
  30. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/py.typed +0 -0
  31. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/roborock_future.py +0 -0
  32. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/roborock_message.py +0 -0
  33. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/roborock_typing.py +0 -0
  34. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/util.py +0 -0
  35. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/version_1_apis/__init__.py +0 -0
  36. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  37. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  38. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  39. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/version_a01_apis/__init__.py +0 -0
  40. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  41. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  42. {python_roborock-2.26.0 → python_roborock-2.28.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-roborock
3
- Version: 2.26.0
3
+ Version: 2.28.0
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Home-page: https://github.com/humbertogontijo/python-roborock
6
6
  License: GPL-3.0-only
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-roborock"
3
- version = "2.26.0"
3
+ version = "2.28.0"
4
4
  description = "A package to control Roborock vacuums."
5
5
  authors = ["humbertogontijo <humbertogontijo@users.noreply.github.com>"]
6
6
  license = "GPL-3.0-only"
@@ -115,7 +115,16 @@ async def session(ctx, duration: int):
115
115
  devices = await device_manager.get_devices()
116
116
  click.echo(f"Discovered devices: {', '.join([device.name for device in devices])}")
117
117
 
118
- click.echo("MQTT session started. Listening for messages...")
118
+ click.echo("MQTT session started. Querying devices...")
119
+ for device in devices:
120
+ try:
121
+ status = await device.get_status()
122
+ except RoborockException as e:
123
+ click.echo(f"Failed to get status for {device.name}: {e}")
124
+ else:
125
+ click.echo(f"Device {device.name} status: {status.as_dict()}")
126
+
127
+ click.echo("Listening for messages.")
119
128
  await asyncio.sleep(duration)
120
129
 
121
130
  # Close the device manager (this will close all devices and MQTT session)
@@ -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]