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,1365 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+ const fs = require('fs');
5
+ const axios = require("axios");
6
+ const crypto = require("crypto");
7
+ const express = require("express");
8
+ const { debug } = require("console");
9
+ const { get } = require("http");
10
+
11
+ const rrLocalConnector = require("./lib/localConnector").localConnector;
12
+ const roborock_mqtt_connector = require("./lib/roborock_mqtt_connector").roborock_mqtt_connector;
13
+ const rrMessage = require("./lib/message").message;
14
+ const vacuum_class = require("./lib/vacuum").vacuum;
15
+ const roborockPackageHelper = require("./lib/roborockPackageHelper").roborockPackageHelper;
16
+ const deviceFeatures = require("./lib/deviceFeatures").deviceFeatures;
17
+ const messageQueueHandler = require("./lib/messageQueueHandler").messageQueueHandler;
18
+
19
+
20
+ let socketServer, webserver;
21
+
22
+ const dockingStationStates = ["cleanFluidStatus", "waterBoxFilterStatus", "dustBagStatus", "dirtyWaterBoxStatus", "clearWaterBoxStatus", "isUpdownWaterReady"];
23
+
24
+ function md5hex(str) {
25
+ return crypto.createHash("md5").update(str).digest("hex");
26
+ }
27
+
28
+ class Roborock {
29
+
30
+ constructor(options) {
31
+
32
+ this.bInited = false;
33
+
34
+ this.config = options;
35
+
36
+ this.updateInterval = options.updateInterval || 180;
37
+ this.log = options.log || console;
38
+ this.language = options.language || "en";
39
+
40
+ this.localKeys = null;
41
+ this.roomIDs = {};
42
+ this.vacuums = {};
43
+ this.socket = null;
44
+
45
+ this.objects = {};
46
+ this.states = {};
47
+
48
+ this.idCounter = 0;
49
+ this.nonce = crypto.randomBytes(16);
50
+ this.messageQueue = new Map();
51
+
52
+ this.roborockPackageHelper = new roborockPackageHelper(this);
53
+
54
+ this.localConnector = new rrLocalConnector(this);
55
+ this.rr_mqtt_connector = new roborock_mqtt_connector(this);
56
+ this.message = new rrMessage(this);
57
+
58
+ this.messageQueueHandler = new messageQueueHandler(this);
59
+
60
+ this.pendingRequests = new Map();
61
+
62
+ this.localDevices = {};
63
+ this.remoteDevices = new Set();
64
+
65
+ this.name = "roborock";
66
+ this.deviceNotify = null;
67
+ this.baseURL = options.baseURL || "usiot.roborock.com";
68
+ }
69
+
70
+ isInited() {
71
+ return this.bInited;
72
+ }
73
+
74
+ setInterval(callback, interval, ...args) {
75
+ return setInterval(() => callback(...args), interval);
76
+ }
77
+
78
+ clearInterval(interval) {
79
+ clearInterval(interval);
80
+ }
81
+
82
+ setTimeout(callback, timeout, ...args) {
83
+ return setTimeout(() => callback(...args), timeout);
84
+ }
85
+
86
+ clearTimeout(timeout) {
87
+ clearTimeout(timeout);
88
+ }
89
+
90
+ //dummy function for calling setObjectNotExistsAsync
91
+ async setObjectNotExistsAsync(id, obj) {
92
+
93
+ }
94
+
95
+ //dummy function for calling setObjectAsync
96
+ async setObjectAsync(id, obj) {
97
+
98
+ }
99
+
100
+ //dummy function for calling getObjectAsync
101
+ async getObjectAsync(id) {
102
+
103
+ }
104
+
105
+ //dummy function for calling delObjectAsync
106
+ async delObjectAsync(id) {
107
+
108
+ }
109
+
110
+ getStateAsync(id) {
111
+
112
+ try {
113
+ if(id == "UserData" || id == "clientID"){
114
+ return JSON.parse(fs.readFileSync(path.resolve(__dirname, `./data/${id}`), 'utf8'));
115
+ }
116
+
117
+ return this.states[id];
118
+
119
+ }catch(error) {
120
+ this.log.error(`getStateAsync: ${error}`);
121
+ }
122
+
123
+ return null;
124
+ }
125
+
126
+ async setStateAsync(id, state) {
127
+ try {
128
+
129
+ if(id == "UserData" || id == "clientID"){
130
+ fs.writeFileSync(path.resolve(__dirname, `./data/${id}`), JSON.stringify(state, null, 2, 'utf8'));
131
+ }
132
+
133
+ this.states[id] = state;
134
+
135
+ if(this.deviceNotify && (id == "HomeData" || id == "CloudMessage")){
136
+ this.deviceNotify(id, state);
137
+ }
138
+
139
+ }catch(error) {
140
+ this.log.error(`setStateAsync: ${error}`);
141
+ }
142
+ }
143
+
144
+ async setStateChangedAsync(id, state) {
145
+ await this.setStateAsync(id, state);
146
+ }
147
+
148
+ async deleteStateAsync(id) {
149
+ try {
150
+
151
+ if(id == "UserData" || id == "clientID"){
152
+ fs.unlinkSync(path.resolve(__dirname, `./data/${id}`));
153
+ }
154
+
155
+ delete this.states[id];
156
+
157
+
158
+ }catch(error) {
159
+ this.log.error(`deleteStateAsync: ${error}`);
160
+ }
161
+ }
162
+
163
+ subscribeStates(id) {
164
+ this.log.debug(`subscribeStates: ${id}`);
165
+ }
166
+
167
+ /**
168
+ * Is called when databases are connected and adapter received configuration.
169
+ */
170
+ async startService(callback) {
171
+
172
+
173
+ this.log.info(`Starting adapter. This might take a few minutes depending on your setup. Please wait.`);
174
+ this.translations = require(`./i18n/${this.language || "en"}/translations.json`);
175
+
176
+
177
+ // create new clientID if it doesn't exist yet
178
+ let clientID = "";
179
+ try {
180
+ const storedClientID = await this.getStateAsync("clientID");
181
+ if (storedClientID) {
182
+ clientID = storedClientID.val?.toString() ?? "";
183
+ } else {
184
+ clientID = crypto.randomUUID();
185
+ await this.setStateAsync("clientID", { val: clientID, ack: true });
186
+ }
187
+ } catch (error) {
188
+ this.log.error(`Error while retrieving or setting clientID: ${error.message}`);
189
+ }
190
+
191
+ if (!this.config.username || !this.config.password) {
192
+ this.log.error("Username or password missing!");
193
+ return;
194
+ }
195
+
196
+ this.instance = clientID;
197
+
198
+ // Initialize the login API (which is needed to get access to the real API).
199
+ this.loginApi = axios.create({
200
+ baseURL: 'https://' + this.baseURL,
201
+ headers: {
202
+ header_clientid: crypto.createHash("md5").update(this.config.username).update(clientID).digest().toString("base64"),
203
+ },
204
+ });
205
+ await this.setStateAsync("info.connection", { val: true, ack: true });
206
+ // api/v1/getUrlByEmail(email = ...)
207
+
208
+ const userdata = await this.getUserData(this.loginApi);
209
+
210
+ try {
211
+ this.loginApi.defaults.headers.common["Authorization"] = userdata.token;
212
+ } catch (error) {
213
+ this.log.error("Failed to login. Most likely wrong token! Deleting HomeData and UserData. Try again! " + error);
214
+
215
+ this.deleteStateAsync("HomeData");
216
+ this.deleteStateAsync("UserData");
217
+ }
218
+ const rriot = userdata.rriot;
219
+
220
+ // Initialize the real API.
221
+ this.api = axios.create({
222
+ baseURL: rriot.r.a,
223
+ });
224
+ this.api.interceptors.request.use((config) => {
225
+ try {
226
+ const timestamp = Math.floor(Date.now() / 1000);
227
+ const nonce = crypto.randomBytes(6).toString("base64").substring(0, 6).replace("+", "X").replace("/", "Y");
228
+ let url;
229
+ if (this.api) {
230
+ url = new URL(this.api.getUri(config));
231
+ const prestr = [rriot.u, rriot.s, nonce, timestamp, md5hex(url.pathname), /*queryparams*/ "", /*body*/ ""].join(":");
232
+ const mac = crypto.createHmac("sha256", rriot.h).update(prestr).digest("base64");
233
+
234
+ config.headers["Authorization"] = `Hawk id="${rriot.u}", s="${rriot.s}", ts="${timestamp}", nonce="${nonce}", mac="${mac}"`;
235
+ }
236
+ } catch (error) {
237
+ this.log.error("Failed to initialize API. Error: " + error);
238
+ }
239
+ return config;
240
+ });
241
+
242
+ // Get home details.
243
+ try {
244
+ const homeDetail = await this.loginApi.get("api/v1/getHomeDetail");
245
+ if (homeDetail) {
246
+ const homeId = homeDetail.data.data.rrHomeId;
247
+
248
+ if (this.api) {
249
+ const homedata = await this.api.get(`v2/user/homes/${homeId}`);
250
+ const homedataResult = homedata.data.result;
251
+
252
+ const scene = await this.api.get(`user/scene/home/${homeId}`);
253
+
254
+ await this.setStateAsync("HomeData", {
255
+ val: JSON.stringify(homedataResult),
256
+ ack: true,
257
+ });
258
+
259
+ // skip devices that sn in ingoredDevices
260
+ const ignoredDevices = this.config.ignoredDevices || [];
261
+ // create devices and set states
262
+ this.products = homedataResult.products;
263
+ this.devices = homedataResult.devices;
264
+ this.devices = this.devices.filter((device) => !ignoredDevices.includes(device.sn));
265
+ this.localKeys = new Map(this.devices.map((device) => [device.duid, device.localKey]));
266
+
267
+ // this.adapter.log.debug(`initUser test: ${JSON.stringify(Array.from(this.adapter.localKeys.entries()))}`);
268
+
269
+ await this.rr_mqtt_connector.initUser(userdata);
270
+ await this.rr_mqtt_connector.initMQTT_Subscribe();
271
+ await this.rr_mqtt_connector.initMQTT_Message();
272
+
273
+ // store name of each room via ID
274
+ const rooms = homedataResult.rooms;
275
+ for (const room in rooms) {
276
+ const roomID = rooms[room].id;
277
+ const roomName = rooms[room].name;
278
+
279
+ this.roomIDs[roomID] = roomName;
280
+ }
281
+ this.log.debug(`RoomIDs debug: ${JSON.stringify(this.roomIDs)}`);
282
+
283
+ // reconnect every 3 hours (10800 seconds)
284
+ this.reconnectIntervall = this.setInterval(async () => {
285
+ this.log.debug(`Reconnecting after 3 hours!`);
286
+
287
+ await this.rr_mqtt_connector.reconnectClient();
288
+ }, 3600 * 1000);
289
+
290
+ this.processScene(scene);
291
+
292
+ this.homedataInterval = this.setInterval(this.updateHomeData.bind(this), this.updateInterval * 1000, homeId);
293
+ await this.updateHomeData(homeId);
294
+
295
+ const discoveredDevices = await this.localConnector.getLocalDevices();
296
+
297
+ await this.createDevices();
298
+ await this.getNetworkInfo();
299
+
300
+ // merge udp discovered devices with local devices found via mqtt
301
+ Object.entries(discoveredDevices).forEach(([duid, ip]) => {
302
+
303
+ if (!Object.prototype.hasOwnProperty.call(this.localDevices, duid)) {
304
+ this.localDevices[duid] = ip;
305
+ }
306
+
307
+
308
+ });
309
+ this.log.debug(`localDevices: ${JSON.stringify(this.localDevices)}`);
310
+
311
+ for (const device in this.localDevices) {
312
+ const duid = device;
313
+ const ip = this.localDevices[device];
314
+
315
+ await this.localConnector.createClient(duid, ip);
316
+ }
317
+
318
+ await this.initializeDeviceUpdates();
319
+ this.bInited = true;
320
+ this.log.info(`Starting adapter finished. Lets go!!!!!!!`);
321
+
322
+ } else {
323
+ this.log.info(`Most likely failed to login. Deleting UserData to force new login!`);
324
+ await this.deleteStateAsync(`UserData`);
325
+
326
+
327
+ }
328
+ }
329
+ } catch (error) {
330
+ this.log.error("Failed to get home details: " + error.stack);
331
+ }
332
+
333
+ if(callback){
334
+ callback();
335
+ }
336
+
337
+ }
338
+
339
+
340
+ async stopService() {
341
+
342
+ try {
343
+ await this.clearTimersAndIntervals();
344
+ this.bInited = false;
345
+ } catch (e) {
346
+ this.catchError(e.stack);
347
+ }
348
+
349
+ }
350
+
351
+ async getUserData(loginApi) {
352
+ try {
353
+ const response = await loginApi.post(
354
+ "api/v1/login",
355
+ new URLSearchParams({
356
+ username: this.config.username,
357
+ password: this.config.password,
358
+ needtwostepauth: "false",
359
+ }).toString()
360
+ );
361
+ const userdata = response.data.data;
362
+
363
+ if (!userdata) {
364
+ throw new Error("Login returned empty userdata.");
365
+ }
366
+
367
+ await this.setStateAsync("UserData", {
368
+ val: JSON.stringify(userdata),
369
+ ack: true,
370
+ });
371
+
372
+ return userdata;
373
+ } catch (error) {
374
+ this.log.error(`Error in getUserData: ${error.message}`);
375
+ await this.deleteStateAsync("HomeData");
376
+ await this.deleteStateAsync("UserData");
377
+ throw error;
378
+ }
379
+ }
380
+
381
+ async getNetworkInfo() {
382
+ const devices = this.devices;
383
+ for (const device in devices) {
384
+ const duid = devices[device].duid;
385
+ const vacuum = this.vacuums[duid];
386
+ await vacuum.getParameter(duid, "get_network_info");
387
+ }
388
+ }
389
+
390
+ async createDevices() {
391
+ const devices = this.devices;
392
+
393
+ for (const device of devices) {
394
+ const duid = device.duid;
395
+ const name = device.name;
396
+
397
+ this.log.debug(`Creating device: ${name} with duid: ${duid}`);
398
+
399
+ const robotModel = this.getProductAttribute(duid, "model");
400
+
401
+ this.vacuums[duid] = new vacuum_class(this, robotModel);
402
+ this.vacuums[duid].name = name;
403
+ this.vacuums[duid].features = new deviceFeatures(this, device.featureSet, device.newFeatureSet, duid);
404
+
405
+ await this.vacuums[duid].features.processSupportedFeatures();
406
+
407
+ await this.vacuums[duid].setUpObjects(duid);
408
+
409
+ // sub to all commands of this robot
410
+ this.subscribeStates("Devices." + duid + ".commands.*");
411
+ this.subscribeStates("Devices." + duid + ".reset_consumables.*");
412
+ this.subscribeStates("Devices." + duid + ".programs.startProgram");
413
+ this.subscribeStates("Devices." + duid + ".deviceInfo.online");
414
+ }
415
+ }
416
+
417
+ async initializeDeviceUpdates() {
418
+ this.log.debug(`initializeDeviceUpdates`);
419
+
420
+ const devices = this.devices;
421
+
422
+ for (const device of devices) {
423
+ const duid = device.duid;
424
+ const robotModel = this.getProductAttribute(duid);
425
+
426
+ this.vacuums[duid].mainUpdateInterval = () =>
427
+ this.setInterval(this.updateDataMinimumData.bind(this), this.updateInterval * 1000, duid, this.vacuums[duid], robotModel);
428
+
429
+ if (device.online) {
430
+ this.log.debug(`${duid} online. Starting mainUpdateInterval.`);
431
+ this.vacuums[duid].mainUpdateInterval(); // actually start mainUpdateInterval()
432
+ }
433
+
434
+ this.vacuums[duid].getStatusIntervall = () => this.setInterval(this.getStatus.bind(this), 1000, duid, this.vacuums[duid], robotModel);
435
+
436
+ if (device.online) {
437
+ this.log.debug(`${duid} online. Starting getStatusIntervall.`);
438
+ this.vacuums[duid].getStatusIntervall(); // actually start getStatusIntervall()
439
+ }
440
+
441
+ await this.updateDataExtraData(duid, this.vacuums[duid]);
442
+ await this.updateDataMinimumData(duid, this.vacuums[duid], robotModel);
443
+
444
+ await this.vacuums[duid].getCleanSummary(duid);
445
+
446
+ // get map once at start of adapter
447
+ await this.vacuums[duid].getMap(duid);
448
+ }
449
+ }
450
+
451
+
452
+ async processScene(scene) {
453
+ if (scene && scene.data.result) {
454
+ this.log.debug(`Processing scene ${JSON.stringify(scene.data.result)}`);
455
+
456
+ const programs = {};
457
+ for (const program in scene.data.result) {
458
+ const enabled = scene.data.result[program].enabled;
459
+ const programID = scene.data.result[program].id;
460
+ const programName = scene.data.result[program].name;
461
+ const param = scene.data.result[program].param;
462
+
463
+ this.log.debug(`Processing scene param ${param}`);
464
+ const duid = JSON.parse(param).action.items[0].entityId;
465
+
466
+ if (!programs[duid]) {
467
+ programs[duid] = {};
468
+ }
469
+ programs[duid][programID] = programName;
470
+
471
+ await this.setObjectNotExistsAsync(`Devices.${duid}.programs`, {
472
+ type: "folder",
473
+ common: {
474
+ name: "Programs",
475
+ },
476
+ native: {},
477
+ });
478
+
479
+ await this.setObjectAsync(`Devices.${duid}.programs.${programID}`, {
480
+ type: "folder",
481
+ common: {
482
+ name: programName,
483
+ },
484
+ native: {},
485
+ });
486
+
487
+ const enabledPath = `Devices.${duid}.programs.${programID}.enabled`;
488
+ await this.createStateObjectHelper(enabledPath, "enabled", "boolean", null, null, "value");
489
+ this.setStateAsync(enabledPath, enabled, true);
490
+
491
+ const items = JSON.parse(param).action.items;
492
+ for (const item in items) {
493
+ for (const attribute in items[item]) {
494
+ const objectPath = `Devices.${duid}.programs.${programID}.items.${item}.${attribute}`;
495
+ let value = items[item][attribute];
496
+ const typeOfValue = typeof value;
497
+
498
+ await this.createStateObjectHelper(objectPath, attribute, typeOfValue, null, null, "value", true, false);
499
+
500
+ if (typeOfValue == "object") {
501
+ value = value.toString();
502
+ }
503
+ this.setStateAsync(objectPath, value, true);
504
+ }
505
+ }
506
+ }
507
+
508
+ for (const duid in programs) {
509
+ const objectPath = `Devices.${duid}.programs.startProgram`;
510
+ await this.createStateObjectHelper(objectPath, "Start saved program", "string", null, Object.keys(programs[duid])[0], "value", true, true, programs[duid]);
511
+ }
512
+ }
513
+ }
514
+
515
+ async executeScene(sceneID) {
516
+ if (this.api) {
517
+ try {
518
+ await this.api.post(`user/scene/${sceneID.val}/execute`);
519
+ } catch (error) {
520
+ this.catchError(error.stack, "executeScene");
521
+ }
522
+ }
523
+ }
524
+
525
+ async startMapUpdater(duid) {
526
+ if (!this.vacuums[duid].mapUpdater) {
527
+ this.log.debug(`Started map updater on robot: ${duid}`);
528
+ this.vacuums[duid].mapUpdater = this.setInterval(() => {
529
+ this.vacuums[duid].getMap(duid);
530
+ }, this.config.map_creation_interval * 1000);
531
+ } else {
532
+ this.log.debug(`Map updater on robot: ${duid} already running!`);
533
+ }
534
+ }
535
+
536
+ async stopMapUpdater(duid) {
537
+ this.log.debug(`Stopping map updater on robot: ${duid}`);
538
+
539
+ if (this.vacuums[duid].mapUpdater) {
540
+ this.clearInterval(this.vacuums[duid].mapUpdater);
541
+ this.vacuums[duid].mapUpdater = null;
542
+
543
+ await this.vacuums[duid].getCleanSummary(duid);
544
+ }
545
+ }
546
+
547
+ getProductAttribute(duid, attribute) {
548
+ const products = this.products;
549
+ const productID = this.devices.find((device) => device.duid == duid).productId;
550
+ const product = products.find((product) => product.id == productID);
551
+
552
+ return product ? product[attribute] : null;
553
+ }
554
+
555
+ startMainUpdateInterval(duid, online) {
556
+ const robotModel = this.getProductAttribute(duid, "model");
557
+
558
+ this.vacuums[duid].mainUpdateInterval = () =>
559
+ this.setInterval(this.updateDataMinimumData.bind(this), this.updateInterval * 1000, duid, this.vacuums[duid], robotModel);
560
+ if (online) {
561
+ this.log.debug(`${duid} online. Starting mainUpdateInterval.`);
562
+ this.vacuums[duid].mainUpdateInterval(); // actually start mainUpdateInterval()
563
+ // Map updater gets startet automatically via getParameter with get_status
564
+ }
565
+ }
566
+
567
+ decodeSniffedMessage(data, devices) {
568
+ const dataString = JSON.stringify(data);
569
+
570
+ const duidMatch = dataString.match(/\/(\w+)\.\w{3}'/);
571
+ if (duidMatch) {
572
+ const duidSniffed = duidMatch[1];
573
+
574
+ const device = devices.find((device) => device.duid === duidSniffed);
575
+ if (device) {
576
+ const localKey = device.localKey;
577
+
578
+ const payloadMatch = dataString.match(/'([a-fA-F0-9]+)'/);
579
+ if (payloadMatch) {
580
+ const hexPayload = payloadMatch[1];
581
+ const msg = Buffer.from(hexPayload, "hex");
582
+
583
+ const decodedMessage = this.message._decodeMsg(msg, localKey);
584
+ this.log.debug(`Decoded sniffing message: ${JSON.stringify(JSON.parse(decodedMessage.payload))}`);
585
+ }
586
+ }
587
+ }
588
+ }
589
+
590
+ async onlineChecker(duid) {
591
+
592
+ const homedata = await this.getStateAsync("HomeData");
593
+
594
+ // If the home data is not found or if its value is not a string, return false.
595
+ if (homedata && typeof homedata.val == "string") {
596
+ const homedataJSON = JSON.parse(homedata.val);
597
+ const device = homedataJSON.devices.find((device) => device.duid == duid);
598
+ const receivedDevice = homedataJSON.receivedDevices.find((device) => device.duid == duid);
599
+
600
+ // If the device is not found, return false.
601
+ if (!device && !receivedDevice) {
602
+ return false;
603
+ }
604
+
605
+ return device?.online || receivedDevice?.online;
606
+ } else {
607
+ return false;
608
+ }
609
+ }
610
+
611
+ async isRemoteDevice(duid) {
612
+ const homedata = await this.getStateAsync("HomeData");
613
+
614
+ if (homedata && typeof homedata.val == "string") {
615
+ const homedataJSON = JSON.parse(homedata.val);
616
+ const receivedDevice = homedataJSON.receivedDevices.find((device) => device.duid == duid);
617
+ const remoteDevice = this.remoteDevices.has(duid);
618
+
619
+ if (receivedDevice || remoteDevice) {
620
+ return true;
621
+ }
622
+
623
+ return false;
624
+ } else {
625
+ return false;
626
+ }
627
+ }
628
+
629
+ async getConnector(duid) {
630
+ const isRemote = await this.isRemoteDevice(duid);
631
+
632
+ if (isRemote) {
633
+ return this.rr_mqtt_connector;
634
+ } else {
635
+ return this.localConnector;
636
+ }
637
+ }
638
+
639
+ async manageDeviceIntervals(duid) {
640
+ return this.onlineChecker(duid)
641
+ .then((onlineState) => {
642
+ if (!onlineState && this.vacuums[duid].mainUpdateInterval) {
643
+ this.clearInterval(this.vacuums[duid].getStatusIntervall);
644
+ this.clearInterval(this.vacuums[duid].mainUpdateInterval);
645
+ this.clearInterval(this.vacuums[duid].mapUpdater);
646
+ } else if (!this.vacuums[duid].mainUpdateInterval) {
647
+ this.vacuums[duid].getStatusIntervall();
648
+ this.startMainUpdateInterval(duid, onlineState);
649
+ }
650
+ return onlineState;
651
+ })
652
+ .catch((error) => {
653
+ this.log.error("startStopIntervals " + error);
654
+
655
+ return false; // Make device appear as offline on error. Just in case.
656
+ });
657
+ }
658
+
659
+ async updateDataMinimumData(duid, vacuum, robotModel) {
660
+ this.log.debug(`Latest data requested`);
661
+
662
+ if (robotModel == "roborock.wm.a102") {
663
+ // nothing for now
664
+ } else if (robotModel == "roborock.wetdryvac.a56") {
665
+ // nothing for now
666
+ } else {
667
+ await vacuum.getParameter(duid, "get_room_mapping");
668
+
669
+ await vacuum.getParameter(duid, "get_consumable");
670
+
671
+ await vacuum.getParameter(duid, "get_server_timer");
672
+
673
+ await vacuum.getParameter(duid, "get_timer");
674
+
675
+ await this.checkForNewFirmware(duid);
676
+
677
+ switch (robotModel) {
678
+ case "roborock.vacuum.s4":
679
+ case "roborock.vacuum.s5":
680
+ case "roborock.vacuum.s5e":
681
+ case "roborock.vacuum.a08":
682
+ case "roborock.vacuum.a10":
683
+ case "roborock.vacuum.a40":
684
+ //do nothing
685
+ break;
686
+ case "roborock.vacuum.s6":
687
+ await vacuum.getParameter(duid, "get_carpet_mode");
688
+ break;
689
+ case "roborock.vacuum.a27":
690
+ await vacuum.getParameter(duid, "get_dust_collection_switch_status");
691
+ await vacuum.getParameter(duid, "get_wash_towel_mode");
692
+ await vacuum.getParameter(duid, "get_smart_wash_params");
693
+ await vacuum.getParameter(duid, "app_get_dryer_setting");
694
+ break;
695
+ default:
696
+ await vacuum.getParameter(duid, "get_carpet_mode");
697
+ await vacuum.getParameter(duid, "get_carpet_clean_mode");
698
+ await vacuum.getParameter(duid, "get_water_box_custom_mode");
699
+ }
700
+ }
701
+ }
702
+
703
+ async updateDataExtraData(duid, vacuum) {
704
+ await vacuum.getParameter(duid, "get_fw_features");
705
+
706
+ await vacuum.getParameter(duid, "get_multi_maps_list");
707
+ }
708
+
709
+ clearTimersAndIntervals() {
710
+ if (this.reconnectIntervall) {
711
+ this.clearInterval(this.reconnectIntervall);
712
+ }
713
+ if (this.homedataInterval) {
714
+ this.clearInterval(this.homedataInterval);
715
+ }
716
+ if (this.commandTimeout) {
717
+ this.clearTimeout(this.commandTimeout);
718
+ }
719
+
720
+ this.localConnector.clearLocalDevicedTimeout();
721
+
722
+ for (const duid in this.vacuums) {
723
+ this.clearInterval(this.vacuums[duid].getStatusIntervall);
724
+ this.clearInterval(this.vacuums[duid].mainUpdateInterval);
725
+ this.clearInterval(this.vacuums[duid].mapUpdater);
726
+ }
727
+
728
+ this.messageQueue.forEach(({ timeout102, timeout301 }) => {
729
+ this.clearTimeout(timeout102);
730
+ if (timeout301) {
731
+ this.clearTimeout(timeout301);
732
+ }
733
+ });
734
+
735
+ // Clear the messageQueue map
736
+ this.messageQueue.clear();
737
+
738
+ if (this.webSocketInterval) {
739
+ this.clearInterval(this.webSocketInterval);
740
+ }
741
+ }
742
+
743
+ checkAndClearRequest(requestId) {
744
+ const request = this.messageQueue.get(requestId);
745
+ if (!request?.timeout102 && !request?.timeout301) {
746
+ this.messageQueue.delete(requestId);
747
+ // this.log.debug(`Cleared messageQueue`);
748
+ } else {
749
+ this.log.debug(`Not clearing messageQueue. ${request.timeout102} - ${request.timeout301}`);
750
+ }
751
+ this.log.debug(`Length of message queue: ${this.messageQueue.size}`);
752
+ }
753
+
754
+ async updateHomeData(homeId) {
755
+ this.log.debug(`Updating HomeData with homeId: ${homeId}`);
756
+ if (this.api) {
757
+ try {
758
+ const home = await this.api.get(`user/homes/${homeId}`);
759
+ const homedata = home.data.result;
760
+
761
+ if (homedata) {
762
+ await this.setStateAsync("HomeData", {
763
+ val: JSON.stringify(homedata),
764
+ ack: true,
765
+ });
766
+ this.log.debug(`homedata successfully updated`);
767
+
768
+ await this.updateConsumablesPercent(homedata.devices);
769
+ await this.updateConsumablesPercent(homedata.receivedDevices);
770
+ await this.updateDeviceInfo(homedata.devices);
771
+ await this.updateDeviceInfo(homedata.receivedDevices);
772
+ } else {
773
+ this.log.warn("homedata failed to download");
774
+ }
775
+ } catch (error) {
776
+ this.log.error(`Failed to update updateHomeData with error: ${error}`);
777
+ }
778
+ }
779
+ }
780
+
781
+ async updateConsumablesPercent(devices) {
782
+ for (const device of devices) {
783
+ const duid = device.duid;
784
+ const deviceStatus = device.deviceStatus;
785
+
786
+ for (const [attribute, value] of Object.entries(deviceStatus)) {
787
+ const targetConsumable = await this.getObjectAsync(`Devices.${duid}.consumables.${attribute}`);
788
+
789
+ if (targetConsumable) {
790
+ const val = value >= 0 && value <= 100 ? parseInt(value) : 0;
791
+ await this.setStateAsync(`Devices.${duid}.consumables.${attribute}`, { val: val, ack: true });
792
+ }
793
+ }
794
+ }
795
+ }
796
+
797
+ async updateDeviceInfo(devices) {
798
+ for (const device in devices) {
799
+ const duid = devices[device].duid;
800
+
801
+ for (const deviceAttribute in devices[device]) {
802
+ if (typeof devices[device][deviceAttribute] != "object") {
803
+ let unit;
804
+ if (deviceAttribute == "activeTime") {
805
+ unit = "h";
806
+ devices[device][deviceAttribute] = Math.round(devices[device][deviceAttribute] / 1000 / 60 / 60);
807
+ }
808
+ await this.setObjectAsync("Devices." + duid + ".deviceInfo." + deviceAttribute, {
809
+ type: "state",
810
+ common: {
811
+ name: deviceAttribute,
812
+ type: this.getType(devices[device][deviceAttribute]),
813
+ unit: unit,
814
+ role: "value",
815
+ read: true,
816
+ write: false,
817
+ },
818
+ native: {},
819
+ });
820
+ this.setStateChangedAsync("Devices." + duid + ".deviceInfo." + deviceAttribute, { val: devices[device][deviceAttribute], ack: true });
821
+ }
822
+ }
823
+ }
824
+ }
825
+
826
+ async checkForNewFirmware(duid) {
827
+ const isLocalDevice = !this.isRemoteDevice(duid);
828
+
829
+ if (isLocalDevice) {
830
+ this.log.debug(`getting firmware status`);
831
+ if (this.api) {
832
+ try {
833
+ const update = await this.api.get(`ota/firmware/${duid}/updatev2`);
834
+
835
+ await this.setObjectNotExistsAsync("Devices." + duid + ".updateStatus", {
836
+ type: "folder",
837
+ common: {
838
+ name: "Update status",
839
+ },
840
+ native: {},
841
+ });
842
+
843
+ for (const state in update.data.result) {
844
+ await this.setObjectNotExistsAsync("Devices." + duid + ".updateStatus." + state, {
845
+ type: "state",
846
+ common: {
847
+ name: state,
848
+ type: this.getType(update.data.result[state]),
849
+ role: "value",
850
+ read: true,
851
+ write: false,
852
+ },
853
+ native: {},
854
+ });
855
+ this.setStateAsync("Devices." + duid + ".updateStatus." + state, {
856
+ val: update.data.result[state],
857
+ ack: true,
858
+ });
859
+ }
860
+ } catch (error) {
861
+ this.catchError(error, "checkForNewFirmware()", duid);
862
+ }
863
+ }
864
+ }
865
+ }
866
+
867
+ getType(attribute) {
868
+ // Get the type of the attribute.
869
+ const type = typeof attribute;
870
+
871
+ // Return the appropriate string representation of the type.
872
+ switch (type) {
873
+ case "boolean":
874
+ return "boolean";
875
+ case "number":
876
+ return "number";
877
+ default:
878
+ return "string";
879
+ }
880
+ }
881
+
882
+ async createStateObjectHelper(path, name, type, unit, def, role, read, write, states, native = {}) {
883
+ const common = {
884
+ name: name,
885
+ type: type,
886
+ unit: unit,
887
+ role: role,
888
+ read: read,
889
+ write: write,
890
+ states: states,
891
+ };
892
+
893
+ if (def !== undefined && def !== null && def !== "") {
894
+ common.def = def;
895
+ }
896
+
897
+ this.setObjectAsync(path, {
898
+ type: "state",
899
+ common: common,
900
+ native: native,
901
+ });
902
+ }
903
+
904
+ async createCommand(duid, command, type, defaultState, states) {
905
+ const path = `Devices.${duid}.commands.${command}`;
906
+ const name = this.translations[command];
907
+
908
+ const common = {
909
+ name: name,
910
+ type: type,
911
+ role: "value",
912
+ read: true,
913
+ write: true,
914
+ def: defaultState,
915
+ states: states,
916
+ };
917
+
918
+ this.setObjectAsync(path, {
919
+ type: "state",
920
+ common: common,
921
+ native: {},
922
+ });
923
+ }
924
+
925
+ async createDeviceStatus(duid, state, type, states, unit) {
926
+ const path = `Devices.${duid}.deviceStatus.${state}`;
927
+ const name = this.translations[state];
928
+
929
+ const common = {
930
+ name: name,
931
+ type: type,
932
+ role: "value",
933
+ unit: unit,
934
+ read: true,
935
+ write: false,
936
+ states: states,
937
+ };
938
+
939
+ this.setObjectAsync(path, {
940
+ type: "state",
941
+ common: common,
942
+ native: {},
943
+ });
944
+ }
945
+
946
+ async createDockingStationObject(duid) {
947
+ for (const state of dockingStationStates) {
948
+ const path = `Devices.${duid}.dockingStationStatus.${state}`;
949
+ const name = this.translations[state];
950
+
951
+ this.setObjectNotExistsAsync(path, {
952
+ type: "state",
953
+ common: {
954
+ name: name,
955
+ type: "number",
956
+ role: "value",
957
+ read: true,
958
+ write: false,
959
+ states: { 0: "UNKNOWN", 1: "ERROR", 2: "OK" },
960
+ },
961
+ native: {},
962
+ });
963
+ }
964
+ }
965
+
966
+ async createConsumable(duid, state, type, states, unit) {
967
+ const path = `Devices.${duid}.consumables.${state}`;
968
+ const name = this.translations[state];
969
+
970
+ const common = {
971
+ name: name,
972
+ type: type,
973
+ role: "value",
974
+ unit: unit,
975
+ read: true,
976
+ write: false,
977
+ states: states,
978
+ };
979
+
980
+ this.setObjectAsync(path, {
981
+ type: "state",
982
+ common: common,
983
+ native: {},
984
+ });
985
+ }
986
+
987
+ async createResetConsumables(duid, state) {
988
+ const path = `Devices.${duid}.resetConsumables.${state}`;
989
+ const name = this.translations[state];
990
+
991
+ this.setObjectNotExistsAsync(path, {
992
+ type: "state",
993
+ common: {
994
+ name: name,
995
+ type: "boolean",
996
+ role: "value",
997
+ read: true,
998
+ write: true,
999
+ def: false,
1000
+ },
1001
+ native: {},
1002
+ });
1003
+ }
1004
+
1005
+ async createCleaningRecord(duid, state, type, states, unit) {
1006
+ let start = 0;
1007
+ let end = 19;
1008
+ const robotModel = this.getProductAttribute(duid, "model");
1009
+ if (robotModel == "roborock.vacuum.a97") {
1010
+ start = 1;
1011
+ end = 20;
1012
+ }
1013
+
1014
+ for (let i = start; i <= end; i++) {
1015
+ await this.setObjectAsync(`Devices.${duid}.cleaningInfo.records.${i}`, {
1016
+ type: "folder",
1017
+ common: {
1018
+ name: `Cleaning record ${i}`,
1019
+ },
1020
+ native: {},
1021
+ });
1022
+
1023
+ this.setObjectAsync(`Devices.${duid}.cleaningInfo.records.${i}.${state}`, {
1024
+ type: "state",
1025
+ common: {
1026
+ name: this.translations[state],
1027
+ type: type,
1028
+ role: "value",
1029
+ unit: unit,
1030
+ read: true,
1031
+ write: false,
1032
+ states: states,
1033
+ },
1034
+ native: {},
1035
+ });
1036
+
1037
+ await this.setObjectAsync(`Devices.${duid}.cleaningInfo.records.${i}.map`, {
1038
+ type: "folder",
1039
+ common: {
1040
+ name: "Map",
1041
+ },
1042
+ native: {},
1043
+ });
1044
+ for (const name of ["mapBase64", "mapBase64Truncated", "mapData"]) {
1045
+ const objectString = `Devices.${duid}.cleaningInfo.records.${i}.map.${name}`;
1046
+ await this.createStateObjectHelper(objectString, name, "string", null, null, "value", true, false);
1047
+ }
1048
+ }
1049
+ }
1050
+
1051
+ async createCleaningInfo(duid, key, object) {
1052
+ const path = `Devices.${duid}.cleaningInfo.${key}`;
1053
+ const name = this.translations[object.name];
1054
+
1055
+ this.setObjectAsync(path, {
1056
+ type: "state",
1057
+ common: {
1058
+ name: name,
1059
+ type: "number",
1060
+ role: "value",
1061
+ unit: object.unit,
1062
+ read: true,
1063
+ write: false,
1064
+ },
1065
+ native: {},
1066
+ });
1067
+ }
1068
+
1069
+ async createBaseRobotObjects(duid) {
1070
+ for (const name of ["mapBase64", "mapBase64Truncated", "mapData"]) {
1071
+ const objectString = `Devices.${duid}.map.${name}`;
1072
+ await this.createStateObjectHelper(objectString, name, "string", null, null, "value", true, false);
1073
+ }
1074
+
1075
+ this.createNetworkInfoObjects(duid);
1076
+ }
1077
+
1078
+ async createBasicVacuumObjects(duid) {
1079
+ this.createNetworkInfoObjects(duid);
1080
+ }
1081
+
1082
+ async createBasicWashingMachineObjects(duid) {
1083
+ this.createNetworkInfoObjects(duid);
1084
+ }
1085
+
1086
+ async createNetworkInfoObjects(duid) {
1087
+ for (const name of ["ssid", "ip", "mac", "bssid", "rssi"]) {
1088
+ const objectString = `Devices.${duid}.networkInfo.${name}`;
1089
+ const objectType = name == "rssi" ? "number" : "string";
1090
+ await this.createStateObjectHelper(objectString, name, objectType, null, null, "value", true, false);
1091
+ }
1092
+ }
1093
+
1094
+ async startCommand(duid, command, parameters) {
1095
+
1096
+ if(!this.isInited()){
1097
+ this.log.warn("Adapter not inited. Command not executed.");
1098
+ return;
1099
+ }
1100
+
1101
+
1102
+ switch (command) {
1103
+ case "app_zoned_clean":
1104
+ case "app_goto_target":
1105
+ case "app_start":
1106
+ case "app_stop":
1107
+ case "stop_zoned_clean":
1108
+ case "app_pause":
1109
+ case "app_charge":
1110
+ this.vacuums[duid].command(duid, command, parameters);
1111
+ break;
1112
+
1113
+ case "getMap":
1114
+ this.vacuums[duid].getMap(duid);
1115
+ break;
1116
+
1117
+ case "get_photo":
1118
+ this.vacuums[duid].getParameter(duid, "get_photo", parameters);
1119
+ break;
1120
+ case "sniffing_decrypt":
1121
+ await this.getStateAsync("HomeData")
1122
+ .then((homedata) => {
1123
+ if (homedata) {
1124
+ const homedataVal = homedata.val;
1125
+ if (typeof homedataVal == "string") {
1126
+ // this.log.debug("Sniffing message received!");
1127
+ const homedataParsed = JSON.parse(homedataVal);
1128
+
1129
+ this.decodeSniffedMessage(data, homedataParsed.devices);
1130
+ this.decodeSniffedMessage(data, homedataParsed.receivedDevices);
1131
+ }
1132
+ }
1133
+ })
1134
+ .catch((error) => {
1135
+ this.log.error("Failed to decode/decrypt sniffing message. " + error);
1136
+
1137
+ });
1138
+
1139
+ break;
1140
+ default:
1141
+ this.log.warn(`Command ${command} not found.`);
1142
+
1143
+ }
1144
+ }
1145
+
1146
+
1147
+ isCleaning(state) {
1148
+ switch (state) {
1149
+ case 4: // Remote Control
1150
+ case 5: // Cleaning
1151
+ case 6: // Returning Dock
1152
+ case 7: // Manual Mode
1153
+ case 11: // Spot Cleaning
1154
+ case 15: // Docking
1155
+ case 16: // Go To
1156
+ case 17: // Zone Clean
1157
+ case 18: // Room Clean
1158
+ case 26: // Going to wash the mop
1159
+ return true;
1160
+ default:
1161
+ return false;
1162
+ }
1163
+ }
1164
+
1165
+ async getRobotVersion(duid) {
1166
+ const homedata = await this.getStateAsync("HomeData");
1167
+ if (homedata && homedata.val) {
1168
+ const devices = JSON.parse(homedata.val.toString()).devices.concat(JSON.parse(homedata.val.toString()).receivedDevices);
1169
+
1170
+ for (const device in devices) {
1171
+ if (devices[device].duid == duid) return devices[device].pv;
1172
+ }
1173
+ }
1174
+
1175
+ return "Error in getRobotVersion. Version not found.";
1176
+ }
1177
+
1178
+ getRequestId() {
1179
+ if (this.idCounter >= 9999) {
1180
+ this.idCounter = 0;
1181
+ return this.idCounter;
1182
+ }
1183
+ return this.idCounter++;
1184
+ }
1185
+
1186
+ async setupBasicObjects() {
1187
+ await this.setObjectAsync("Devices", {
1188
+ type: "folder",
1189
+ common: {
1190
+ name: "Devices",
1191
+ },
1192
+ native: {},
1193
+ });
1194
+
1195
+ await this.setObjectAsync("UserData", {
1196
+ type: "state",
1197
+ common: {
1198
+ name: "UserData string",
1199
+ type: "string",
1200
+ role: "value",
1201
+ read: true,
1202
+ write: false,
1203
+ },
1204
+ native: {},
1205
+ });
1206
+
1207
+ await this.setObjectAsync("HomeData", {
1208
+ type: "state",
1209
+ common: {
1210
+ name: "HomeData string",
1211
+ type: "string",
1212
+ role: "value",
1213
+ read: true,
1214
+ write: false,
1215
+ },
1216
+ native: {},
1217
+ });
1218
+
1219
+ await this.setObjectAsync("clientID", {
1220
+ type: "state",
1221
+ common: {
1222
+ name: "Client ID",
1223
+ type: "string",
1224
+ role: "value",
1225
+ read: true,
1226
+ write: false,
1227
+ },
1228
+ native: {},
1229
+ });
1230
+ }
1231
+
1232
+ async catchError(error, attribute, duid, model) {
1233
+ if (error) {
1234
+ if (error.toString().includes("retry") || error.toString().includes("locating") || error.toString().includes("timed out after 10 seconds")) {
1235
+ this.log.warn(`Failed to execute ${attribute} on robot ${duid} (${model || "unknown model"}): ${error}`);
1236
+ } else {
1237
+ this.log.error(`Failed to execute ${attribute} on robot ${duid} (${model || "unknown model"}): ${error.stack || error}`);
1238
+ }
1239
+ }
1240
+ }
1241
+
1242
+ async app_start(duid){
1243
+ await this.startCommand(duid, "app_start", null);
1244
+ }
1245
+
1246
+ async app_stop(duid){
1247
+ await this.startCommand(duid, "app_stop", null);
1248
+ }
1249
+
1250
+ async app_charge(duid){
1251
+ await this.startCommand(duid, "app_charge", null);
1252
+ }
1253
+
1254
+ async getStatus(duid, vacuum) {
1255
+ await vacuum.getParameter(duid, "get_status");
1256
+ }
1257
+
1258
+ async getStatus(duid) {
1259
+
1260
+ try{
1261
+ await this.vacuums[duid].getParameter(duid, "get_status", "state");
1262
+ }catch(error){
1263
+ this.catchError(error, "getStatus", duid);
1264
+ }
1265
+ }
1266
+
1267
+ getProductData(productId) {
1268
+
1269
+ const homedata = this.getStateAsync("HomeData");
1270
+
1271
+ if (homedata && typeof homedata.val == "string") {
1272
+ const homedataJSON = JSON.parse(homedata.val);
1273
+ const product = homedataJSON.products.find((product) => product.id == productId);
1274
+
1275
+ return product;
1276
+ }
1277
+ }
1278
+
1279
+ getVacuumDeviceData(duid) {
1280
+ const homedata = this.getStateAsync("HomeData");
1281
+
1282
+ if (homedata && typeof homedata.val == "string") {
1283
+ const homedataJSON = JSON.parse(homedata.val);
1284
+ const device = homedataJSON.devices.find((device) => device.duid == duid);
1285
+ const receivedDevice = homedataJSON.receivedDevices.find((device) => device.duid == duid);
1286
+
1287
+ return device || receivedDevice;
1288
+ }
1289
+ }
1290
+
1291
+ getVacuumSchemaId(duid, code) {
1292
+
1293
+ const productId = this.getVacuumDeviceInfo(duid, "productId");
1294
+ const product = this.getProductData(productId);
1295
+
1296
+ if (product) {
1297
+
1298
+ const schema = product.schema;
1299
+ const schemaId = schema.find((schema) => schema.code == code);
1300
+
1301
+ if (schemaId) {
1302
+ return schemaId.id;
1303
+ }
1304
+ }
1305
+
1306
+ return null;
1307
+
1308
+ }
1309
+
1310
+ getVacuumDeviceInfo(duid, property) {
1311
+
1312
+ const device = this.getVacuumDeviceData(duid);
1313
+
1314
+ if (device) {
1315
+ return device[property];
1316
+ } else {
1317
+ return "";
1318
+ }
1319
+ }
1320
+
1321
+ getVacuumDeviceStatus(duid, property) {
1322
+
1323
+ const propertyID = this.getVacuumSchemaId(duid, property);
1324
+
1325
+ if(propertyID == null){
1326
+ return "";
1327
+ }
1328
+
1329
+ const device = this.getVacuumDeviceData(duid);
1330
+
1331
+ if (device.deviceStatus && device.deviceStatus[propertyID] != undefined) {
1332
+ return device.deviceStatus[propertyID];
1333
+ }
1334
+
1335
+ return "";
1336
+
1337
+
1338
+
1339
+ }
1340
+
1341
+ getVacuumList() {
1342
+
1343
+ const homedata = this.getStateAsync("HomeData");
1344
+
1345
+ if (homedata && typeof homedata.val == "string") {
1346
+ const homedataJSON = JSON.parse(homedata.val);
1347
+ const devices = homedataJSON.devices.concat(homedataJSON.receivedDevices);
1348
+
1349
+ return devices;
1350
+ }
1351
+
1352
+ return [];
1353
+
1354
+ }
1355
+
1356
+ setDeviceNotify(callback){
1357
+ this.deviceNotify = callback;
1358
+ }
1359
+
1360
+ }
1361
+
1362
+ module.exports = {Roborock};
1363
+
1364
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
1365
+