roborock-cli 0.1.1__py3-none-any.whl

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 (106) hide show
  1. roborock_cli/__init__.py +3 -0
  2. roborock_cli/__main__.py +76 -0
  3. roborock_cli/_vendor/VERSION +6 -0
  4. roborock_cli/_vendor/__init__.py +0 -0
  5. roborock_cli/_vendor/roborock/__init__.py +27 -0
  6. roborock_cli/_vendor/roborock/broadcast_protocol.py +114 -0
  7. roborock_cli/_vendor/roborock/callbacks.py +130 -0
  8. roborock_cli/_vendor/roborock/cli.py +1338 -0
  9. roborock_cli/_vendor/roborock/const.py +84 -0
  10. roborock_cli/_vendor/roborock/data/__init__.py +9 -0
  11. roborock_cli/_vendor/roborock/data/b01_q10/__init__.py +2 -0
  12. roborock_cli/_vendor/roborock/data/b01_q10/b01_q10_code_mappings.py +213 -0
  13. roborock_cli/_vendor/roborock/data/b01_q10/b01_q10_containers.py +102 -0
  14. roborock_cli/_vendor/roborock/data/b01_q7/__init__.py +2 -0
  15. roborock_cli/_vendor/roborock/data/b01_q7/b01_q7_code_mappings.py +303 -0
  16. roborock_cli/_vendor/roborock/data/b01_q7/b01_q7_containers.py +302 -0
  17. roborock_cli/_vendor/roborock/data/code_mappings.py +198 -0
  18. roborock_cli/_vendor/roborock/data/containers.py +530 -0
  19. roborock_cli/_vendor/roborock/data/dyad/__init__.py +2 -0
  20. roborock_cli/_vendor/roborock/data/dyad/dyad_code_mappings.py +102 -0
  21. roborock_cli/_vendor/roborock/data/dyad/dyad_containers.py +28 -0
  22. roborock_cli/_vendor/roborock/data/v1/__init__.py +3 -0
  23. roborock_cli/_vendor/roborock/data/v1/v1_clean_modes.py +192 -0
  24. roborock_cli/_vendor/roborock/data/v1/v1_code_mappings.py +644 -0
  25. roborock_cli/_vendor/roborock/data/v1/v1_containers.py +800 -0
  26. roborock_cli/_vendor/roborock/data/zeo/__init__.py +2 -0
  27. roborock_cli/_vendor/roborock/data/zeo/zeo_code_mappings.py +138 -0
  28. roborock_cli/_vendor/roborock/data/zeo/zeo_containers.py +0 -0
  29. roborock_cli/_vendor/roborock/device_features.py +668 -0
  30. roborock_cli/_vendor/roborock/devices/README.md +41 -0
  31. roborock_cli/_vendor/roborock/devices/__init__.py +11 -0
  32. roborock_cli/_vendor/roborock/devices/cache.py +143 -0
  33. roborock_cli/_vendor/roborock/devices/device.py +240 -0
  34. roborock_cli/_vendor/roborock/devices/device_manager.py +269 -0
  35. roborock_cli/_vendor/roborock/devices/file_cache.py +79 -0
  36. roborock_cli/_vendor/roborock/devices/rpc/__init__.py +14 -0
  37. roborock_cli/_vendor/roborock/devices/rpc/a01_channel.py +94 -0
  38. roborock_cli/_vendor/roborock/devices/rpc/b01_q10_channel.py +57 -0
  39. roborock_cli/_vendor/roborock/devices/rpc/b01_q7_channel.py +101 -0
  40. roborock_cli/_vendor/roborock/devices/rpc/v1_channel.py +457 -0
  41. roborock_cli/_vendor/roborock/devices/traits/__init__.py +28 -0
  42. roborock_cli/_vendor/roborock/devices/traits/a01/__init__.py +191 -0
  43. roborock_cli/_vendor/roborock/devices/traits/b01/__init__.py +12 -0
  44. roborock_cli/_vendor/roborock/devices/traits/b01/q10/__init__.py +76 -0
  45. roborock_cli/_vendor/roborock/devices/traits/b01/q10/command.py +32 -0
  46. roborock_cli/_vendor/roborock/devices/traits/b01/q10/common.py +115 -0
  47. roborock_cli/_vendor/roborock/devices/traits/b01/q10/status.py +32 -0
  48. roborock_cli/_vendor/roborock/devices/traits/b01/q10/vacuum.py +81 -0
  49. roborock_cli/_vendor/roborock/devices/traits/b01/q7/__init__.py +136 -0
  50. roborock_cli/_vendor/roborock/devices/traits/b01/q7/clean_summary.py +75 -0
  51. roborock_cli/_vendor/roborock/devices/traits/traits_mixin.py +64 -0
  52. roborock_cli/_vendor/roborock/devices/traits/v1/__init__.py +344 -0
  53. roborock_cli/_vendor/roborock/devices/traits/v1/child_lock.py +29 -0
  54. roborock_cli/_vendor/roborock/devices/traits/v1/clean_summary.py +83 -0
  55. roborock_cli/_vendor/roborock/devices/traits/v1/command.py +38 -0
  56. roborock_cli/_vendor/roborock/devices/traits/v1/common.py +172 -0
  57. roborock_cli/_vendor/roborock/devices/traits/v1/consumeable.py +48 -0
  58. roborock_cli/_vendor/roborock/devices/traits/v1/device_features.py +74 -0
  59. roborock_cli/_vendor/roborock/devices/traits/v1/do_not_disturb.py +41 -0
  60. roborock_cli/_vendor/roborock/devices/traits/v1/dust_collection_mode.py +13 -0
  61. roborock_cli/_vendor/roborock/devices/traits/v1/flow_led_status.py +29 -0
  62. roborock_cli/_vendor/roborock/devices/traits/v1/home.py +285 -0
  63. roborock_cli/_vendor/roborock/devices/traits/v1/led_status.py +43 -0
  64. roborock_cli/_vendor/roborock/devices/traits/v1/map_content.py +83 -0
  65. roborock_cli/_vendor/roborock/devices/traits/v1/maps.py +80 -0
  66. roborock_cli/_vendor/roborock/devices/traits/v1/network_info.py +55 -0
  67. roborock_cli/_vendor/roborock/devices/traits/v1/rooms.py +105 -0
  68. roborock_cli/_vendor/roborock/devices/traits/v1/routines.py +26 -0
  69. roborock_cli/_vendor/roborock/devices/traits/v1/smart_wash_params.py +13 -0
  70. roborock_cli/_vendor/roborock/devices/traits/v1/status.py +101 -0
  71. roborock_cli/_vendor/roborock/devices/traits/v1/valley_electricity_timer.py +44 -0
  72. roborock_cli/_vendor/roborock/devices/traits/v1/volume.py +27 -0
  73. roborock_cli/_vendor/roborock/devices/traits/v1/wash_towel_mode.py +13 -0
  74. roborock_cli/_vendor/roborock/devices/transport/__init__.py +8 -0
  75. roborock_cli/_vendor/roborock/devices/transport/channel.py +32 -0
  76. roborock_cli/_vendor/roborock/devices/transport/local_channel.py +295 -0
  77. roborock_cli/_vendor/roborock/devices/transport/mqtt_channel.py +118 -0
  78. roborock_cli/_vendor/roborock/diagnostics.py +166 -0
  79. roborock_cli/_vendor/roborock/exceptions.py +95 -0
  80. roborock_cli/_vendor/roborock/map/__init__.py +7 -0
  81. roborock_cli/_vendor/roborock/map/map_parser.py +123 -0
  82. roborock_cli/_vendor/roborock/mqtt/__init__.py +10 -0
  83. roborock_cli/_vendor/roborock/mqtt/health_manager.py +60 -0
  84. roborock_cli/_vendor/roborock/mqtt/roborock_session.py +463 -0
  85. roborock_cli/_vendor/roborock/mqtt/session.py +108 -0
  86. roborock_cli/_vendor/roborock/protocol.py +558 -0
  87. roborock_cli/_vendor/roborock/protocols/__init__.py +3 -0
  88. roborock_cli/_vendor/roborock/protocols/a01_protocol.py +74 -0
  89. roborock_cli/_vendor/roborock/protocols/b01_q10_protocol.py +87 -0
  90. roborock_cli/_vendor/roborock/protocols/b01_q7_protocol.py +81 -0
  91. roborock_cli/_vendor/roborock/protocols/v1_protocol.py +271 -0
  92. roborock_cli/_vendor/roborock/py.typed +0 -0
  93. roborock_cli/_vendor/roborock/roborock_message.py +246 -0
  94. roborock_cli/_vendor/roborock/roborock_typing.py +382 -0
  95. roborock_cli/_vendor/roborock/util.py +54 -0
  96. roborock_cli/_vendor/roborock/web_api.py +761 -0
  97. roborock_cli/cli.py +715 -0
  98. roborock_cli/connection.py +202 -0
  99. roborock_cli/helpers.py +71 -0
  100. roborock_cli/server.py +759 -0
  101. roborock_cli/setup_auth.py +92 -0
  102. roborock_cli-0.1.1.dist-info/METADATA +172 -0
  103. roborock_cli-0.1.1.dist-info/RECORD +106 -0
  104. roborock_cli-0.1.1.dist-info/WHEEL +4 -0
  105. roborock_cli-0.1.1.dist-info/entry_points.txt +2 -0
  106. roborock_cli-0.1.1.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,668 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field, fields
4
+ from enum import IntEnum, StrEnum
5
+ from typing import Any
6
+
7
+ from roborock_cli._vendor.roborock.data.code_mappings import RoborockProductNickname
8
+ from roborock_cli._vendor.roborock.data.containers import RoborockBase
9
+ from roborock_cli._vendor.roborock.data.v1 import RoborockDockTypeCode
10
+
11
+
12
+ class NewFeatureStrBit(IntEnum):
13
+ TWO_KEY_REAL_TIME_VIDEO = 32
14
+ TWO_KEY_RTV_IN_CHARGING = 33
15
+ DIRTY_REPLENISH_CLEAN = 34
16
+ AUTO_DELIVERY_FIELD_IN_GLOBAL_STATUS = 35
17
+ AVOID_COLLISION_MODE = 36
18
+ VOICE_CONTROL = 37
19
+ NEW_ENDPOINT = 38
20
+ PUMPING_WATER = 39
21
+ CORNER_MOP_STRETCH = 40
22
+ HOT_WASH_TOWEL = 41
23
+ FLOOR_DIR_CLEAN_ANY_TIME = 42
24
+ PET_SUPPLIES_DEEP_CLEAN = 43
25
+ MOP_SHAKE_WATER_MAX = 45
26
+ EXACT_CUSTOM_MODE = 47
27
+ VIDEO_PATROL = 48
28
+ CARPET_CUSTOM_CLEAN = 49
29
+ PET_SNAPSHOT = 50
30
+ CUSTOM_CLEAN_MODE_COUNT = 51
31
+ NEW_AI_RECOGNITION = 52
32
+ AUTO_COLLECTION_2 = 53
33
+ RIGHT_BRUSH_STRETCH = 54
34
+ SMART_CLEAN_MODE_SET = 55
35
+ DIRTY_OBJECT_DETECT = 56
36
+ NO_NEED_CARPET_PRESS_SET = 57
37
+ VOICE_CONTROL_LED = 58
38
+ WATER_LEAK_CHECK = 60
39
+ MIN_BATTERY_15_TO_CLEAN_TASK = 62
40
+ GAP_DEEP_CLEAN = 63
41
+ OBJECT_DETECT_CHECK = 64
42
+ IDENTIFY_ROOM = 66
43
+ MATTER = 67
44
+ WORKDAY_HOLIDAY = 69
45
+ CLEAN_DIRECT_STATUS = 70
46
+ MAP_ERASER = 71
47
+ OPTIMIZE_BATTERY = 72
48
+ ACTIVATE_VIDEO_CHARGING_AND_STANDBY = 73
49
+ CARPET_LONG_HAIRED = 75
50
+ CLEAN_HISTORY_TIME_LINE = 76
51
+ MAX_ZONE_OPENED = 77
52
+ EXHIBITION_FUNCTION = 78
53
+ LDS_LIFTING = 79
54
+ AUTO_TEAR_DOWN_MOP = 80
55
+ SMALL_SIDE_MOP = 81
56
+ SUPPORT_SIDE_BRUSH_UP_DOWN = 82
57
+ DRY_INTERVAL_TIMER = 83
58
+ UVC_STERILIZE = 84
59
+ MIDWAY_BACK_TO_DOCK = 85
60
+ SUPPORT_MAIN_BRUSH_UP_DOWN = 86
61
+ EGG_DANCE_MODE = 87
62
+ MECHANICAL_ARM_MODE = 89
63
+ TIDYUP_ZONES = MECHANICAL_ARM_MODE
64
+ CLEAN_TIME_LINE = 91
65
+ CLEAN_THEN_MOP_MODE = 93
66
+ TYPE_IDENTIFY = 94
67
+ SUPPORT_GET_PARTICULAR_STATUS = 96
68
+ THREE_D_MAPPING_INNER_TEST = 97
69
+ SYNC_SERVER_NAME = 98
70
+ SHOULD_SHOW_ARM_OVER_LOAD = 99
71
+ COLLECT_DUST_COUNT_SHOW = 100
72
+ SUPPORT_API_APP_STOP_GRASP = 101
73
+ CTM_WITH_REPEAT = 102
74
+ SIDE_BRUSH_LIFT_CARPET = 104
75
+ DETECT_WIRE_CARPET = 105
76
+ WATER_SLIDE_MODE = 106
77
+ SOAK_AND_WASH = 107
78
+ CLEAN_EFFICIENCY = 108
79
+ BACK_WASH_NEW_SMART = 109
80
+ DUAL_BAND_WI_FI = 110
81
+ PROGRAM_MODE = 111
82
+ CLEAN_FLUID_DELIVERY = 112
83
+ CARPET_LONG_HAIRED_EX = 113
84
+ OVER_SEA_CTM = 114
85
+ FULL_DUPLES_SWITCH = 115
86
+ LOW_AREA_ACCESS = 116
87
+ FOLLOW_LOW_OBS = 117
88
+ TWO_GEARS_NO_COLLISION = 118
89
+ CARPET_SHAPE_TYPE = 119
90
+ SR_MAP = 120
91
+
92
+
93
+ class ProductFeatures(StrEnum):
94
+ REMOTE_BACK = "remote_back"
95
+ CLEANMODE_MAXPLUS = "cleanmode_maxplus"
96
+ CLEANMODE_PURECLEANMOP = "cleanmode_purecleanmop"
97
+ CLEANMODE_NONE_PURECLEANMOP_WITH_MAXPLUS = "cleanmode_none_purecleanmop_with_maxplus"
98
+ MOP_ELECTRONIC_MODULE = "mop_electronic_module"
99
+ MOP_SHAKE_MODULE = "mop_shake_module"
100
+ MOP_SPIN_MODULE = "mop_spin_module"
101
+ DEFAULT_MAP3D = "map3d"
102
+ DEFAULT_CLEANMODECUSTOM = "custom_cleanmode"
103
+ REALTIMEVIDEO = "realtimevideo"
104
+ REALTIMEVIDEO_LIVECALL = "realtimevideo_livecall"
105
+ REALTIMEVIDEO_RECORDANDSHORTCUT = "realtimevideo_livecall"
106
+ CAMERA_SINGLELINE = "camera_singleline"
107
+ CAMERA_DUALLINE = "camera_dualline"
108
+ CAMERA_RGB = "camera_rgb"
109
+ CAMERA_DOUBLERGB = "camera_doublergb"
110
+ AIRECOGNITION_SETTING = "airecognition_setting"
111
+ AIRECOGNITION_SCENE = "airecognition_scene"
112
+ AIRECOGNITION_PET = "airecognition_pet"
113
+ AIRECOGNITION_OBSTACLE = "airecognition_obstacle"
114
+
115
+
116
+ # The following combinations are pulled directly from decompiled source code.
117
+ AIRECOGNITION_OBSTACLE = [ProductFeatures.AIRECOGNITION_OBSTACLE]
118
+ RGB_CAMERA_FEATURES = [
119
+ ProductFeatures.CAMERA_RGB,
120
+ ProductFeatures.AIRECOGNITION_SETTING,
121
+ ProductFeatures.AIRECOGNITION_SCENE,
122
+ ProductFeatures.AIRECOGNITION_PET,
123
+ ProductFeatures.AIRECOGNITION_OBSTACLE,
124
+ ProductFeatures.REALTIMEVIDEO,
125
+ ProductFeatures.REALTIMEVIDEO_LIVECALL,
126
+ ProductFeatures.REALTIMEVIDEO_RECORDANDSHORTCUT,
127
+ ]
128
+ DOUBLE_RGB_CAMERA_FEATURES = [
129
+ ProductFeatures.CAMERA_DOUBLERGB,
130
+ ProductFeatures.AIRECOGNITION_SETTING,
131
+ ProductFeatures.AIRECOGNITION_PET,
132
+ ProductFeatures.AIRECOGNITION_OBSTACLE,
133
+ ProductFeatures.REALTIMEVIDEO,
134
+ ]
135
+ SINGLE_LINE_CAMERA_FEATURES = [
136
+ ProductFeatures.CAMERA_SINGLELINE,
137
+ ProductFeatures.AIRECOGNITION_SETTING,
138
+ ProductFeatures.AIRECOGNITION_OBSTACLE,
139
+ ]
140
+ DUAL_LINE_CAMERA_FEATURES = [
141
+ ProductFeatures.CAMERA_DUALLINE,
142
+ ProductFeatures.AIRECOGNITION_SETTING,
143
+ ProductFeatures.AIRECOGNITION_OBSTACLE,
144
+ ProductFeatures.AIRECOGNITION_PET,
145
+ ]
146
+
147
+ NEW_DEFAULT_FEATURES = [ProductFeatures.REMOTE_BACK, ProductFeatures.CLEANMODE_MAXPLUS]
148
+
149
+
150
+ PEARL_FEATURES = SINGLE_LINE_CAMERA_FEATURES + [ProductFeatures.CLEANMODE_MAXPLUS, ProductFeatures.MOP_SPIN_MODULE]
151
+ PEARL_PLUS_FEATURES = NEW_DEFAULT_FEATURES + RGB_CAMERA_FEATURES + [ProductFeatures.MOP_SPIN_MODULE]
152
+ ULTRON_FEATURES = NEW_DEFAULT_FEATURES + DUAL_LINE_CAMERA_FEATURES + [ProductFeatures.MOP_SHAKE_MODULE]
153
+ ULTRONSV_FEATURES = NEW_DEFAULT_FEATURES + RGB_CAMERA_FEATURES + [ProductFeatures.MOP_SHAKE_MODULE]
154
+ TANOSS_FEATURES = [ProductFeatures.REMOTE_BACK, ProductFeatures.MOP_SHAKE_MODULE]
155
+ TOPAZSPOWER_FEATURES = [ProductFeatures.CLEANMODE_MAXPLUS, ProductFeatures.MOP_SHAKE_MODULE]
156
+
157
+ PRODUCTS_WITHOUT_CUSTOM_CLEAN: set[RoborockProductNickname] = {
158
+ RoborockProductNickname.TANOS,
159
+ RoborockProductNickname.RUBYPLUS,
160
+ RoborockProductNickname.RUBYSC,
161
+ RoborockProductNickname.RUBYSE,
162
+ }
163
+ PRODUCTS_WITHOUT_DEFAULT_3D_MAP: set[RoborockProductNickname] = {
164
+ RoborockProductNickname.TANOS,
165
+ RoborockProductNickname.TANOSSPLUS,
166
+ RoborockProductNickname.TANOSE,
167
+ RoborockProductNickname.TANOSV,
168
+ RoborockProductNickname.RUBYPLUS,
169
+ RoborockProductNickname.RUBYSC,
170
+ RoborockProductNickname.RUBYSE,
171
+ }
172
+ PRODUCTS_WITHOUT_PURE_CLEAN_MOP: set[RoborockProductNickname] = {
173
+ RoborockProductNickname.TANOS,
174
+ RoborockProductNickname.TANOSE,
175
+ RoborockProductNickname.TANOSV,
176
+ RoborockProductNickname.TANOSSLITE,
177
+ RoborockProductNickname.TANOSSE,
178
+ RoborockProductNickname.TANOSSC,
179
+ RoborockProductNickname.ULTRONLITE,
180
+ RoborockProductNickname.ULTRONE,
181
+ RoborockProductNickname.RUBYPLUS,
182
+ RoborockProductNickname.RUBYSLITE,
183
+ RoborockProductNickname.RUBYSC,
184
+ RoborockProductNickname.RUBYSE,
185
+ }
186
+
187
+ # Base map containing the initial, unconditional features for each product.
188
+ _BASE_PRODUCT_FEATURE_MAP: dict[RoborockProductNickname, list[ProductFeatures]] = {
189
+ RoborockProductNickname.PEARL: PEARL_FEATURES,
190
+ RoborockProductNickname.PEARLS: PEARL_FEATURES,
191
+ RoborockProductNickname.PEARLPLUS: PEARL_PLUS_FEATURES,
192
+ RoborockProductNickname.VIVIAN: PEARL_PLUS_FEATURES,
193
+ RoborockProductNickname.CORAL: PEARL_PLUS_FEATURES,
194
+ RoborockProductNickname.ULTRON: ULTRON_FEATURES,
195
+ RoborockProductNickname.ULTRONE: [ProductFeatures.CLEANMODE_NONE_PURECLEANMOP_WITH_MAXPLUS],
196
+ RoborockProductNickname.ULTRONSV: ULTRONSV_FEATURES,
197
+ RoborockProductNickname.TOPAZSPOWER: TOPAZSPOWER_FEATURES,
198
+ RoborockProductNickname.TANOSS: TANOSS_FEATURES,
199
+ RoborockProductNickname.PEARLC: PEARL_FEATURES,
200
+ RoborockProductNickname.PEARLPLUSS: PEARL_PLUS_FEATURES,
201
+ RoborockProductNickname.PEARLSLITE: PEARL_FEATURES,
202
+ RoborockProductNickname.PEARLE: PEARL_FEATURES,
203
+ RoborockProductNickname.PEARLELITE: PEARL_FEATURES,
204
+ RoborockProductNickname.VIVIANC: [ProductFeatures.CLEANMODE_MAXPLUS, ProductFeatures.MOP_SPIN_MODULE]
205
+ + SINGLE_LINE_CAMERA_FEATURES,
206
+ RoborockProductNickname.CORALPRO: PEARL_PLUS_FEATURES,
207
+ RoborockProductNickname.ULTRONLITE: SINGLE_LINE_CAMERA_FEATURES
208
+ + [ProductFeatures.CLEANMODE_NONE_PURECLEANMOP_WITH_MAXPLUS, ProductFeatures.MOP_ELECTRONIC_MODULE],
209
+ RoborockProductNickname.ULTRONSC: ULTRON_FEATURES,
210
+ RoborockProductNickname.ULTRONSE: [
211
+ ProductFeatures.CLEANMODE_NONE_PURECLEANMOP_WITH_MAXPLUS,
212
+ ProductFeatures.MOP_ELECTRONIC_MODULE,
213
+ ],
214
+ RoborockProductNickname.ULTRONSPLUS: ULTRON_FEATURES,
215
+ RoborockProductNickname.VERDELITE: ULTRONSV_FEATURES,
216
+ RoborockProductNickname.TOPAZS: [ProductFeatures.REMOTE_BACK, ProductFeatures.MOP_SHAKE_MODULE],
217
+ RoborockProductNickname.TOPAZSPLUS: NEW_DEFAULT_FEATURES
218
+ + DUAL_LINE_CAMERA_FEATURES
219
+ + [ProductFeatures.MOP_SHAKE_MODULE],
220
+ RoborockProductNickname.TOPAZSC: TOPAZSPOWER_FEATURES + SINGLE_LINE_CAMERA_FEATURES,
221
+ RoborockProductNickname.TOPAZSV: NEW_DEFAULT_FEATURES + RGB_CAMERA_FEATURES + [ProductFeatures.MOP_SHAKE_MODULE],
222
+ RoborockProductNickname.TANOSSPLUS: TANOSS_FEATURES + DUAL_LINE_CAMERA_FEATURES,
223
+ RoborockProductNickname.TANOSSLITE: [ProductFeatures.MOP_ELECTRONIC_MODULE],
224
+ RoborockProductNickname.TANOSSC: [],
225
+ RoborockProductNickname.TANOSSE: [],
226
+ RoborockProductNickname.TANOSSMAX: NEW_DEFAULT_FEATURES
227
+ + DUAL_LINE_CAMERA_FEATURES
228
+ + [ProductFeatures.MOP_SHAKE_MODULE],
229
+ RoborockProductNickname.TANOS: [ProductFeatures.REMOTE_BACK],
230
+ RoborockProductNickname.TANOSE: [ProductFeatures.MOP_ELECTRONIC_MODULE, ProductFeatures.REMOTE_BACK],
231
+ RoborockProductNickname.TANOSV: DOUBLE_RGB_CAMERA_FEATURES
232
+ + [ProductFeatures.REMOTE_BACK, ProductFeatures.MOP_ELECTRONIC_MODULE],
233
+ RoborockProductNickname.RUBYPLUS: [],
234
+ RoborockProductNickname.RUBYSC: [],
235
+ RoborockProductNickname.RUBYSE: [],
236
+ RoborockProductNickname.RUBYSLITE: [ProductFeatures.MOP_ELECTRONIC_MODULE],
237
+ }
238
+
239
+ PRODUCT_FEATURE_MAP: dict[RoborockProductNickname, list[ProductFeatures]] = {
240
+ product: (
241
+ features
242
+ + ([ProductFeatures.DEFAULT_CLEANMODECUSTOM] if product not in PRODUCTS_WITHOUT_CUSTOM_CLEAN else [])
243
+ + ([ProductFeatures.DEFAULT_MAP3D] if product not in PRODUCTS_WITHOUT_DEFAULT_3D_MAP else [])
244
+ + ([ProductFeatures.CLEANMODE_PURECLEANMOP] if product not in PRODUCTS_WITHOUT_PURE_CLEAN_MOP else [])
245
+ )
246
+ for product, features in _BASE_PRODUCT_FEATURE_MAP.items()
247
+ }
248
+
249
+
250
+ @dataclass
251
+ class DeviceFeatures(RoborockBase):
252
+ """Represents the features supported by a Roborock device."""
253
+
254
+ # Features from robot_new_features (lower 32 bits)
255
+ is_show_clean_finish_reason_supported: bool = field(metadata={"robot_new_features": 1})
256
+ is_re_segment_supported: bool = field(metadata={"robot_new_features": 4})
257
+ is_video_monitor_supported: bool = field(metadata={"robot_new_features": 8})
258
+ is_any_state_transit_goto_supported: bool = field(metadata={"robot_new_features": 16})
259
+ is_fw_filter_obstacle_supported: bool = field(metadata={"robot_new_features": 32})
260
+ is_video_setting_supported: bool = field(metadata={"robot_new_features": 64})
261
+ is_ignore_unknown_map_object_supported: bool = field(metadata={"robot_new_features": 128})
262
+ is_set_child_supported: bool = field(metadata={"robot_new_features": 256})
263
+ is_carpet_supported: bool = field(metadata={"robot_new_features": 512})
264
+ is_record_allowed: bool = field(metadata={"robot_new_features": 1024})
265
+ is_mop_path_supported: bool = field(metadata={"robot_new_features": 2048})
266
+ is_multi_map_segment_timer_supported: bool = field(metadata={"robot_new_features": 4096})
267
+ is_current_map_restore_enabled: bool = field(metadata={"robot_new_features": 8192})
268
+ is_room_name_supported: bool = field(metadata={"robot_new_features": 16384})
269
+ is_shake_mop_set_supported: bool = field(metadata={"robot_new_features": 262144})
270
+ is_map_beautify_internal_debug_supported: bool = field(metadata={"robot_new_features": 2097152})
271
+ is_new_data_for_clean_history: bool = field(metadata={"robot_new_features": 4194304})
272
+ is_new_data_for_clean_history_detail: bool = field(metadata={"robot_new_features": 8388608})
273
+ is_flow_led_setting_supported: bool = field(metadata={"robot_new_features": 16777216})
274
+ is_dust_collection_setting_supported: bool = field(metadata={"robot_new_features": 33554432})
275
+ is_rpc_retry_supported: bool = field(metadata={"robot_new_features": 67108864})
276
+ is_avoid_collision_supported: bool = field(metadata={"robot_new_features": 134217728})
277
+ is_support_set_switch_map_mode: bool = field(metadata={"robot_new_features": 268435456})
278
+ is_map_carpet_add_support: bool = field(metadata={"robot_new_features": 1073741824})
279
+ is_custom_water_box_distance_supported: bool = field(metadata={"robot_new_features": 2147483648})
280
+
281
+ # Features from robot_new_features (upper 32 bits)
282
+ is_support_smart_scene: bool = field(metadata={"upper_32_bits": 1})
283
+ is_support_floor_edit: bool = field(metadata={"upper_32_bits": 3})
284
+ is_support_furniture: bool = field(metadata={"upper_32_bits": 4})
285
+ is_wash_then_charge_cmd_supported: bool = field(metadata={"upper_32_bits": 5})
286
+ is_support_room_tag: bool = field(metadata={"upper_32_bits": 6})
287
+ is_support_quick_map_builder: bool = field(metadata={"upper_32_bits": 7})
288
+ is_support_smart_global_clean_with_custom_mode: bool = field(metadata={"upper_32_bits": 8})
289
+ is_careful_slow_mop_supported: bool = field(metadata={"upper_32_bits": 9})
290
+ is_egg_mode_supported_from_new_features: bool = field(metadata={"upper_32_bits": 10})
291
+ is_carpet_show_on_map: bool = field(metadata={"upper_32_bits": 12})
292
+ is_supported_valley_electricity: bool = field(metadata={"upper_32_bits": 13})
293
+ is_unsave_map_reason_supported: bool = field(metadata={"upper_32_bits": 14})
294
+ is_supported_drying: bool = field(metadata={"upper_32_bits": 15})
295
+ is_supported_download_test_voice: bool = field(metadata={"upper_32_bits": 16})
296
+ is_support_backup_map: bool = field(metadata={"upper_32_bits": 17})
297
+ is_support_custom_mode_in_cleaning: bool = field(metadata={"upper_32_bits": 18})
298
+ is_support_remote_control_in_call: bool = field(metadata={"upper_32_bits": 19})
299
+
300
+ # Features from new_feature_info_str (masking last 8 chars / 32 bits)
301
+ is_support_set_volume_in_call: bool = field(metadata={"new_feature_str_mask": (1, 8)})
302
+ is_support_clean_estimate: bool = field(metadata={"new_feature_str_mask": (2, 8)})
303
+ is_support_custom_dnd: bool = field(metadata={"new_feature_str_mask": (4, 8)})
304
+ is_carpet_deep_clean_supported: bool = field(metadata={"new_feature_str_mask": (8, 8)})
305
+ is_support_stuck_zone: bool = field(metadata={"new_feature_str_mask": (16, 8)})
306
+ is_support_custom_door_sill: bool = field(metadata={"new_feature_str_mask": (32, 8)})
307
+ is_wifi_manage_supported: bool = field(metadata={"new_feature_str_mask": (128, 8)})
308
+ is_clean_route_fast_mode_supported: bool = field(metadata={"new_feature_str_mask": (256, 8)})
309
+ is_support_cliff_zone: bool = field(metadata={"new_feature_str_mask": (512, 8)})
310
+ is_support_smart_door_sill: bool = field(metadata={"new_feature_str_mask": (1024, 8)})
311
+ is_support_floor_direction: bool = field(metadata={"new_feature_str_mask": (2048, 8)})
312
+ is_back_charge_auto_wash_supported: bool = field(metadata={"new_feature_str_mask": (4096, 8)})
313
+ is_support_incremental_map: bool = field(metadata={"new_feature_str_mask": (4194304, 8)})
314
+ is_offline_map_supported: bool = field(metadata={"new_feature_str_mask": (16384, 8)})
315
+ is_super_deep_wash_supported: bool = field(metadata={"new_feature_str_mask": (32768, 8)})
316
+ is_ces_2022_supported: bool = field(metadata={"new_feature_str_mask": (65536, 8)})
317
+ is_dss_believable: bool = field(metadata={"new_feature_str_mask": (131072, 8)})
318
+ is_main_brush_up_down_supported_from_str: bool = field(metadata={"new_feature_str_mask": (262144, 8)})
319
+ is_goto_pure_clean_path_supported: bool = field(metadata={"new_feature_str_mask": (524288, 8)})
320
+ is_water_up_down_drain_supported: bool = field(metadata={"new_feature_str_mask": (1048576, 8)})
321
+ is_setting_carpet_first_supported: bool = field(metadata={"new_feature_str_mask": (8388608, 8)})
322
+ is_clean_route_deep_slow_plus_supported: bool = field(metadata={"new_feature_str_mask": (16777216, 8)})
323
+ is_dynamically_skip_clean_zone_supported: bool = field(metadata={"new_feature_str_mask": (33554432, 8)})
324
+ is_dynamically_add_clean_zones_supported: bool = field(metadata={"new_feature_str_mask": (67108864, 8)})
325
+ is_left_water_drain_supported: bool = field(metadata={"new_feature_str_mask": (134217728, 8)})
326
+ is_clean_count_setting_supported: bool = field(metadata={"new_feature_str_mask": (1073741824, 8)})
327
+ is_corner_clean_mode_supported: bool = field(metadata={"new_feature_str_mask": (2147483648, 8)})
328
+
329
+ # Features from new_feature_info_str (by bit index)
330
+ is_two_key_real_time_video_supported: bool = field(
331
+ metadata={"new_feature_str_bit": NewFeatureStrBit.TWO_KEY_REAL_TIME_VIDEO}
332
+ )
333
+ is_two_key_rtv_in_charging_supported: bool = field(
334
+ metadata={"new_feature_str_bit": NewFeatureStrBit.TWO_KEY_RTV_IN_CHARGING}
335
+ )
336
+ is_dirty_replenish_clean_supported: bool = field(
337
+ metadata={"new_feature_str_bit": NewFeatureStrBit.DIRTY_REPLENISH_CLEAN}
338
+ )
339
+ is_auto_delivery_field_in_global_status_supported: bool = field(
340
+ metadata={"new_feature_str_bit": NewFeatureStrBit.AUTO_DELIVERY_FIELD_IN_GLOBAL_STATUS}
341
+ )
342
+ is_avoid_collision_mode_supported: bool = field(
343
+ metadata={"new_feature_str_bit": NewFeatureStrBit.AVOID_COLLISION_MODE}
344
+ )
345
+ is_voice_control_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.VOICE_CONTROL})
346
+ is_new_endpoint_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.NEW_ENDPOINT})
347
+ is_pumping_water_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.PUMPING_WATER})
348
+ is_corner_mop_stretch_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.CORNER_MOP_STRETCH})
349
+ is_hot_wash_towel_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.HOT_WASH_TOWEL})
350
+ is_floor_dir_clean_any_time_supported: bool = field(
351
+ metadata={"new_feature_str_bit": NewFeatureStrBit.FLOOR_DIR_CLEAN_ANY_TIME}
352
+ )
353
+ is_pet_supplies_deep_clean_supported: bool = field(
354
+ metadata={"new_feature_str_bit": NewFeatureStrBit.PET_SUPPLIES_DEEP_CLEAN}
355
+ )
356
+ is_mop_shake_water_max_supported: bool = field(
357
+ metadata={"new_feature_str_bit": NewFeatureStrBit.MOP_SHAKE_WATER_MAX}
358
+ )
359
+ is_exact_custom_mode_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.EXACT_CUSTOM_MODE})
360
+ is_video_patrol_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.VIDEO_PATROL})
361
+ is_carpet_custom_clean_supported: bool = field(
362
+ metadata={"new_feature_str_bit": NewFeatureStrBit.CARPET_CUSTOM_CLEAN}
363
+ )
364
+ is_pet_snapshot_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.PET_SNAPSHOT})
365
+ is_custom_clean_mode_count_supported: bool = field(
366
+ metadata={"new_feature_str_bit": NewFeatureStrBit.CUSTOM_CLEAN_MODE_COUNT}
367
+ )
368
+ is_new_ai_recognition_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.NEW_AI_RECOGNITION})
369
+ is_auto_collection_2_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.AUTO_COLLECTION_2})
370
+ is_right_brush_stretch_supported: bool = field(
371
+ metadata={"new_feature_str_bit": NewFeatureStrBit.RIGHT_BRUSH_STRETCH}
372
+ )
373
+ is_smart_clean_mode_set_supported: bool = field(
374
+ metadata={"new_feature_str_bit": NewFeatureStrBit.SMART_CLEAN_MODE_SET}
375
+ )
376
+ is_dirty_object_detect_supported: bool = field(
377
+ metadata={"new_feature_str_bit": NewFeatureStrBit.DIRTY_OBJECT_DETECT}
378
+ )
379
+ is_no_need_carpet_press_set_supported: bool = field(
380
+ metadata={"new_feature_str_bit": NewFeatureStrBit.NO_NEED_CARPET_PRESS_SET}
381
+ )
382
+ is_voice_control_led_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.VOICE_CONTROL_LED})
383
+ is_water_leak_check_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.WATER_LEAK_CHECK})
384
+ is_min_battery_15_to_clean_task_supported: bool = field(
385
+ metadata={"new_feature_str_bit": NewFeatureStrBit.MIN_BATTERY_15_TO_CLEAN_TASK}
386
+ )
387
+ is_gap_deep_clean_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.GAP_DEEP_CLEAN})
388
+ is_object_detect_check_supported: bool = field(
389
+ metadata={"new_feature_str_bit": NewFeatureStrBit.OBJECT_DETECT_CHECK}
390
+ )
391
+ is_identify_room_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.IDENTIFY_ROOM})
392
+ is_matter_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.MATTER})
393
+ is_workday_holiday_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.WORKDAY_HOLIDAY})
394
+ is_clean_direct_status_supported: bool = field(
395
+ metadata={"new_feature_str_bit": NewFeatureStrBit.CLEAN_DIRECT_STATUS}
396
+ )
397
+ is_map_eraser_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.MAP_ERASER})
398
+ is_optimize_battery_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.OPTIMIZE_BATTERY})
399
+ is_activate_video_charging_and_standby_supported: bool = field(
400
+ metadata={"new_feature_str_bit": NewFeatureStrBit.ACTIVATE_VIDEO_CHARGING_AND_STANDBY}
401
+ )
402
+ is_carpet_long_haired_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.CARPET_LONG_HAIRED})
403
+ is_clean_history_time_line_supported: bool = field(
404
+ metadata={"new_feature_str_bit": NewFeatureStrBit.CLEAN_HISTORY_TIME_LINE}
405
+ )
406
+ is_max_zone_opened_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.MAX_ZONE_OPENED})
407
+ is_exhibition_function_supported: bool = field(
408
+ metadata={"new_feature_str_bit": NewFeatureStrBit.EXHIBITION_FUNCTION}
409
+ )
410
+ is_lds_lifting_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.LDS_LIFTING})
411
+ is_auto_tear_down_mop_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.AUTO_TEAR_DOWN_MOP})
412
+ is_small_side_mop_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.SMALL_SIDE_MOP})
413
+ is_support_side_brush_up_down_supported: bool = field(
414
+ metadata={"new_feature_str_bit": NewFeatureStrBit.SUPPORT_SIDE_BRUSH_UP_DOWN}
415
+ )
416
+ is_dry_interval_timer_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.DRY_INTERVAL_TIMER})
417
+ is_uvc_sterilize_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.UVC_STERILIZE})
418
+ is_midway_back_to_dock_supported: bool = field(
419
+ metadata={"new_feature_str_bit": NewFeatureStrBit.MIDWAY_BACK_TO_DOCK}
420
+ )
421
+ is_support_main_brush_up_down_supported: bool = field(
422
+ metadata={"new_feature_str_bit": NewFeatureStrBit.SUPPORT_MAIN_BRUSH_UP_DOWN}
423
+ )
424
+ is_egg_dance_mode_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.EGG_DANCE_MODE})
425
+ is_mechanical_arm_mode_supported: bool = field(
426
+ metadata={"new_feature_str_bit": NewFeatureStrBit.MECHANICAL_ARM_MODE}
427
+ )
428
+ is_tidyup_zones_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.TIDYUP_ZONES})
429
+ is_clean_time_line_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.CLEAN_TIME_LINE})
430
+ is_clean_then_mop_mode_supported: bool = field(
431
+ metadata={"new_feature_str_bit": NewFeatureStrBit.CLEAN_THEN_MOP_MODE}
432
+ )
433
+ is_type_identify_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.TYPE_IDENTIFY})
434
+ is_support_get_particular_status_supported: bool = field(
435
+ metadata={"new_feature_str_bit": NewFeatureStrBit.SUPPORT_GET_PARTICULAR_STATUS}
436
+ )
437
+ is_three_d_mapping_inner_test_supported: bool = field(
438
+ metadata={"new_feature_str_bit": NewFeatureStrBit.THREE_D_MAPPING_INNER_TEST}
439
+ )
440
+ is_sync_server_name_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.SYNC_SERVER_NAME})
441
+ is_should_show_arm_over_load_supported: bool = field(
442
+ metadata={"new_feature_str_bit": NewFeatureStrBit.SHOULD_SHOW_ARM_OVER_LOAD}
443
+ )
444
+ is_collect_dust_count_show_supported: bool = field(
445
+ metadata={"new_feature_str_bit": NewFeatureStrBit.COLLECT_DUST_COUNT_SHOW}
446
+ )
447
+ is_support_api_app_stop_grasp_supported: bool = field(
448
+ metadata={"new_feature_str_bit": NewFeatureStrBit.SUPPORT_API_APP_STOP_GRASP}
449
+ )
450
+ is_ctm_with_repeat_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.CTM_WITH_REPEAT})
451
+ is_side_brush_lift_carpet_supported: bool = field(
452
+ metadata={"new_feature_str_bit": NewFeatureStrBit.SIDE_BRUSH_LIFT_CARPET}
453
+ )
454
+ is_detect_wire_carpet_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.DETECT_WIRE_CARPET})
455
+ is_water_slide_mode_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.WATER_SLIDE_MODE})
456
+ is_soak_and_wash_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.SOAK_AND_WASH})
457
+ is_clean_efficiency_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.CLEAN_EFFICIENCY})
458
+ is_back_wash_new_smart_supported: bool = field(
459
+ metadata={"new_feature_str_bit": NewFeatureStrBit.BACK_WASH_NEW_SMART}
460
+ )
461
+ is_dual_band_wi_fi_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.DUAL_BAND_WI_FI})
462
+ is_program_mode_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.PROGRAM_MODE})
463
+ is_clean_fluid_delivery_supported: bool = field(
464
+ metadata={"new_feature_str_bit": NewFeatureStrBit.CLEAN_FLUID_DELIVERY}
465
+ )
466
+ is_carpet_long_haired_ex_supported: bool = field(
467
+ metadata={"new_feature_str_bit": NewFeatureStrBit.CARPET_LONG_HAIRED_EX}
468
+ )
469
+ is_over_sea_ctm_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.OVER_SEA_CTM})
470
+ is_full_duples_switch_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.FULL_DUPLES_SWITCH})
471
+ is_low_area_access_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.LOW_AREA_ACCESS})
472
+ is_follow_low_obs_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.FOLLOW_LOW_OBS})
473
+ is_two_gears_no_collision_supported: bool = field(
474
+ metadata={"new_feature_str_bit": NewFeatureStrBit.TWO_GEARS_NO_COLLISION}
475
+ )
476
+ is_carpet_shape_type_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.CARPET_SHAPE_TYPE})
477
+ is_sr_map_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.SR_MAP})
478
+
479
+ # Features from feature_info list
480
+ is_led_status_switch_supported: bool = field(metadata={"robot_features": 119})
481
+ is_multi_floor_supported: bool = field(metadata={"robot_features": 120})
482
+ is_support_fetch_timer_summary: bool = field(metadata={"robot_features": 122})
483
+ is_order_clean_supported: bool = field(metadata={"robot_features": 123})
484
+ is_analysis_supported: bool = field(metadata={"robot_features": 124})
485
+ is_remote_supported: bool = field(metadata={"robot_features": 125})
486
+ is_support_voice_control_debug: bool = field(metadata={"robot_features": 130})
487
+
488
+ # Features from model whitelists/blacklists or other flags
489
+ is_mop_forbidden_supported: bool = field(
490
+ metadata={
491
+ "model_whitelist": [
492
+ RoborockProductNickname.TANOSV,
493
+ RoborockProductNickname.TOPAZSV,
494
+ RoborockProductNickname.TANOS,
495
+ RoborockProductNickname.TANOSE,
496
+ RoborockProductNickname.TANOSSLITE,
497
+ RoborockProductNickname.TANOSS,
498
+ RoborockProductNickname.TANOSSPLUS,
499
+ RoborockProductNickname.TANOSSMAX,
500
+ RoborockProductNickname.ULTRON,
501
+ RoborockProductNickname.ULTRONLITE,
502
+ RoborockProductNickname.PEARL,
503
+ RoborockProductNickname.RUBYSLITE,
504
+ ]
505
+ }
506
+ )
507
+ is_soft_clean_mode_supported: bool = field(
508
+ metadata={
509
+ "model_whitelist": [
510
+ RoborockProductNickname.TANOSV,
511
+ RoborockProductNickname.TANOSE,
512
+ RoborockProductNickname.TANOS,
513
+ ]
514
+ }
515
+ )
516
+ is_custom_mode_supported: bool = field(metadata={"model_blacklist": [RoborockProductNickname.TANOS]})
517
+ is_support_custom_carpet: bool = field(metadata={"model_whitelist": [RoborockProductNickname.ULTRONLITE]})
518
+ is_show_general_obstacle_supported: bool = field(metadata={"model_whitelist": [RoborockProductNickname.TANOSSPLUS]})
519
+ is_show_obstacle_photo_supported: bool = field(
520
+ metadata={
521
+ "model_whitelist": [
522
+ RoborockProductNickname.TANOSSPLUS,
523
+ RoborockProductNickname.TANOSSMAX,
524
+ RoborockProductNickname.ULTRON,
525
+ ]
526
+ }
527
+ )
528
+ is_rubber_brush_carpet_supported: bool = field(metadata={"model_whitelist": [RoborockProductNickname.ULTRONLITE]})
529
+ is_carpet_pressure_use_origin_paras_supported: bool = field(
530
+ metadata={"model_whitelist": [RoborockProductNickname.ULTRONLITE]}
531
+ )
532
+ is_support_mop_back_pwm_set: bool = field(metadata={"model_whitelist": [RoborockProductNickname.PEARL]})
533
+ is_collect_dust_mode_supported: bool = field(metadata={"model_blacklist": [RoborockProductNickname.PEARL]})
534
+ is_support_water_mode: bool = field(
535
+ metadata={
536
+ "product_features": [
537
+ ProductFeatures.MOP_ELECTRONIC_MODULE,
538
+ ProductFeatures.MOP_SHAKE_MODULE,
539
+ ProductFeatures.MOP_SPIN_MODULE,
540
+ ]
541
+ }
542
+ )
543
+ is_pure_clean_mop_supported: bool = field(metadata={"product_features": [ProductFeatures.CLEANMODE_PURECLEANMOP]})
544
+ is_new_remote_view_supported: bool = field(metadata={"product_features": [ProductFeatures.REMOTE_BACK]})
545
+ is_max_plus_mode_supported: bool = field(metadata={"product_features": [ProductFeatures.CLEANMODE_MAXPLUS]})
546
+ is_none_pure_clean_mop_with_max_plus: bool = field(
547
+ metadata={"product_features": [ProductFeatures.CLEANMODE_NONE_PURECLEANMOP_WITH_MAXPLUS]}
548
+ )
549
+ is_clean_route_setting_supported: bool = field(
550
+ metadata={"product_features": [ProductFeatures.MOP_SHAKE_MODULE, ProductFeatures.MOP_SPIN_MODULE]}
551
+ )
552
+ is_mop_shake_module_supported: bool = field(metadata={"product_features": [ProductFeatures.MOP_SHAKE_MODULE]})
553
+ is_customized_clean_supported: bool = field(
554
+ metadata={"product_features": [ProductFeatures.MOP_SHAKE_MODULE, ProductFeatures.MOP_SPIN_MODULE]}
555
+ )
556
+
557
+ # Raw feature info values from get_init_status for diagnostics
558
+ new_feature_info: int = field(default=0, repr=False)
559
+ new_feature_info_str: str = field(default="", repr=False)
560
+ feature_info: list[int] = field(default_factory=list, repr=False)
561
+
562
+ @classmethod
563
+ def from_feature_flags(
564
+ cls,
565
+ new_feature_info: int,
566
+ new_feature_info_str: str,
567
+ feature_info: list[int],
568
+ product_nickname: RoborockProductNickname | None,
569
+ ) -> DeviceFeatures:
570
+ """Creates a DeviceFeatures instance from raw feature flags.
571
+ :param new_feature_info: A int from get_init_status (sometimes can be found in homedata, but it is not always)
572
+ :param new_feature_info_str: A hex string from get_init_status or home_data.
573
+ :param feature_info: A list of ints from get_init_status
574
+ :param product_nickname: The product nickname of the device."""
575
+ # For any future reverse engineerining:
576
+ # RobotNewFeatures = new_feature_info
577
+ # newFeatureInfoStr = new_feature_info_str
578
+ # feature_info =robotFeatures
579
+ kwargs: dict[str, Any] = {
580
+ # Store raw feature info for diagnostics
581
+ "new_feature_info": new_feature_info,
582
+ "new_feature_info_str": new_feature_info_str,
583
+ "feature_info": feature_info,
584
+ }
585
+
586
+ for f in fields(cls):
587
+ # Skip raw feature info fields (already set above)
588
+ if f.name in ("new_feature_info", "new_feature_info_str", "feature_info"):
589
+ continue
590
+ # Default all features to False.
591
+ kwargs[f.name] = False
592
+ if not f.metadata:
593
+ continue
594
+
595
+ if (mask := f.metadata.get("robot_new_features")) is not None:
596
+ kwargs[f.name] = bool(mask & new_feature_info)
597
+ elif (bit_index := f.metadata.get("upper_32_bits")) is not None:
598
+ # Check bits in the upper 32-bit integer of new_feature_info
599
+ if new_feature_info:
600
+ kwargs[f.name] = bool(((new_feature_info >> 32) >> bit_index) & 1)
601
+ elif (mask_info := f.metadata.get("new_feature_str_mask")) is not None:
602
+ # Check bitmask against a slice of the hex string
603
+ if new_feature_info_str:
604
+ try:
605
+ mask, slice_count = mask_info
606
+ if len(new_feature_info_str) >= slice_count:
607
+ last_chars = new_feature_info_str[-slice_count:]
608
+ value = int(last_chars, 16)
609
+ kwargs[f.name] = bool(mask & value)
610
+ except (ValueError, IndexError):
611
+ pass # Keep it False
612
+ elif (bit := f.metadata.get("new_feature_str_bit")) is not None:
613
+ # Check a specific bit in the hex string using its index
614
+ if new_feature_info_str:
615
+ try:
616
+ # Bit index defines which character and which bit inside it to check
617
+ char_index_from_end = 1 + bit.value // 4
618
+ if char_index_from_end <= len(new_feature_info_str):
619
+ char_hex = new_feature_info_str[-char_index_from_end]
620
+ nibble = int(char_hex, 16)
621
+ bit_in_nibble = bit.value % 4
622
+ kwargs[f.name] = bool((nibble >> bit_in_nibble) & 1)
623
+ except (ValueError, IndexError):
624
+ pass # Keep it False
625
+ elif (feature_id := f.metadata.get("robot_features")) is not None:
626
+ kwargs[f.name] = feature_id in feature_info
627
+ elif (whitelist := f.metadata.get("model_whitelist")) is not None:
628
+ # If product_nickname is None, assume it is not in the whitelist
629
+ kwargs[f.name] = product_nickname in whitelist or product_nickname is None
630
+ elif (blacklist := f.metadata.get("model_blacklist")) is not None:
631
+ # If product_nickname is None, assume it is not in the blacklist.
632
+ if product_nickname is None:
633
+ kwargs[f.name] = True
634
+ else:
635
+ kwargs[f.name] = product_nickname not in blacklist
636
+ elif (product_features := f.metadata.get("product_features")) is not None:
637
+ if product_nickname is not None:
638
+ available_features = PRODUCT_FEATURE_MAP.get(product_nickname, [])
639
+ if any(feat in available_features for feat in product_features): # type: ignore
640
+ kwargs[f.name] = True
641
+
642
+ return cls(**kwargs)
643
+
644
+ def get_supported_features(self) -> list[str]:
645
+ """Returns a list of supported features (Primarily used for logging purposes)."""
646
+ return [k for k, v in vars(self).items() if v]
647
+
648
+
649
+ WASH_N_FILL_DOCK_TYPES = [
650
+ RoborockDockTypeCode.empty_wash_fill_dock,
651
+ RoborockDockTypeCode.s8_dock,
652
+ RoborockDockTypeCode.p10_dock,
653
+ RoborockDockTypeCode.p10_pro_dock,
654
+ RoborockDockTypeCode.s8_maxv_ultra_dock,
655
+ RoborockDockTypeCode.qrevo_s_dock,
656
+ RoborockDockTypeCode.saros_r10_dock,
657
+ RoborockDockTypeCode.qrevo_curv_dock,
658
+ ]
659
+
660
+
661
+ def is_wash_n_fill_dock(dock_type: RoborockDockTypeCode) -> bool:
662
+ """Check if the dock type is a wash and fill dock."""
663
+ return dock_type in WASH_N_FILL_DOCK_TYPES
664
+
665
+
666
+ def is_valid_dock(dock_type: RoborockDockTypeCode) -> bool:
667
+ """Check if device supports a dock."""
668
+ return dock_type != RoborockDockTypeCode.no_dock