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,636 @@
1
+ "use strict";
2
+
3
+ const rrMessage = require("./message").message;
4
+ const RRMapParser = require("./RRMapParser");
5
+ const MapCreator = require("./mapCreator");
6
+ const fs = require("fs");
7
+ const zlib = require("zlib");
8
+
9
+ const mappedCleanSummary = {
10
+ 0: "clean_time",
11
+ 1: "clean_area",
12
+ 2: "clean_count",
13
+ 3: "records",
14
+ };
15
+
16
+ const mappedCleaningRecordAttribute = {
17
+ 0: "begin",
18
+ 1: "end",
19
+ 2: "duration",
20
+ 3: "area",
21
+ 4: "error",
22
+ 5: "complete",
23
+ 6: "start_type",
24
+ 7: "clean_type",
25
+ 8: "finish_reason",
26
+ 9: "dust_collection_status",
27
+ };
28
+
29
+ class vacuum {
30
+ constructor(adapter, robotModel) {
31
+ this.adapter = adapter;
32
+
33
+ this.adapter.log.debug(`Robot key: ${robotModel}`);
34
+ this.robotModel = robotModel;
35
+
36
+ this.message = new rrMessage(this.adapter);
37
+
38
+ this.mapParser = new RRMapParser(this.adapter);
39
+ this.mapCreator = new MapCreator(this.adapter);
40
+
41
+ this.parameterFolders = {
42
+ get_mop_mode: "deviceStatus",
43
+ get_water_box_custom_mode: "deviceStatus",
44
+ get_network_info: "networkInfo",
45
+ get_consumable: "consumables",
46
+ get_fw_features: "firmwareFeatures",
47
+ get_carpet_mode: "deviceStatus",
48
+ get_carpet_clean_mode: "deviceStatus",
49
+ get_carpet_cleaning_mode: "deviceStatus",
50
+ };
51
+ }
52
+
53
+ async getMap(duid) {
54
+ if (this.adapter.config.enable_map_creation) {
55
+ this.adapter.log.debug(`Update map`);
56
+
57
+ try {
58
+ // const map = await connector.sendRequest(duid, "get_map_v1", [], true);
59
+ // const map = await this.adapter.rr_mqtt_connector.sendRequest(duid, "get_map_v1", [], true);
60
+ const map = await this.adapter.messageQueueHandler.sendRequest(duid, "get_map_v1", [], true);
61
+ // this.adapter.log.debug(`Map received: ${map}`);
62
+ if (map != "retry") {
63
+ const mappedRooms = await this.adapter.messageQueueHandler.sendRequest(duid, "get_room_mapping", []);
64
+
65
+ // const deviceStatus = await this.adapter.messageQueueHandler.sendRequest(duid, "get_status", []);
66
+ const deviceStatus = await this.adapter.messageQueueHandler.sendRequest(duid, "get_prop", ["get_status"]);
67
+ const selectedMap = deviceStatus[0].map_status >> 2 ?? -1; // to get the currently selected map perform bitwise right shift
68
+
69
+ // This is for testing and debugging maps. This can't be stored in a state.
70
+ zlib.gzip(map, (error, buffer) => {
71
+ if (error) {
72
+ this.adapter.log.error(`Error compressing map to gz ${error}`);
73
+ } else {
74
+ fs.writeFile("./test.rrmap.gz", buffer, (error) => {
75
+ if (error) {
76
+ this.adapter.log.error(`Error writing map file ${error}`);
77
+ }
78
+ });
79
+ }
80
+ });
81
+
82
+ const parsedData = await this.mapParser.parsedata(map);
83
+
84
+ const [mapBase64, mapBase64Truncated] = this.mapCreator.canvasMap(parsedData, duid, selectedMap, mappedRooms);
85
+
86
+ await this.adapter.setStateAsync(`Devices.${duid}.map.mapData`, { val: JSON.stringify(parsedData), ack: true });
87
+ await this.adapter.setStateAsync(`Devices.${duid}.map.mapBase64`, { val: mapBase64, ack: true });
88
+ await this.adapter.setStateAsync(`Devices.${duid}.map.mapBase64Truncated`, { val: mapBase64Truncated, ack: true });
89
+
90
+ // Send current map with Scale factor
91
+ const mapToSend = {
92
+ duid: duid,
93
+ command: "map",
94
+ base64: mapBase64,
95
+ map: parsedData,
96
+ scale: this.adapter.config.map_scale,
97
+ };
98
+
99
+ if (this.adapter.socket != null) {
100
+ this.adapter.socket.send(JSON.stringify(mapToSend));
101
+ }
102
+ }
103
+ } catch (error) {
104
+ this.adapter.catchError(error, "get_map_v1", duid), this.robotModel;
105
+ }
106
+ }
107
+ }
108
+
109
+ async getCleaningRecordMap(duid, startTime) {
110
+ try {
111
+ const cleaningRecordMap = await this.adapter.messageQueueHandler.sendRequest(duid, "get_clean_record_map", { start_time: startTime }, true);
112
+ const parsedData = await this.mapParser.parsedata(cleaningRecordMap);
113
+ const [mapBase64, mapBase64Truncated] = this.mapCreator.canvasMap(parsedData, duid);
114
+
115
+ return {
116
+ mapBase64: mapBase64,
117
+ mapBase64Truncated: mapBase64Truncated,
118
+ mapData: JSON.stringify(parsedData),
119
+ };
120
+ } catch (error) {
121
+ this.adapter.catchError(error, "get_clean_record_map", duid, this.robotModel);
122
+
123
+ return null;
124
+ }
125
+ }
126
+
127
+ async command(duid, parameter, value) {
128
+ try {
129
+ switch (parameter) {
130
+ case "load_multi_map": {
131
+ const result = await this.adapter.messageQueueHandler.sendRequest(duid, "load_multi_map", value);
132
+
133
+ if (result[0] == "ok") {
134
+ await this.getMap(duid).then(async () => {
135
+ await this.getParameter(duid, "get_room_mapping");
136
+ });
137
+ }
138
+
139
+ break;
140
+ }
141
+ case "app_segment_clean": {
142
+ this.adapter.log.debug("Start room cleaning");
143
+
144
+ const roomList = {};
145
+ roomList.segments = [];
146
+ const roomFloor = await this.adapter.getStateAsync(`Devices.${duid}.deviceStatus.map_status`);
147
+ const mappedRoomList = await this.adapter.messageQueueHandler.sendRequest(duid, "get_room_mapping", []);
148
+
149
+ if (mappedRoomList) {
150
+ for (const mappedRoom in mappedRoomList) {
151
+ const roomState = await this.adapter.getStateAsync(`Devices.${duid}.floors.${roomFloor.val}.${mappedRoomList[mappedRoom][0]}`);
152
+
153
+ if (roomState.val) {
154
+ roomList.segments.push(mappedRoomList[mappedRoom][0]);
155
+ }
156
+ }
157
+ }
158
+
159
+ const cleanCount = await this.adapter.getStateAsync(`Devices.${duid}.floors.cleanCount`);
160
+ roomList["repeat"] = cleanCount.val;
161
+
162
+ const result = await this.adapter.messageQueueHandler.sendRequest(duid, "app_segment_clean", [roomList]);
163
+ this.adapter.log.debug(`app_segment_clean with roomIDs: ${JSON.stringify(roomList)} result: ${result}`);
164
+ this.adapter.setStateAsync(`Devices.${duid}.floors.cleanCount`, { val: 1, ack: true });
165
+
166
+ break;
167
+ }
168
+ case "reset_consumable":
169
+ await this.adapter.messageQueueHandler.sendRequest(duid, parameter, [value]);
170
+ this.adapter.log.info(`Consumable ${parameter} successfully reset.`);
171
+
172
+ break;
173
+
174
+ case "app_set_dryer_status": {
175
+ const result = await this.adapter.messageQueueHandler.sendRequest(duid, parameter, JSON.parse(value));
176
+ this.adapter.log.debug(`Command: ${parameter} result: ${result}`);
177
+
178
+ break;
179
+ }
180
+ case "app_goto_target":
181
+ case "app_zoned_clean": {
182
+ const result = await this.adapter.messageQueueHandler.sendRequest(duid, parameter, value);
183
+ this.adapter.log.debug(`Command: ${parameter} with value: ${JSON.stringify(value)} result: ${result}`);
184
+
185
+ break;
186
+ }
187
+ case "set_water_box_distance_off": {
188
+ const mappedValue = ((value - 1) / (30 - 1)) * (60 - 205) + 205;
189
+ const parameterValue = { distance_off: mappedValue };
190
+
191
+ const result = await this.adapter.messageQueueHandler.sendRequest(duid, parameter, parameterValue);
192
+ this.adapter.log.debug(`Command: ${parameter} with value: ${JSON.stringify(parameterValue)} result: ${result}`);
193
+ break;
194
+ }
195
+ default:
196
+ if (value && typeof value !== "boolean") {
197
+ const valueType = typeof value;
198
+
199
+ if (valueType === "string") {
200
+ value = await JSON.parse(value);
201
+ } else if (valueType === "number") {
202
+ value = [value];
203
+ }
204
+
205
+ // await is important here!!! Wait for the command to finish before sending the request to update deviceConfig!!!
206
+ const result = await this.adapter.messageQueueHandler.sendRequest(duid, parameter, value);
207
+ this.adapter.log.debug(`Command: ${parameter} with value: ${JSON.stringify(value)} result: ${result}`);
208
+
209
+ // this is needed to update the states instantly after sending a command
210
+ const getCommand = parameter.replace("set", "get");
211
+ await this.getParameter(duid, getCommand);
212
+ } else {
213
+ const result = await this.adapter.messageQueueHandler.sendRequest(duid, parameter);
214
+ this.adapter.log.debug(`Command: ${parameter} result: ${result}`);
215
+ }
216
+ }
217
+ } catch (error) {
218
+ this.adapter.catchError(error, parameter, duid, this.robotModel);
219
+ }
220
+ }
221
+
222
+ async getParameter(duid, parameter, attribute) {
223
+ let mode;
224
+
225
+ try {
226
+ if (parameter == "get_network_info") {
227
+ mode = parameter;
228
+ const networkInfo = await this.adapter.messageQueueHandler.sendRequest(duid, parameter, []);
229
+
230
+ for (const attribute in networkInfo) {
231
+ if (attribute == "ip" && !(await this.adapter.isRemoteDevice(duid))) {
232
+ this.adapter.localDevices[duid] = networkInfo[attribute];
233
+ }
234
+ this.adapter.setStateAsync(`Devices.${duid}.networkInfo.${attribute}`, { val: networkInfo[attribute], ack: true });
235
+ }
236
+ } else if (parameter == "get_consumable") {
237
+ const consumables = (await this.adapter.messageQueueHandler.sendRequest(duid, "get_consumable", []))[0];
238
+
239
+ for (const consumable in consumables) {
240
+ const divider = this.adapter.vacuums[duid].features.getConsumablesDivider(consumable);
241
+ if (divider) {
242
+ const consumable_val = divider ? Math.round(consumables[consumable] / divider) : consumables[consumable];
243
+
244
+ this.adapter.setStateAsync(`Devices.${duid}.consumables.${consumable}`, { val: consumable_val, ack: true });
245
+ }
246
+ }
247
+ } else if (parameter == "get_status") {
248
+ const now = new Date();
249
+ const seconds = now.getSeconds();
250
+
251
+ if (this.adapter.socket || seconds % this.adapter.config.updateInterval == 0) {
252
+ // only send status every minute or if websocket is connected
253
+
254
+ // const deviceStatus = await this.adapter.messageQueueHandler.sendRequest(duid, "get_status", []);
255
+ const deviceStatus = await this.adapter.messageQueueHandler.sendRequest(duid, "get_prop", ["get_status"]);
256
+
257
+ for (const attribute in deviceStatus[0]) {
258
+ const isCleaning = this.adapter.isCleaning(deviceStatus[0]["state"]);
259
+
260
+ if (!(await this.adapter.getObjectAsync(`Devices.${duid}.deviceStatus.${attribute}`))) {
261
+ this.adapter.log.warn(
262
+ `Unsupported attribute: ${attribute} of get_status with value ${deviceStatus[0][attribute]}. Please contact the dev to add the newly found attribute of your robot. Model: ${this.robotModel}`
263
+ );
264
+ continue; // skip unsupported attributes
265
+ }
266
+
267
+ const divider = this.adapter.vacuums[duid].features.getStatusDivider(attribute);
268
+ if (divider) {
269
+ deviceStatus[0][attribute] = Math.round(deviceStatus[0][attribute] / divider);
270
+ }
271
+
272
+ if (typeof deviceStatus[0][attribute] == "object") {
273
+ deviceStatus[0][attribute] = JSON.stringify(deviceStatus[0][attribute]);
274
+ }
275
+
276
+ switch (attribute) {
277
+ case "dock_type":
278
+ this.adapter.vacuums[duid].features.processDockType(attribute);
279
+ break;
280
+ case "dss":
281
+ await this.adapter.createDockingStationObject(duid);
282
+ const dockingStationStatus = await this.parseDockingStationStatus(deviceStatus[0][attribute]);
283
+
284
+ for (const state in dockingStationStatus) {
285
+ this.adapter.setStateAsync(`Devices.${duid}.dockingStationStatus.${state}`, { val: parseInt(dockingStationStatus[state]), ack: true });
286
+ }
287
+ break;
288
+ case "map_status": {
289
+ deviceStatus[0][attribute] = deviceStatus[0][attribute] >> 2 ?? -1; // to get the currently selected map perform bitwise right shift
290
+
291
+ if (isCleaning) {
292
+ this.adapter.startMapUpdater(duid);
293
+ } else if (!isCleaning) {
294
+ this.adapter.stopMapUpdater(duid);
295
+ } else {
296
+ const mapCount = await this.adapter.getStateAsync(`Devices.${duid}.floors.multi_map_count`);
297
+
298
+ // don't process load_multi_map for single level configuration
299
+ if (mapCount) {
300
+ // sometimes mapCount is not available shortly after first start of adapter
301
+ if (mapCount.val > 1) {
302
+ const currentMap = deviceStatus[0][attribute];
303
+ const mapFromCommand = await this.adapter.getState(`Devices.${duid}.commands.load_multi_map`);
304
+
305
+ if (mapFromCommand && mapFromCommand.val != currentMap) {
306
+ await this.adapter.setStateAsync(`Devices.${duid}.commands.load_multi_map`, currentMap, true);
307
+ await this.adapter.vacuums[duid].getMap(duid);
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ break;
314
+ }
315
+ case "state": {
316
+ if (this.adapter.socket) {
317
+ const sendValue = { duid: duid, command: "get_status", parameters: { isCleaning: isCleaning } };
318
+ this.adapter.socket.send(JSON.stringify(sendValue));
319
+ }
320
+
321
+ break;
322
+ }
323
+ case "last_clean_t":
324
+ deviceStatus[0][attribute] = new Date(deviceStatus[0][attribute]).toString();
325
+
326
+ break;
327
+ }
328
+ this.adapter.setStateChangedAsync(`Devices.${duid}.deviceStatus.${attribute}`, { val: deviceStatus[0][attribute], ack: true });
329
+ }
330
+ this.adapter.manageDeviceIntervals(duid);
331
+ }
332
+ } else if (parameter == "get_room_mapping") {
333
+ const deviceStatus = await this.adapter.messageQueueHandler.sendRequest(duid, "get_status", []);
334
+ const roomFloor = deviceStatus[0]["map_status"] >> 2 ?? -1; // to get the currently selected map perform bitwise right shift
335
+ const mappedRooms = await this.adapter.messageQueueHandler.sendRequest(duid, "get_room_mapping", []);
336
+
337
+ // if no rooms have been named, processing them can't work
338
+ if (mappedRooms.length < 1) {
339
+ this.adapter.log.warn(`Failed to map rooms. You need to name your rooms via the mobile app on your phone.`);
340
+ } else {
341
+ for (const mappedRoom of mappedRooms) {
342
+ const roomID = mappedRoom[1];
343
+ const roomName = this.adapter.roomIDs[roomID];
344
+
345
+ if (roomName) {
346
+ this.adapter.log.debug(`Mapped room matched: ${roomID} with name: ${roomName}`);
347
+ const objectString = `Devices.${duid}.floors.${roomFloor}.${mappedRoom[0]}`;
348
+ await this.adapter.createStateObjectHelper(objectString, roomName, "boolean", null, true, "value", true, true);
349
+ }
350
+ }
351
+ }
352
+
353
+ const objectString = `Devices.${duid}.floors.cleanCount`;
354
+ await this.adapter.createStateObjectHelper(objectString, "Clean count", "number", null, 1, "value", true, true);
355
+ } else if (parameter == "get_multi_maps_list") {
356
+ const mapList = await this.adapter.messageQueueHandler.sendRequest(duid, "get_multi_maps_list", []);
357
+ const mapInfo = mapList[0].map_info;
358
+ const maps = {};
359
+
360
+ // Set states for numeric parameters
361
+ for (const mapParameter in mapList[0]) {
362
+ if (typeof mapList[0][mapParameter] === "number") {
363
+ const statePath = `Devices.${duid}.floors.${mapParameter}`;
364
+ this.adapter.setStateAsync(statePath, { val: mapList[0][mapParameter], ack: true });
365
+ }
366
+ }
367
+
368
+ // Create map folders
369
+ for (const map in mapInfo) {
370
+ const roomFloor = mapInfo[map]["mapFlag"];
371
+ const mapName = mapInfo[map]["name"];
372
+ maps[roomFloor] = mapName;
373
+
374
+ const objectPath = `Devices.${duid}.floors.${roomFloor}`;
375
+ this.adapter.setObjectAsync(objectPath, {
376
+ type: "folder",
377
+ common: {
378
+ name: mapName,
379
+ },
380
+ native: {},
381
+ });
382
+ }
383
+
384
+ // Handle the load_multi_map command
385
+ const commandPath = `Devices.${duid}.commands.load_multi_map`;
386
+ if (mapList[0]["max_multi_map"] > 1) {
387
+ await this.adapter.createStateObjectHelper(commandPath, "Load map", "number", null, 0, "value", true, true, maps);
388
+ } else {
389
+ this.adapter.delObjectAsync(commandPath);
390
+ }
391
+ } else if (parameter == "get_fw_features") {
392
+ const firmwareFeatures = await this.adapter.messageQueueHandler.sendRequest(duid, parameter, []);
393
+ for (const firmwareFeature in firmwareFeatures) {
394
+ const featureID = firmwareFeatures[firmwareFeature];
395
+
396
+ const objectString = `Devices.${duid}.firmwareFeatures.${firmwareFeature}`;
397
+ await this.adapter.createStateObjectHelper(objectString, featureID.toString(), "string", null, null, "value", true, false);
398
+
399
+ const featureName = this.adapter.vacuums[duid].features.getFirmwareFeature(featureID);
400
+
401
+ // this dynamically processes robot features by ID if they are supported
402
+ if (typeof this.adapter.vacuums[duid].features[featureName] === "function") {
403
+ this.adapter.vacuums[duid].features[featureName]();
404
+ }
405
+
406
+ this.adapter.setStateAsync(objectString, { val: featureName, ack: true });
407
+ }
408
+ } else if (parameter == "get_server_timer") {
409
+ // const serverTimers = await this.adapter.messageQueueHandler.sendRequest(duid, parameter, []);
410
+ // if (typeof(attribute_val[0]) == "object") {
411
+ // attribute_val[0] = JSON.stringify(attribute_val[0]);
412
+ // }
413
+ // this.adapter.setStateAsync("Devices." + duid + "." + targetFolder + "." + mode, { val: attribute_val[0], ack: true });
414
+ } else if (parameter == "get_timer") {
415
+ // const timers = await this.adapter.messageQueueHandler.sendRequest(duid, parameter, []);
416
+ // if (typeof(attribute_val[0]) == "object") {
417
+ // attribute_val[0] = JSON.stringify(attribute_val[0]);
418
+ // }
419
+ // this.adapter.setStateAsync("Devices." + duid + "." + targetFolder + "." + mode, { val: attribute_val[0], ack: true });
420
+ } else if (parameter == "get_photo") {
421
+ const photoresponse = await this.adapter.messageQueueHandler.sendRequest(duid, "get_photo", attribute, true, true);
422
+
423
+ if (this.isGZIP(photoresponse)) {
424
+ this.adapter.log.debug(`gzipped photo found.`);
425
+ this.adapter.log.debug(JSON.stringify(photoresponse));
426
+
427
+ this.unzipBuffer(photoresponse, (error, photoData) => {
428
+ if (error) {
429
+ this.adapter.catchError(error, "get_photo", duid, this.robotModel);
430
+
431
+ if (this.adapter.supportsFeature && this.adapter.supportsFeature("PLUGINS")) {
432
+ if (this.adapter.sentryInstance) {
433
+ this.adapter.sentryInstance.getSentryObject().captureException(`Failed to extract gzip: ${JSON.stringify(error)}`);
434
+ }
435
+ }
436
+ } else {
437
+ const extractedPhoto = this.extractPhoto(photoData);
438
+
439
+ // fs.writeFile("slicedBuffer.jpg", extractedPhoto, (err) => {
440
+ // if (err) {
441
+ // console.error("Fehler beim Schreiben der Datei:", err);
442
+ // } else {
443
+ // console.log("Die Datei wurde erfolgreich gespeichert!");
444
+ // }
445
+ // });
446
+
447
+ if (extractedPhoto) {
448
+ const photo = {};
449
+ photo.duid = duid;
450
+ photo.command = "get_photo";
451
+ photo.image = `data:image/jpeg;base64,${extractedPhoto.toString("base64")}`;
452
+
453
+ if (this.adapter.socket) {
454
+ this.adapter.socket.send(JSON.stringify(photo));
455
+ }
456
+ }
457
+ }
458
+ });
459
+ }
460
+ } else if (
461
+ parameter == "get_dust_collection_switch_status" ||
462
+ parameter == "get_wash_towel_mode" ||
463
+ parameter == "get_smart_wash_params" ||
464
+ parameter == "get_dust_collection_mode"
465
+ ) {
466
+ const attribute_val = JSON.stringify(await this.adapter.messageQueueHandler.sendRequest(duid, parameter, {}));
467
+ this.adapter.setStateAsync(`Devices.${duid}.commands.${parameter.replace("get", "set")}`, { val: attribute_val, ack: true });
468
+ } else if (parameter == "app_get_dryer_setting") {
469
+ const attribute_val = await this.adapter.messageQueueHandler.sendRequest(duid, parameter, {});
470
+ const actualVal = JSON.stringify({ on: { dry_time: attribute_val.on.dry_time }, status: attribute_val.status });
471
+ this.adapter.setStateAsync(`Devices.${duid}.commands.${parameter.replace("get", "set")}`, { val: actualVal, ack: true });
472
+ } else if (this.parameterFolders[parameter]) {
473
+ mode = parameter.substring(4);
474
+ const attribute_val = await this.adapter.messageQueueHandler.sendRequest(duid, parameter, []);
475
+
476
+ if (typeof attribute_val[0] == "object") {
477
+ attribute_val[0] = JSON.stringify(attribute_val[0]);
478
+ }
479
+ const targetFolder = this.parameterFolders[parameter];
480
+ this.adapter.setStateAsync(`Devices.${duid}.${targetFolder}.${mode}`, { val: attribute_val[0], ack: true });
481
+ } else {
482
+ // unknown parameter
483
+ const unknown_parameter_val = await this.adapter.messageQueueHandler.sendRequest(duid, parameter, []);
484
+
485
+ // this.adapter.setStateAsync("Devices." + duid + "." + targetFolder + "." + mode, { val: attribute_val[0], ack: true });
486
+ if (typeof unknown_parameter_val == "object") {
487
+ if (typeof unknown_parameter_val[0] != "number") {
488
+ this.adapter.catchError(`Unknown parameter: ${JSON.stringify(unknown_parameter_val)}`, parameter, duid, this.robotModel);
489
+ }
490
+ } else {
491
+ this.adapter.catchError(`Unknown parameter: ${unknown_parameter_val}`, parameter, duid, this.robotModel);
492
+ }
493
+ }
494
+ } catch (error) {
495
+ this.adapter.catchError(error, parameter, duid, this.robotModel);
496
+ }
497
+ }
498
+
499
+ async setUpObjects(duid) {
500
+ await this.adapter.setObjectAsync("Devices." + duid, {
501
+ type: "device",
502
+ common: {
503
+ name: this.adapter.vacuums[duid].name,
504
+ statusStates: {
505
+ onlineId: `${this.adapter.name}.${this.adapter.instance}.Devices.${duid}.deviceInfo.online`,
506
+ },
507
+ },
508
+ native: {},
509
+ });
510
+ }
511
+
512
+ async parseDockingStationStatus(dss) {
513
+
514
+ return {
515
+ cleanFluidStatus: (dss >> 10) & 0b11,
516
+ waterBoxFilterStatus: (dss >> 8) & 0b11,
517
+ dustBagStatus: (dss >> 6) & 0b11,
518
+ dirtyWaterBoxStatus: (dss >> 4) & 0b11,
519
+ clearWaterBoxStatus: (dss >> 2) & 0b11,
520
+ isUpdownWaterReady: dss & 0b11,
521
+ };
522
+ }
523
+ async getCleanSummary(duid) {
524
+ try {
525
+ const cleaningAttributes = await this.adapter.messageQueueHandler.sendRequest(duid, "get_clean_summary", []);
526
+
527
+ for (const cleaningAttribute in cleaningAttributes) {
528
+ const mappedAttribute = mappedCleanSummary[cleaningAttribute] || cleaningAttribute;
529
+
530
+ if (["clean_time", "clean_area", "clean_count"].includes(mappedAttribute)) {
531
+ await this.adapter.setStateAsync(`Devices.${duid}.cleaningInfo.${cleaningAttribute}`, {
532
+ val: this.calculateCleaningValue(mappedAttribute, cleaningAttributes[cleaningAttribute]),
533
+ ack: true,
534
+ });
535
+ } else if (mappedAttribute == "records") {
536
+ const cleaningRecordsJSON = [];
537
+
538
+ for (const cleaningRecord in cleaningAttributes[cleaningAttribute]) {
539
+ const cleaningRecordID = cleaningAttributes[cleaningAttribute][cleaningRecord];
540
+ const cleaningRecordAttributes = (await this.adapter.messageQueueHandler.sendRequest(duid, "get_clean_record", [cleaningRecordID]))[0];
541
+
542
+ cleaningRecordsJSON[cleaningRecord] = cleaningRecordAttributes;
543
+
544
+ for (const cleaningRecordAttribute in cleaningRecordAttributes) {
545
+ const mappedRecordAttribute = mappedCleaningRecordAttribute[cleaningRecordAttribute] || cleaningRecordAttribute;
546
+ await this.adapter.setStateAsync(`Devices.${duid}.cleaningInfo.records.${cleaningRecord}.${mappedRecordAttribute}`, {
547
+ val: this.calculateRecordValue(mappedRecordAttribute, cleaningRecordAttributes[cleaningRecordAttribute]),
548
+ ack: true,
549
+ });
550
+ }
551
+
552
+ if (this.adapter.config.enable_map_creation == true) {
553
+ const mapArray = await this.getCleaningRecordMap(duid, cleaningAttributes[cleaningAttribute][cleaningRecord]);
554
+ for (const mapType in mapArray) {
555
+ const val = mapArray[mapType];
556
+ this.adapter.setStateAsync(`Devices.${duid}.cleaningInfo.records.${cleaningRecord}.map.${mapType}`, { val: val, ack: true });
557
+ }
558
+ }
559
+ }
560
+
561
+ const objectString = `Devices.${duid}.cleaningInfo.JSON`;
562
+ await this.adapter.createStateObjectHelper(objectString, "cleaningInfoJSON", "string", null, null, "json", true, false);
563
+ this.adapter.setStateAsync(`Devices.${duid}.cleaningInfo.JSON`, { val: JSON.stringify(cleaningRecordsJSON), ack: true });
564
+ }
565
+ }
566
+ } catch (error) {
567
+ this.adapter.catchError(error, "get_clean_summary", duid, this.robotModel);
568
+ }
569
+ }
570
+
571
+ calculateCleaningValue(attribute, value) {
572
+ switch (attribute) {
573
+ case "clean_time":
574
+ return Math.round(value / 60 / 60);
575
+ case "clean_area":
576
+ return Math.round(value / 1000 / 1000);
577
+ default:
578
+ return value;
579
+ }
580
+ }
581
+
582
+ calculateRecordValue(attribute, value) {
583
+ switch (attribute) {
584
+ case "begin":
585
+ case "end":
586
+ return new Date(value * 1000).toString();
587
+ case "duration":
588
+ return Math.round(value / 60);
589
+ case "area":
590
+ return Math.round(value / 1000 / 1000);
591
+ default:
592
+ return value;
593
+ }
594
+ }
595
+
596
+ unzipBuffer(buffer, callback) {
597
+ zlib.gunzip(buffer, function (err, result) {
598
+ if (err) {
599
+ callback(err);
600
+ } else {
601
+ callback(null, result);
602
+ }
603
+ });
604
+ }
605
+
606
+ isGZIP(buffer) {
607
+ if (buffer.length < 2) {
608
+ return false;
609
+ }
610
+
611
+ if (buffer[0] == 31 && buffer[1] == 139) {
612
+ return true;
613
+ }
614
+
615
+ return false;
616
+ }
617
+ extractPhoto(buffer) {
618
+ // Verify that the buffer is long enough to hold the header
619
+ if (buffer.length < 10) {
620
+ return false;
621
+ }
622
+
623
+ // Check the signature
624
+ if (buffer[26] == 74 && buffer[27] == 70 && buffer[28] == 73 && buffer[29] == 70) {
625
+ return buffer.slice(20);
626
+ } else if (buffer[42] == 74 && buffer[43] == 70 && buffer[44] == 73 && buffer[45] == 70) {
627
+ return buffer.slice(36);
628
+ }
629
+
630
+ return false;
631
+ }
632
+ }
633
+
634
+ module.exports = {
635
+ vacuum,
636
+ };