homebridge-roborock-vacuum 0.1.0

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 (45) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -0
  4. package/config.schema.json +31 -0
  5. package/dist/index.js +10 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/logger.js +39 -0
  8. package/dist/logger.js.map +1 -0
  9. package/dist/platform.js +167 -0
  10. package/dist/platform.js.map +1 -0
  11. package/dist/settings.js +8 -0
  12. package/dist/settings.js.map +1 -0
  13. package/dist/types.js +3 -0
  14. package/dist/types.js.map +1 -0
  15. package/dist/vacuum_accessory.js +152 -0
  16. package/dist/vacuum_accessory.js.map +1 -0
  17. package/package.json +66 -0
  18. package/roborockLib/data/UserData +4 -0
  19. package/roborockLib/data/clientID +4 -0
  20. package/roborockLib/i18n/de/translations.json +188 -0
  21. package/roborockLib/i18n/en/translations.json +208 -0
  22. package/roborockLib/i18n/es/translations.json +188 -0
  23. package/roborockLib/i18n/fr/translations.json +188 -0
  24. package/roborockLib/i18n/it/translations.json +188 -0
  25. package/roborockLib/i18n/nl/translations.json +188 -0
  26. package/roborockLib/i18n/pl/translations.json +188 -0
  27. package/roborockLib/i18n/pt/translations.json +188 -0
  28. package/roborockLib/i18n/ru/translations.json +188 -0
  29. package/roborockLib/i18n/uk/translations.json +188 -0
  30. package/roborockLib/i18n/zh-cn/translations.json +188 -0
  31. package/roborockLib/lib/RRMapParser.js +447 -0
  32. package/roborockLib/lib/deviceFeatures.js +995 -0
  33. package/roborockLib/lib/localConnector.js +249 -0
  34. package/roborockLib/lib/map/map.html +110 -0
  35. package/roborockLib/lib/map/zones.js +713 -0
  36. package/roborockLib/lib/mapCreator.js +692 -0
  37. package/roborockLib/lib/message.js +223 -0
  38. package/roborockLib/lib/messageQueueHandler.js +87 -0
  39. package/roborockLib/lib/roborockPackageHelper.js +116 -0
  40. package/roborockLib/lib/roborock_mqtt_connector.js +349 -0
  41. package/roborockLib/lib/sniffing/mitmproxy_roborock.py +300 -0
  42. package/roborockLib/lib/vacuum.js +636 -0
  43. package/roborockLib/roborockAPI.js +1365 -0
  44. package/roborockLib/test.js +31 -0
  45. package/roborockLib/userdata.json +24 -0
@@ -0,0 +1,995 @@
1
+ "use strict";
2
+
3
+ const errorCodes = {
4
+ 0: "No error",
5
+ 1: "Laser sensor fault",
6
+ 2: "Collision sensor fault",
7
+ 3: "Wheel floating",
8
+ 4: "Cliff sensor fault",
9
+ 5: "Main brush blocked",
10
+ 6: "Side brush blocked",
11
+ 7: "Wheel blocked",
12
+ 8: "Device stuck",
13
+ 9: "Dust bin missing",
14
+ 10: "Filter blocked",
15
+ 11: "Magnetic field detected",
16
+ 12: "Low battery",
17
+ 13: "Charging problem",
18
+ 14: "Battery failure",
19
+ 15: "Wall sensor fault",
20
+ 16: "Uneven surface",
21
+ 17: "Side brush failure",
22
+ 18: "Suction fan failure",
23
+ 19: "Unpowered charging station",
24
+ 20: "Unknown Error",
25
+ 21: "Laser pressure sensor problem",
26
+ 22: "Charge sensor problem",
27
+ 23: "Dock problem",
28
+ 24: "No-go zone or invisible wall detected",
29
+ 254: "Bin full",
30
+ 255: "Internal error",
31
+ "-1": "Unknown Error",
32
+ };
33
+
34
+ const stateCodes = {
35
+ 0: "Unknown",
36
+ 1: "Initiating",
37
+ 2: "Sleeping",
38
+ 3: "Idle",
39
+ 4: "Remote Control",
40
+ 5: "Cleaning",
41
+ 6: "Returning Dock",
42
+ 7: "Manual Mode",
43
+ 8: "Charging",
44
+ 9: "Charging Error",
45
+ 10: "Paused",
46
+ 11: "Spot Cleaning",
47
+ 12: "In Error",
48
+ 13: "Shutting Down",
49
+ 14: "Updating",
50
+ 15: "Docking",
51
+ 16: "Go To",
52
+ 17: "Zone Clean",
53
+ 18: "Room Clean",
54
+ 22: "Empying dust container",
55
+ 23: "Washing the mop",
56
+ 26: "Going to wash the mop",
57
+ 28: "In call",
58
+ 29: "Mapping",
59
+ 100: "Fully Charged",
60
+ };
61
+
62
+ const dockTypes = {
63
+ 0: "Charging dock",
64
+ 1: "Auto-Empty Dock",
65
+ 2: "Empty Wash Fill Dock",
66
+ 3: "Empty Wash Fill (Dry) Dock",
67
+ 5: "Auto-Empty Dock (Q8 Max+)",
68
+ 7: "Empty Wash Fill Dry Dock (S8 Pro Ultra)",
69
+ 8: "Empty Wash Fill Dry Dock (Q Revo)",
70
+ 9: "Empty Wash Fill Dry Dock (Q Revo Pro)",
71
+ };
72
+
73
+
74
+ const firmwareFeatures = {
75
+ 111: "isSupportFDSEndPoint",
76
+ 112: "isSupportAutoSplitSegments",
77
+ 114: "isSupportOrderSegmentClean",
78
+ 116: "isMapSegmentSupported",
79
+ 119: "isSupportLedStatusSwitch",
80
+ 120: "isMultiFloorSupported",
81
+ 122: "isSupportFetchTimerSummary",
82
+ 123: "isOrderCleanSupported",
83
+ 125: "isRemoteSupported",
84
+ };
85
+
86
+ const commands = {
87
+ app_start: { type: "boolean", defaultState: false },
88
+ app_segment_clean: { type: "boolean", defaultState: false },
89
+ app_stop: { type: "boolean", defaultState: false },
90
+ app_pause: { type: "boolean", defaultState: false },
91
+ app_charge: { type: "boolean", defaultState: false },
92
+ app_spot: { type: "boolean", defaultState: false },
93
+ app_zoned_clean: "json",
94
+ resume_zoned_clean: { type: "boolean", defaultState: false },
95
+ stop_zoned_clean: { type: "boolean", defaultState: false },
96
+ resume_segment_clean: { type: "boolean", defaultState: false },
97
+ stop_segment_clean: { type: "boolean", defaultState: false },
98
+ set_custom_mode: { type: "number", defaultState: 102, states: { 101: "Quiet", 102: "Balanced", 103: "Turbo", 104: "Max", 105: "Off" } },
99
+ find_me: { type: "boolean", defaultState: false },
100
+ app_goto_target: "json",
101
+ };
102
+
103
+ const deviceStates = {
104
+ unsave_map_flag: "number",
105
+ unsave_map_reason: "number",
106
+ dock_error_status: "number",
107
+ debug_mode: "number",
108
+ auto_dust_collection: "number",
109
+ dust_collection_status: "number",
110
+ dock_type: { type: "number", states: dockTypes },
111
+ adbumper_status: "string",
112
+ lock_status: "number",
113
+ is_locating: "number",
114
+ map_status: "number",
115
+ dnd_enabled: "number",
116
+ lab_status: "number",
117
+ in_fresh_state: "number",
118
+ in_returning: "number",
119
+ in_cleaning: "number",
120
+ map_present: "number",
121
+ error_code: { type: "number", states: errorCodes },
122
+ clean_area: { type: "number", unit: "m²", divider: 1000000 },
123
+ clean_time: { type: "number", unit: "min", divider: 60 },
124
+ battery: { type: "number", unit: "%" },
125
+ state: { type: "number", states: stateCodes },
126
+ msg_seq: "number",
127
+ msg_ver: "number",
128
+ fan_power: { type: "number", name: "Fan power", states: { 101: "Quiet", 102: "Balanced", 103: "Turbo", 104: "Max", 105: "Off" } },
129
+ is_exploring: "number",
130
+ events: "string",
131
+ subdivision_sets: "number",
132
+ repeat: "number",
133
+ replenish_mode: "number",
134
+ rdt: "number",
135
+ camera_status: "number",
136
+ distance_off: "number",
137
+ };
138
+
139
+ const consumablesInt = {
140
+ main_brush_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false },
141
+ side_brush_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false },
142
+ filter_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false },
143
+ filter_element_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false },
144
+ sensor_dirty_time: { type: "number", unit: "h", divider: 60 * 60, write: false },
145
+ dust_collection_work_times: { type: "number", unit: "h", divider: 60 * 60, write: false },
146
+ 125: { type: "number", unit: "%", divider: 60 * 60, write: false },
147
+ 126: { type: "number", unit: "%", divider: 60 * 60, write: false },
148
+ 127: { type: "number", unit: "%", divider: 60 * 60, write: false },
149
+ };
150
+ const consumablesString = {
151
+ main_brush_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false },
152
+ side_brush_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false },
153
+ filter_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false },
154
+ filter_element_work_time: { type: "number", unit: "h", divider: 60 * 60, write: false },
155
+ sensor_dirty_time: { type: "number", unit: "h", divider: 60 * 60, write: false },
156
+ dust_collection_work_times: { type: "number", unit: "h", divider: 60 * 60, write: false },
157
+ main_brush_life: { type: "number", unit: "%", divider: 60 * 60, write: false },
158
+ side_brush_life: { type: "number", unit: "%", divider: 60 * 60, write: false },
159
+ filter_life: { type: "number", unit: "%", divider: 60 * 60, write: false },
160
+ };
161
+
162
+ const resetConsumables = [
163
+ "main_brush_work_time",
164
+ "side_brush_work_time",
165
+ "filter_work_time",
166
+ "filter_element_work_time",
167
+ "sensor_dirty_time",
168
+ "dust_collection_work_times",
169
+ ];
170
+
171
+ const cleaningRecordsInt = {
172
+ 0: { type: "string", name: "cleaningRecord_begin" },
173
+ 1: { type: "string", name: "cleaningRecord_end" },
174
+ 2: { type: "number", name: "cleaningRecord_duration", unit: "min" },
175
+ 3: { type: "number", name: "cleaningRecord_area", unit: "m²" },
176
+ 4: { type: "number", name: "cleaningRecord_error" },
177
+ 5: { type: "number", name: "cleaningRecord_complete" },
178
+ 6: { type: "number", name: "cleaningRecord_start_type" },
179
+ 7: { type: "number", name: "cleaningRecord_finish_reason" },
180
+ 8: { type: "number", name: "cleaningRecord_dust_collection_status" },
181
+ };
182
+ const cleaningRecordsString = {
183
+ begin: { type: "string", name: "cleaningRecord_begin" },
184
+ end: { type: "string", name: "cleaningRecord_end" },
185
+ duration: { type: "number", name: "cleaningRecord_duration", unit: "min" },
186
+ area: { type: "number", name: "cleaningRecord_area", unit: "m²" },
187
+ error: { type: "number", name: "cleaningRecord_error" },
188
+ complete: { type: "number", name: "cleaningRecord_complete" },
189
+ start_type: { type: "number", name: "cleaningRecord_start_type" },
190
+ clean_type: { type: "number", name: "cleaningRecord_clean_type" },
191
+ finish_reason: { type: "number", name: "cleaningRecord_finish_reason" },
192
+ dust_collection_status: { type: "number", name: "cleaningRecord_dust_collection_status" },
193
+ manual_replenish: { type: "number", name: "cleaningRecord_manual_replenish" },
194
+ dirty_replenish: { type: "number", name: "cleaningRecord_dirty_replenish" },
195
+ clean_times: { type: "number", name: "cleaningRecord_clean_times" },
196
+ };
197
+
198
+ const cleaningInfoInt = {
199
+ 0: { name: "cleaningInfo_clean_time", type: "number", unit: "h" },
200
+ 1: { name: "cleaningInfo_clean_area", type: "number", unit: "m²" },
201
+ 2: { name: "cleaningInfo_clean_count", type: "number" },
202
+ };
203
+ const cleaningInfoString = {
204
+ clean_time: { name: "cleaningInfo_clean_time", type: "number", unit: "h" },
205
+ clean_area: { name: "cleaningInfo_clean_area", type: "number", unit: "m²" },
206
+ clean_count: { name: "cleaningInfo_clean_count", type: "number" },
207
+ dust_collection_count: { name: "cleaningInfo_dust_collection_count", type: "number" },
208
+ };
209
+
210
+ const actions = {
211
+ setConsumablesInt: async (context) => {
212
+ context.consumables = consumablesInt;
213
+ },
214
+ setConsumablesString: async (context) => {
215
+ context.consumables = consumablesString;
216
+ },
217
+ setCleaningRecordsInt: async (context) => {
218
+ context.cleaningRecords = cleaningRecordsInt;
219
+ context.cleaningInfo = cleaningInfoInt;
220
+ },
221
+ setCleaningRecordsString: async (context) => {
222
+ context.cleaningRecords = cleaningRecordsString;
223
+ context.cleaningInfo = cleaningInfoString;
224
+ },
225
+ setCleaningRecordsMixed: async (context) => {
226
+ // this is a special case some olders robots
227
+ context.cleaningRecords = cleaningRecordsString;
228
+ context.cleaningInfo = cleaningInfoInt;
229
+ },
230
+ set_charge_status: async () => {
231
+ deviceStates.charge_status = "number";
232
+ },
233
+ set_clean_percent: async () => {
234
+ deviceStates.clean_percent = "number";
235
+ },
236
+ set_common_status: () => {
237
+ deviceStates.common_status = "number";
238
+ },
239
+ set_rss: () => {
240
+ deviceStates.rss = "number";
241
+ },
242
+ set_dss: () => {
243
+ deviceStates.dss = "number";
244
+ },
245
+ set_kct: () => {
246
+ deviceStates.kct = "number";
247
+ },
248
+ set_in_warmup: () => {
249
+ deviceStates.in_warmup = "number";
250
+ },
251
+ set_last_clean_t: () => {
252
+ deviceStates.last_clean_t = "string";
253
+ },
254
+ set_wash_count: () => {
255
+ cleaningRecordsString.wash_count = { type: "number", name: "cleaning_record_wash_count" };
256
+ },
257
+ set_map_flag: (context) => {
258
+ deviceStates.map_flag = "number";
259
+ context.cleaningRecords.map_flag = { type: "number", name: "cleaningRecord_map_flag" };
260
+ },
261
+ set_back_type: () => {
262
+ deviceStates.back_type = "number";
263
+ },
264
+ set_rdt: () => {
265
+ deviceStates.rdt = "number";
266
+ },
267
+ set_replenish_mode: () => {
268
+ cleaningRecordsString.manual_replenish = { type: "number", name: "cleaning_record_manual_replenish" };
269
+ cleaningRecordsString.dirty_replenish = { type: "number", name: "cleaning_record_dirty_replenish" };
270
+ deviceStates.replenish_mode = "number";
271
+ },
272
+ set_repeat: () => {
273
+ deviceStates.repeat = "number";
274
+ },
275
+ set_cleaned_area: () => {
276
+ cleaningRecordsString.cleaned_area = { type: "number", name: "cleaning_record_cleaned_area", unit: "m²", divider: 1000000 };
277
+ },
278
+ set_clean_times: () => {
279
+ cleaningRecordsString.clean_times = { type: "number", name: "cleaning_record_clean_times" };
280
+ },
281
+ set_switch_status: () => {
282
+ deviceStates.switch_status = "number";
283
+ },
284
+ set_task_id: () => {
285
+ cleaningRecordsString.task_id = { type: "number", name: "cleaning_record_task_id" };
286
+ },
287
+ set_monitor_status: () => {
288
+ deviceStates.monitor_status = "number";
289
+ },
290
+ set_clean_fluid: () => {
291
+ deviceStates.clean_fluid = "number";
292
+ }
293
+ };
294
+
295
+ class deviceFeatures {
296
+ constructor(adapter, features, featuresStr, duid) {
297
+ this.adapter = adapter;
298
+ this.features = features;
299
+ this.featuresStr = featuresStr;
300
+ this.duid = duid;
301
+ this.cleaningInfo = {};
302
+ this.cleaningRecords = {};
303
+ this.consumables = {};
304
+ }
305
+
306
+ isWashThenChargeCmdSupported() {
307
+ commands.app_start_wash = { type: "boolean", defaultState: false };
308
+ commands.app_stop_wash = { type: "boolean", defaultState: false };
309
+
310
+ this.cleaningRecords.wash_count = { type: "number", name: "cleaning_record_wash_count" };
311
+
312
+ commands.set_wash_towel_mode = {
313
+ type: "json",
314
+ defaultState: '{"wash_mode":2}',
315
+ states: {
316
+ '{"wash_mode":0}': "Eco",
317
+ '{"wash_mode":1}': "Medium",
318
+ '{"wash_mode":2}': "Intense",
319
+ },
320
+ };
321
+ commands.set_smart_wash_params = {
322
+ type: "json",
323
+ defaultState: '{"smart_wash":0,"wash_interval":1800}',
324
+ states: {
325
+ '{"smart_wash":0,"wash_interval":600}': "10 Min",
326
+ '{"smart_wash":0,"wash_interval":900}': "15 Min",
327
+ '{"smart_wash":0,"wash_interval":1200}': "20 Min",
328
+ '{"smart_wash":0,"wash_interval":1500}': "25 Min",
329
+ '{"smart_wash":0,"wash_interval":1800}': "30 Min",
330
+ '{"smart_wash":0,"wash_interval":2100}': "35 Min",
331
+ '{"smart_wash":0,"wash_interval":2400}': "40 Min",
332
+ '{"smart_wash":0,"wash_interval":2700}': "45 Min",
333
+ '{"smart_wash":0,"wash_interval":3000}': "50 Min",
334
+ '{"smart_wash":1,"wash_interval":1200}': "Per room",
335
+ },
336
+ };
337
+ deviceStates.wash_phase = "number";
338
+ deviceStates.wash_ready = "number";
339
+ deviceStates.back_type = "number";
340
+ deviceStates.wash_status = "number";
341
+
342
+ consumablesString.strainer_work_times = { type: "number", unit: null, divider: null };
343
+ consumablesString.cleaning_brush_work_times = { type: "number", unit: null, divider: null };
344
+
345
+ resetConsumables.push("strainer_work_times");
346
+ resetConsumables.push("cleaning_brush_work_times");
347
+ }
348
+
349
+ isDustCollectionSettingSupported() {
350
+ commands.app_start_collect_dust = { type: "boolean", defaultState: false };
351
+ commands.app_stop_collect_dust = { type: "boolean", defaultState: false };
352
+ commands.set_dust_collection_switch_status = { type: "json", defaultState: '{"status":1}', states: { '{"status":0}': "Off", '{"status":1}': "On" } };
353
+ commands.set_dust_collection_mode = {
354
+ type: "json",
355
+ defaultState: '{"mode":0}',
356
+ states: {
357
+ '{"mode":0}': "Smart",
358
+ '{"mode":1}': "Low",
359
+ '{"mode":2}': "Medium",
360
+ '{"mode":4}': "Max",
361
+ },
362
+ };
363
+ }
364
+
365
+ isSupportedDrying() {
366
+ commands.app_set_dryer_status = { type: "string", defaultState: '{"status": 0}', states: { '{"status": 1}': "On", '{"status": 0}': "Off" } };
367
+ commands.app_set_dryer_setting = {
368
+ type: "json",
369
+ defaultState: '{"on":{"dry_time":10800},"status":0}',
370
+ states: {
371
+ '{"on":{"dry_time":10800},"status":0}': "Off",
372
+ '{"on":{"dry_time":7200},"status":1}': "2h",
373
+ '{"on":{"dry_time":10800},"status":1}': "3h",
374
+ '{"on":{"dry_time":14400},"status":1}': "4h",
375
+ },
376
+ };
377
+
378
+ deviceStates.dry_status = "number";
379
+ }
380
+
381
+ isSupportedWaterMode() {
382
+ commands.set_mop_mode = { type: "number", defaultState: 300, states: { 300: "Standard", 301: "Deep", 303: "Deep+" } };
383
+ commands.set_water_box_custom_mode = { type: "number", defaultState: 201, states: { 200: "Off", 201: "Mild", 202: "Moderate", 203: "Intense" } };
384
+
385
+ deviceStates.water_box_custom_mode = { type: "number", states: { 200: "Off", 201: "Mild", 202: "Moderate", 203: "Intense" } };
386
+ }
387
+
388
+ isShakeMopSetSupported() {
389
+ this.isSupportedWaterMode();
390
+ }
391
+
392
+ isElectronicWaterBoxSupported() {
393
+ this.isSupportedWaterMode();
394
+ }
395
+
396
+ isCarpetSupported() {
397
+ // nothing for now
398
+ }
399
+
400
+ isCleanRouteFastModeSupported() {
401
+ commands.set_mop_mode = { type: "number", defaultState: 300, states: { 300: "Standard", 301: "Deep", 303: "Deep+", 304: "Fast" } };
402
+ deviceStates.mop_mode = { type: "number", states: { 300: "Standard", 301: "Deep", 303: "Deep+", 304: "Fast" } };
403
+ }
404
+
405
+ isAvoidCollisionSupported() {
406
+ deviceStates.collision_avoid_status = "number";
407
+ deviceStates.avoid_count = "number";
408
+
409
+ this.cleaningRecords.avoid_count = { type: "number", name: "cleaning_record_avoid_count" };
410
+ }
411
+
412
+ isCornerCleanModeSupported() {
413
+ deviceStates.corner_clean_mode = "number";
414
+ }
415
+
416
+ isCameraSupported() {
417
+ deviceStates.distance_off = "number";
418
+ deviceStates.camera_status = "number";
419
+ }
420
+
421
+ isVideoLiveCallSupported() {
422
+ deviceStates.home_sec_enable_password = "number";
423
+ deviceStates.home_sec_status = "number";
424
+ const ip = this.adapter.config.hostname_ip;
425
+ const streamTypes = {
426
+ stream_html: `http://${ip}:1984/stream.html?src=${this.duid}`,
427
+ webrtc_html: `http://${ip}:1984/webrtc.html?src=${this.duid}&media=video`,
428
+ stream_mp4: `http://${ip}:1984/api/stream.mp4?src=${this.duid}`,
429
+ rtsp: `rtsp://${ip}:8554/${this.duid}?video`,
430
+ };
431
+
432
+ for (const [name, stream_uri] of Object.entries(streamTypes)) {
433
+ this.adapter.setObjectAsync(`Devices.${this.duid}.camera.${name}`, {
434
+ type: "state",
435
+ common: {
436
+ name: name,
437
+ type: "string",
438
+ role: "value",
439
+ read: true,
440
+ write: false,
441
+ def: stream_uri,
442
+ },
443
+ native: {},
444
+ });
445
+ }
446
+ }
447
+
448
+ isVoiceControlSupported() {
449
+ deviceStates.voice_chat_status = "number";
450
+ }
451
+
452
+ isSupportSetSwitchMapMode() {
453
+ deviceStates.switch_map_mode = "number";
454
+ }
455
+
456
+ isMopForbiddenSupported() {
457
+ deviceStates.mop_forbidden_enable = "number";
458
+ }
459
+
460
+ isShakeMopStrengthSupported() {
461
+ this.isSupportedWaterMode();
462
+ }
463
+
464
+ isWaterBoxSupported() {
465
+ deviceStates.water_box_carriage_status = "number";
466
+ deviceStates.water_box_mode = { type: "number", states: { 200: "Off", 201: "Mild", 202: "Moderate", 203: "Intense" } };
467
+ deviceStates.water_box_status = "number";
468
+ deviceStates.water_shortage_status = "number";
469
+
470
+ deviceStates.mop_mode = { type: "number", states: { 300: "Standard", 301: "Deep", 303: "Deep+" } };
471
+ }
472
+
473
+ isCustomWaterBoxDistanceSupported() {
474
+ // in this special case, create the command directly instead of the usual way
475
+ this.adapter.setObjectAsync(`Devices.${this.duid}.commands.set_water_box_distance_off`, {
476
+ type: "state",
477
+ common: {
478
+ name: this.adapter.translations.set_water_box_distance_off,
479
+ type: "number",
480
+ role: "value",
481
+ read: true,
482
+ write: true,
483
+ def: 1,
484
+ min: 1,
485
+ max: 30,
486
+ },
487
+ native: {},
488
+ });
489
+ }
490
+
491
+ isBackChargeAutoWashSupported() {
492
+ // this means the robot can stay reversed into the dock and still charge
493
+ // nothing for now
494
+ }
495
+
496
+ isAvoidCarpetSupported() {
497
+ deviceStates.carpet_mode = {
498
+ type: "string",
499
+ states: {
500
+ '[{"enable":0,"stall_time":10,"current_low":400,"current_high":500,"current_integral":450}]': "off",
501
+ '[{"enable":1,"stall_time":10,"current_low":400,"current_high":500,"current_integral":450}]': "on",
502
+ },
503
+ };
504
+ deviceStates.carpet_clean_mode = {
505
+ type: "string",
506
+ states: {
507
+ '{"carpet_clean_mode":0}': "Avoid",
508
+ '{"carpet_clean_mode":1}': "Rise",
509
+ '{"carpet_clean_mode":2}': "Ignore",
510
+ },
511
+ };
512
+ }
513
+
514
+ // the functions below are used for processing the firmware features from get_fw_features
515
+ isSupportFDSEndPoint() {
516
+ // nothing for now
517
+ }
518
+ isSupportAutoSplitSegments() {
519
+ // nothing for now
520
+ }
521
+ isSupportOrderSegmentClean() {
522
+ // nothing for now
523
+ }
524
+ isMapSegmentSupported() {
525
+ // nothing for now
526
+ }
527
+ isSupportLedStatusSwitch() {
528
+ // nothing for now
529
+ }
530
+ isMultiFloorSupported() {
531
+ const features = ["max_multi_map", "max_bak_map", "multi_map_count"];
532
+
533
+ for (const [, feature] of Object.entries(features)) {
534
+ this.adapter.setObjectAsync(`Devices.${this.duid}.floors.${feature}`, {
535
+ type: "state",
536
+ common: {
537
+ name: feature,
538
+ type: "number",
539
+ role: "value",
540
+ read: true,
541
+ write: false,
542
+ },
543
+ native: {},
544
+ });
545
+ }
546
+ }
547
+ isSupportFetchTimerSummary() {
548
+ // nothing for now
549
+ }
550
+ isOrderCleanSupported() {
551
+ // nothing for now
552
+ }
553
+ isRemoteSupported() {
554
+ // nothing for now
555
+ }
556
+
557
+ getFeatureList() {
558
+ const robotModel = this.adapter.getProductAttribute(this.duid, "model");
559
+
560
+ return {
561
+ isWashThenChargeCmdSupported: ((this.features / Math.pow(2, 32)) >> 5) & 1,
562
+ isDustCollectionSettingSupported: !!(33554432 & this.features),
563
+ isSupportedDrying: ((this.features / Math.pow(2, 32)) >> 15) & 1,
564
+ isShakeMopSetSupported: !!(262144 & this.features),
565
+ isVideoMonitorSupported: !!(8 & this.features), // I tested this for S7 MaxV, S8 MaxV
566
+ isVideoSettingSupported: !!(64 & this.features), // I tested this for S7 MaxV, S8 MaxV
567
+ isCarpetSupported: !!(512 & this.features),
568
+ isPhotoUploadSupported: !!(65536 & this.features),
569
+ isAvoidCollisionSupported: !!(134217728 & this.features),
570
+ isCornerCleanModeSupported: !!(2147483648 & this.features),
571
+ // isCameraSupported: [p.Products.TanosV_CN, p.Products.TanosV_CE, p.Products.TopazSV_CN, p.Products.TopazSV_CE, p.Products.TanosSV].hasElement(p.DMM.currentProduct),
572
+ isCameraSupported: !!["roborock.vacuum.a10", "roborock.vacuum.a27", "roborock.vacuum.a51", "roborock.vacuum.a87"].includes(robotModel),
573
+ isSupportSetSwitchMapMode: !!(268435456 & this.features),
574
+ // isMopForbiddenSupported: !!(p.DMM.isTanosV || p.DMM.isTanos || p.DMM.isTopazSV || p.DMM.isPearlPlus) || !![p.Products.TanosE, p.Products.TanosSL, p.Products.TanosS, p.Products.TanosSPlus, p.Products.TanosSMax, p.Products.Ultron, p.Products.UltronLite, p.Products.Pearl, p.Products.RubysLite].hasElement(p.DMM.currentProduct),
575
+ isMopForbiddenSupported: [
576
+ "roborock.vacuum.s6", // S6
577
+ "roborock.vacuum.a10", // S6 MaxV
578
+ "roborock.vacuum.a40", // Q7
579
+ "roborock.vacuum.s5e", // S5 Max
580
+ "roborock.vacuum.a38", // Q7 Max
581
+ "roborock.vacuum.a72", // Q5 Pro
582
+ "roborock.vacuum.a73", // Q8 Max
583
+ "roborock.vacuum.a75", // Q Revo
584
+ "roborock.vacuum.a15", // S7
585
+ "roborock.vacuum.a51", // S8
586
+ "roborock.vacuum.a70", // S8 Pro Ultra
587
+ "roborock.vacuum.a62", // S7 Pro Ultra
588
+ "roborock.vacuum.a65", // S7 Max Ultra
589
+ "roborock.vacuum.a27", // S7 MaxV (Ultra)
590
+ "roborock.vacuum.a87", // Qrevo MaxV
591
+ "roborock.vacuum.a101", // Q Revo Pro
592
+ "roborock.vacuum.a97", // S8 MaxV (Ultra)
593
+ ].includes(robotModel),
594
+ // isShakeMopStrengthSupported: p.DMM.currentProduct == p.Products.TanosS || p.DMM.currentProduct == p.Products.TanosSPlus || p.DMM.isGarnet || p.DMM.isTopazSV || p.DMM.isPearlPlus || p.DMM.isCoral || p.DMM.isTopazS || p.DMM.isTopazSPlus || p.DMM.isTopazSC || p.DMM.isTopazSV || p.DMM.isPearlPlus || p.DMM.isTanosSMax || p.DMM.isUltron || p.DMM.isUltronSPlus || p.DMM.isUltronSMop || p.DMM.isUltronSV || p.DMM.isPearl
595
+ isShakeMopStrengthSupported: [
596
+ "roborock.vacuum.a08", // S6 Pure
597
+ "roborock.vacuum.a10", // S6 MaxV
598
+ "roborock.vacuum.s6", // S6
599
+ "roborock.vacuum.a62", // S7 Pro Ultra
600
+ "roborock.vacuum.a51", // S8
601
+ "roborock.vacuum.a15", // S7
602
+ "roborock.vacuum.a72", // Q5 Pro
603
+ "roborock.vacuum.a27", // S7 MaxV (Ultra)
604
+ "roborock.vacuum.a19", // S4 Max
605
+ "roborock.vacuum.a40", // Q7
606
+ "roborock.vacuum.a65", // S7 Max Ultra
607
+ "roborock.vacuum.a38", // Q7 Max
608
+ "roborock.vacuum.a73", // Q8 Max
609
+ "roborock.vacuum.a75", // Q Revo
610
+ "roborock.vacuum.a70", // S8 Pro Ultra
611
+ "roborock.vacuum.s5e", // S5 Max
612
+ "roborock.vacuum.a87", // Qrevo MaxV
613
+ "roborock.vacuum.a101", // Q Revo Pro
614
+ ].includes(robotModel),
615
+ // isWaterBoxSupported: [p.Products.Tanos_CE, p.Products.Tanos_CN].hasElement(p.DMM.currentProduct)
616
+ isWaterBoxSupported: [
617
+ "roborock.vacuum.s5e", // S5 Max
618
+ "roborock.vacuum.a08", // S6 Pure
619
+ "roborock.vacuum.a10", // S6 MaxV
620
+ "roborock.vacuum.a15", // S7
621
+ "roborock.vacuum.a27", // S7 MaxV (Ultra)
622
+ "roborock.vacuum.a38", // Q7 Max
623
+ "roborock.vacuum.a40", // Q7
624
+ "roborock.vacuum.a51", // S8
625
+ "roborock.vacuum.a62", // S7 Pro Ultra
626
+ "roborock.vacuum.a65", // S7 Max Ultra
627
+ "roborock.vacuum.a70", // S8 Pro Ultra
628
+ "roborock.vacuum.a72", // Q5 Pro
629
+ "roborock.vacuum.a73", // Q8 Max
630
+ "roborock.vacuum.a75", // Q Revo
631
+ "roborock.vacuum.a87", // Qrevo MaxV
632
+ "roborock.vacuum.a101", // Q Revo Pro
633
+ "roborock.vacuum.a97", // S8 MaxV (Ultra)
634
+ ].includes(robotModel),
635
+ isCustomWaterBoxDistanceSupported: !!(2147483648 & this.features),
636
+ isBackChargeAutoWashSupported: this.featuresStr && !!(4096 & parseInt("0x" + this.featuresStr.slice(-8))),
637
+ isAvoidCarpetSupported: [
638
+ "roborock.vacuum.a10", // S6 MaxV
639
+ "roborock.vacuum.a40", // Q7
640
+ "roborock.vacuum.s6", // S6
641
+ "roborock.vacuum.a72", // Q5 Pro
642
+ "roborock.vacuum.a73", // Q8 Max
643
+ "roborock.vacuum.a38", // Q7 Max
644
+ "roborock.vacuum.a51", // S8
645
+ "roborock.vacuum.a75", // Q Revo
646
+ "roborock.vacuum.a27", // S7 MaxV (Ultra)
647
+ "roborock.vacuum.a15", // S7
648
+ "roborock.vacuum.a70", // S8 Pro Ultra
649
+ "roborock.vacuum.a62", // S7 Pro Ultra
650
+ "roborock.vacuum.a65", // S7 Max Ultra
651
+ "roborock.vacuum.a87", // Qrevo MaxV
652
+ "roborock.vacuum.a101", // Q Revo Pro
653
+ "roborock.vacuum.a97", // S8 MaxV (Ultra)
654
+ ].includes(robotModel),
655
+ // this isn't the correct way to use this. This code must be from a different robot
656
+ // isVoiceControlSupported: !!(parseInt(`0x${this.featuresStr || "0"}`.slice(-10, -9)) & 2),
657
+ isVoiceControlSupported: [
658
+ "roborock.vacuum.a27", // S7 MaxV (Ultra)
659
+ ],
660
+ isElectronicWaterBoxSupported: [], // nothing for now. If this is needed, add the models here
661
+ isCleanRouteFastModeSupported: this.featuresStr && !!(256 & parseInt("0x" + this.featuresStr.slice(-8))),
662
+ isVideoLiveCallSupported: [
663
+ "roborock.vacuum.a10", // S6 MaxV
664
+ "roborock.vacuum.a27", // S7 MaxV (Ultra)
665
+ "roborock.vacuum.a97", // S8 MaxV (Ultra)
666
+ "roborock.vacuum.a87", // Qrevo MaxV
667
+ ].includes(robotModel),
668
+ };
669
+ }
670
+
671
+ async processSupportedFeatures() {
672
+ const robotModel = this.adapter.getProductAttribute(this.duid, "model");
673
+ const productCategory = this.adapter.getProductAttribute(this.duid, "category");
674
+
675
+ if (productCategory == "robot.vacuum.cleaner") {
676
+ // process states etc. depending on model
677
+ const modelConfig = {
678
+ // S6 Pure
679
+ "roborock.vacuum.a08": ["setCleaningRecordsMixed", "setConsumablesInt"],
680
+ // S6 MaxV
681
+ "roborock.vacuum.a10": ["setCleaningRecordsMixed", "setConsumablesInt"],
682
+ // S7
683
+ "roborock.vacuum.a15": ["setCleaningRecordsString", "setConsumablesInt"],
684
+ // S4 Max
685
+ "roborock.vacuum.a19": ["setCleaningRecordsInt", "setConsumablesInt"],
686
+ // S7 MaxV (Ultra)
687
+ "roborock.vacuum.a27": [
688
+ "setCleaningRecordsString",
689
+ "setConsumablesInt",
690
+ "set_map_flag",
691
+ "set_back_type",
692
+ "set_charge_status",
693
+ "set_clean_percent",
694
+ "set_switch_status",
695
+ ],
696
+ // Q7 Max
697
+ "roborock.vacuum.a38": ["setCleaningRecordsString", "setConsumablesInt", "set_map_flag", "set_charge_status"],
698
+ // Q7
699
+ "roborock.vacuum.a40": ["setCleaningRecordsString", "setConsumablesInt", "set_map_flag", "set_charge_status"],
700
+ // S8
701
+ "roborock.vacuum.a51": [
702
+ "setCleaningRecordsString",
703
+ "setConsumablesInt",
704
+ "set_dss",
705
+ "set_rss",
706
+ "set_last_clean_t",
707
+ "set_map_flag",
708
+ "set_charge_status",
709
+ "set_clean_percent",
710
+ "set_switch_status",
711
+ ],
712
+ // S7 Pro Ultra
713
+ "roborock.vacuum.a62": ["setCleaningRecordsString", "setConsumablesInt", "set_dss", "set_rss", "set_map_flag", "set_charge_status", "set_clean_percent"],
714
+ // S7 Max Ultra
715
+ "roborock.vacuum.a65": [
716
+ "setCleaningRecordsString",
717
+ "setConsumablesInt",
718
+ "set_dss",
719
+ "set_rss",
720
+ "set_map_flag",
721
+ "set_back_type",
722
+ "set_charge_status",
723
+ "set_clean_percent",
724
+ "set_switch_status",
725
+ "set_clean_fluid",
726
+ "set_cleaned_area",
727
+ ],
728
+ // S8 Pro Ultra
729
+ "roborock.vacuum.a70": [
730
+ "setCleaningRecordsString",
731
+ "setConsumablesInt",
732
+ "set_common_status",
733
+ "set_dss",
734
+ "set_rss",
735
+ "set_last_clean_t",
736
+ "set_map_flag",
737
+ "set_back_type",
738
+ "set_charge_status",
739
+ "set_switch_status",
740
+
741
+ "set_clean_percent",
742
+ ],
743
+ // Q5 Pro
744
+ "roborock.vacuum.a72": [
745
+ "setCleaningRecordsString",
746
+ "setConsumablesInt",
747
+ "set_dss",
748
+ "set_rss",
749
+ "set_last_clean_t",
750
+ "set_map_flag",
751
+ "set_back_type",
752
+ "set_charge_status",
753
+ "set_clean_percent",
754
+ ],
755
+ // Q8 Max
756
+ "roborock.vacuum.a73": [
757
+ "setCleaningRecordsString",
758
+ "setConsumablesInt",
759
+ "set_common_status",
760
+ "set_dss",
761
+ "set_rss",
762
+ "set_in_warmup",
763
+ "set_last_clean_t",
764
+ "set_map_flag",
765
+ "set_charge_status",
766
+ "set_clean_percent",
767
+ ],
768
+ // Q Revo
769
+ "roborock.vacuum.a75": [
770
+ "setCleaningRecordsString",
771
+ "setConsumablesInt",
772
+ "set_common_status",
773
+ "set_dss",
774
+ "set_rss",
775
+ "set_kct",
776
+ "set_in_warmup",
777
+ "set_last_clean_t",
778
+ "set_map_flag",
779
+ "set_back_type",
780
+ "set_charge_status",
781
+ "set_clean_percent",
782
+ "set_rdt",
783
+ "set_switch_status",
784
+ ],
785
+ // S4
786
+ "roborock.vacuum.s4": ["setCleaningRecordsInt", "setConsumablesString"],
787
+ // S5 Max
788
+ "roborock.vacuum.s5e": ["setCleaningRecordsMixed", "setConsumablesInt"],
789
+ // S6
790
+ "roborock.vacuum.s6": ["setCleaningRecordsMixed", "setConsumablesInt"],
791
+ // Qrevo MaxV
792
+ "roborock.vacuum.a87": [
793
+ "setCleaningRecordsString",
794
+ "setConsumablesInt",
795
+ "set_in_warmup",
796
+ "set_charge_status",
797
+ "set_clean_percent",
798
+ "set_rss",
799
+ "set_common_status",
800
+ "set_last_clean_t",
801
+ "set_kct",
802
+ "set_map_flag",
803
+ "set_replenish_mode",
804
+ "set_repeat",
805
+ "set_rdt",
806
+ "set_cleaned_area",
807
+ "set_clean_times",
808
+ "set_switch_status",
809
+ ],
810
+ // Q Revo Pro
811
+ "roborock.vacuum.a101": [
812
+ "setCleaningRecordsString",
813
+ "setConsumablesInt",
814
+ "set_common_status",
815
+ "set_dss",
816
+ "set_rss",
817
+ "set_kct",
818
+ "set_in_warmup",
819
+ "set_last_clean_t",
820
+ "set_map_flag",
821
+ "set_back_type",
822
+ "set_charge_status",
823
+ "set_clean_percent",
824
+ "set_cleaned_area",
825
+ "set_switch_status",
826
+ ],
827
+ // S8 MaxV (Ultra)
828
+ "roborock.vacuum.a97": [
829
+ "setCleaningRecordsString",
830
+ "setConsumablesInt",
831
+ "set_cleaned_area",
832
+ "set_last_clean_t",
833
+ "set_switch_status",
834
+ "set_monitor_status",
835
+ "set_kct",
836
+ "set_common_status",
837
+ "set_rss",
838
+ "set_clean_percent",
839
+ "set_charge_status",
840
+ "set_in_warmup",
841
+ "set_map_flag",
842
+ "set_task_id",
843
+ ],
844
+ };
845
+
846
+ // process modelConfig
847
+ const configActions = modelConfig[robotModel];
848
+ if (configActions) {
849
+ for (const actionName of configActions) {
850
+ const action = actions[actionName];
851
+ if (action) {
852
+ await action(this);
853
+ }
854
+ }
855
+ } else {
856
+ this.adapter.catchError(`This robot ${robotModel} is not fully supported just yet. Contact the dev to get this robot fully supported!`);
857
+ }
858
+
859
+ this.adapter.createBaseRobotObjects(this.duid);
860
+
861
+ const featureList = this.getFeatureList();
862
+ this.adapter.log.debug(`Supported features of robot ${this.duid} - ${robotModel}: ${JSON.stringify(featureList)}`);
863
+ Object.keys(featureList).forEach((feature) => {
864
+ if (featureList[feature]) {
865
+ if (typeof this[feature] === "function") {
866
+ this[feature]();
867
+ }
868
+ }
869
+ });
870
+
871
+ // process commands
872
+ for (const [command, type] of Object.entries(commands)) {
873
+ if (typeof type == "string") {
874
+ await this.adapter.createCommand(this.duid, command, type);
875
+ } else {
876
+ await this.adapter.createCommand(this.duid, command, type.type, type.defaultState, type.states);
877
+ }
878
+ }
879
+
880
+ // process device states
881
+ for (const [state, type] of Object.entries(deviceStates)) {
882
+ if (typeof type == "string") {
883
+ await this.adapter.createDeviceStatus(this.duid, state, type);
884
+ } else {
885
+ await this.adapter.createDeviceStatus(this.duid, state, type.type, type.states, type.unit);
886
+ }
887
+ }
888
+
889
+ // process consumables
890
+ for (const [consumable, object] of Object.entries(this.consumables)) {
891
+ await this.adapter.createConsumable(this.duid, consumable, object.type, object.states, object.unit);
892
+ }
893
+ // process reset of consumables
894
+ for (const [, resetConsumable] of Object.entries(resetConsumables)) {
895
+ await this.adapter.createResetConsumables(this.duid, resetConsumable);
896
+ }
897
+
898
+ // process cleaning info
899
+ for (const [cleaningInfoKey, cleaningInfoEntry] of Object.entries(this.cleaningInfo)) {
900
+ await this.adapter.createCleaningInfo(this.duid, cleaningInfoKey, cleaningInfoEntry);
901
+ }
902
+
903
+ // process cleaning records
904
+ for (const [cleaningRecord, object] of Object.entries(this.cleaningRecords)) {
905
+ await this.adapter.createCleaningRecord(this.duid, cleaningRecord, object.type, object.states, object.unit);
906
+ }
907
+ } else if (productCategory == "roborock.vacuum") {
908
+ // vacuum (not sure if it's actually roborock.vacuum. Might be something else. Haven't testet)
909
+ this.adapter.createBasicVacuumObjects(this.duid);
910
+ } else if (productCategory == "roborock.wm") {
911
+ // washing machine
912
+ this.adapter.createBasicWashingMachineObjects(this.duid);
913
+ }
914
+ }
915
+
916
+ processDockType(dockType) {
917
+ switch (dockType) {
918
+ case 0: // Charging dock
919
+ // nothing to do here
920
+ break;
921
+ case 1: // Auto-Empty Dock - Onyx: S7+/S7Plus (Empty Dock)
922
+ this.isDustCollectionSettingSupported();
923
+ break;
924
+ case 2: // Empty Wash Fill Dock - Onyx2: G10
925
+ this.isWashThenChargeCmdSupported();
926
+ break;
927
+ case 3: // Empty Wash Fill (Dry) Dock - Onyx3: S7 Pro, MaxV Ultra
928
+ this.isDustCollectionSettingSupported();
929
+ this.isWashThenChargeCmdSupported();
930
+ this.isSupportedDrying();
931
+ break;
932
+ case 5: // Onyx-C: S8, S8 Plus, Q8, Q8 Max
933
+ this.isDustCollectionSettingSupported();
934
+ break;
935
+ case 6: // Onyx3 Plus: S7 Max Ultra
936
+ this.isDustCollectionSettingSupported();
937
+ this.isWashThenChargeCmdSupported();
938
+ this.isSupportedDrying();
939
+ break;
940
+ case 7: // Onyx4: S8 Pro Ultra
941
+ this.isDustCollectionSettingSupported();
942
+ this.isWashThenChargeCmdSupported();
943
+ this.isSupportedDrying();
944
+ break;
945
+ case 8: // PEARL: Q Revo, P10
946
+ this.isDustCollectionSettingSupported();
947
+ this.isWashThenChargeCmdSupported();
948
+ this.isSupportedDrying();
949
+ break;
950
+ // Not much info on this one. Might be missing some features
951
+ case 9: // Unknown codename for now: Q Revo Pro
952
+ this.isDustCollectionSettingSupported();
953
+ this.isWashThenChargeCmdSupported();
954
+ this.isSupportedDrying();
955
+ break;
956
+ default:
957
+ break;
958
+ }
959
+ }
960
+
961
+ getConsumablesDivider(consumable) {
962
+ const robotModel = this.adapter.getProductAttribute(this.duid, "model");
963
+ const consumables = robotModel == "roborock.vacuum.s4" ? consumablesInt : consumablesString;
964
+
965
+ if (consumables[consumable]) {
966
+ return consumables[consumable].divider;
967
+ } else {
968
+ return false;
969
+ }
970
+ }
971
+
972
+ getStatusDivider(attribute) {
973
+ const divider = deviceStates[attribute]?.divider;
974
+
975
+ if (divider) {
976
+ return divider;
977
+ } else {
978
+ return false;
979
+ }
980
+ }
981
+
982
+ getFirmwareFeature(featureID) {
983
+ const feature = firmwareFeatures[featureID];
984
+
985
+ if (feature) {
986
+ return feature;
987
+ } else {
988
+ return "unknown feature";
989
+ }
990
+ }
991
+ }
992
+
993
+ module.exports = {
994
+ deviceFeatures,
995
+ };