homebridge-roborock-matter 2.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.
- package/.gitattributes +13 -0
- package/CHANGELOG.md +469 -0
- package/LICENSE +23 -0
- package/README.md +45 -0
- package/__tests__/testdata/b01_q7_get_prop_response.json +1 -0
- package/__tests__/testdata/b01_q7_map_fixture.json +16 -0
- package/config.schema.json +118 -0
- package/dist/crypto.js +65 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/live_message.js +25 -0
- package/dist/live_message.js.map +1 -0
- package/dist/logger.js +37 -0
- package/dist/logger.js.map +1 -0
- package/dist/matter_vacuum_accessory.js +1877 -0
- package/dist/matter_vacuum_accessory.js.map +1 -0
- package/dist/platform.js +441 -0
- package/dist/platform.js.map +1 -0
- package/dist/settings.js +8 -0
- package/dist/settings.js.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/index.js +741 -0
- package/dist/ui/index.js.map +1 -0
- package/docs/matter-rvc-updating-homebridge-report.md +120 -0
- package/homebridge-roborock-vacuum.code-workspace +7 -0
- package/homebridge-ui/public/index.html +257 -0
- package/homebridge-ui/public/index.js +1276 -0
- package/homebridge-ui/public/styles.css +611 -0
- package/homebridge-ui/server.js +3 -0
- package/package.json +91 -0
- package/roborockLib/i18n/de/translations.json +188 -0
- package/roborockLib/i18n/en/translations.json +208 -0
- package/roborockLib/i18n/es/translations.json +188 -0
- package/roborockLib/i18n/fr/translations.json +188 -0
- package/roborockLib/i18n/it/translations.json +188 -0
- package/roborockLib/i18n/nl/translations.json +188 -0
- package/roborockLib/i18n/pl/translations.json +188 -0
- package/roborockLib/i18n/pt/translations.json +188 -0
- package/roborockLib/i18n/ru/translations.json +188 -0
- package/roborockLib/i18n/uk/translations.json +188 -0
- package/roborockLib/i18n/zh-cn/translations.json +188 -0
- package/roborockLib/lib/RRMapParser.js +459 -0
- package/roborockLib/lib/b01Q7Adapter.js +468 -0
- package/roborockLib/lib/deviceFeatures.js +1349 -0
- package/roborockLib/lib/localConnector.js +629 -0
- package/roborockLib/lib/map/map.html +137 -0
- package/roborockLib/lib/map/zones.js +851 -0
- package/roborockLib/lib/message.js +402 -0
- package/roborockLib/lib/messageQueueHandler.js +411 -0
- package/roborockLib/lib/roborockAuth.js +178 -0
- package/roborockLib/lib/roborockCrypto.js +55 -0
- package/roborockLib/lib/roborockPackageHelper.js +142 -0
- package/roborockLib/lib/roborock_mqtt_connector.js +567 -0
- package/roborockLib/lib/sniffing/mitmproxy_roborock.py +300 -0
- package/roborockLib/lib/vacuum.js +992 -0
- package/roborockLib/roborockAPI.js +3635 -0
- package/tsconfig.roborockLib.json +17 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const rrMessage = require("./message").message;
|
|
4
|
+
const RRMapParser = require("./RRMapParser");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const zlib = require("zlib");
|
|
7
|
+
|
|
8
|
+
const mappedCleanSummary = {
|
|
9
|
+
0: "clean_time",
|
|
10
|
+
1: "clean_area",
|
|
11
|
+
2: "clean_count",
|
|
12
|
+
3: "records",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const mappedCleaningRecordAttribute = {
|
|
16
|
+
0: "begin",
|
|
17
|
+
1: "end",
|
|
18
|
+
2: "duration",
|
|
19
|
+
3: "area",
|
|
20
|
+
4: "error",
|
|
21
|
+
5: "complete",
|
|
22
|
+
6: "start_type",
|
|
23
|
+
7: "clean_type",
|
|
24
|
+
8: "finish_reason",
|
|
25
|
+
9: "dust_collection_status",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
class vacuum {
|
|
29
|
+
constructor(adapter, robotModel) {
|
|
30
|
+
this.adapter = adapter;
|
|
31
|
+
|
|
32
|
+
this.adapter.log.debug(`Robot key: ${robotModel}`);
|
|
33
|
+
this.robotModel = robotModel;
|
|
34
|
+
|
|
35
|
+
this.message = new rrMessage(this.adapter);
|
|
36
|
+
|
|
37
|
+
this.mapParser = new RRMapParser(this.adapter);
|
|
38
|
+
|
|
39
|
+
this.parameterFolders = {
|
|
40
|
+
get_mop_mode: "deviceStatus",
|
|
41
|
+
get_water_box_mode: "deviceStatus",
|
|
42
|
+
get_water_box_custom_mode: "deviceStatus",
|
|
43
|
+
get_network_info: "networkInfo",
|
|
44
|
+
get_consumable: "consumables",
|
|
45
|
+
get_fw_features: "firmwareFeatures",
|
|
46
|
+
get_carpet_mode: "deviceStatus",
|
|
47
|
+
get_carpet_clean_mode: "deviceStatus",
|
|
48
|
+
get_carpet_cleaning_mode: "deviceStatus",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async updateDiagnosticSnapshot(duid, key, payload) {
|
|
53
|
+
if (typeof this.adapter.updateRoborockDiagnostics !== "function") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await this.adapter.updateRoborockDiagnostics(duid, key, {
|
|
58
|
+
capturedAt: new Date().toISOString(),
|
|
59
|
+
payload,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async command(duid, parameter, value, options = {}) {
|
|
64
|
+
try {
|
|
65
|
+
const requestOptions = {};
|
|
66
|
+
if (options.preferCloud) {
|
|
67
|
+
requestOptions.preferCloud = true;
|
|
68
|
+
}
|
|
69
|
+
if (options.preferLocal) {
|
|
70
|
+
requestOptions.preferLocal = true;
|
|
71
|
+
}
|
|
72
|
+
if (options.allowOfflineCloudSend) {
|
|
73
|
+
requestOptions.allowOfflineCloudSend = true;
|
|
74
|
+
}
|
|
75
|
+
if (
|
|
76
|
+
typeof options.requestTimeoutMs === "number" &&
|
|
77
|
+
Number.isFinite(options.requestTimeoutMs) &&
|
|
78
|
+
options.requestTimeoutMs > 0
|
|
79
|
+
) {
|
|
80
|
+
requestOptions.requestTimeoutMs = options.requestTimeoutMs;
|
|
81
|
+
}
|
|
82
|
+
const hasRequestOptions = Object.keys(requestOptions).length > 0;
|
|
83
|
+
const sendCommandRequest = (method, params) =>
|
|
84
|
+
hasRequestOptions
|
|
85
|
+
? this.adapter.messageQueueHandler.sendRequest(
|
|
86
|
+
duid,
|
|
87
|
+
method,
|
|
88
|
+
params,
|
|
89
|
+
false,
|
|
90
|
+
false,
|
|
91
|
+
requestOptions
|
|
92
|
+
)
|
|
93
|
+
: this.adapter.messageQueueHandler.sendRequest(duid, method, params);
|
|
94
|
+
|
|
95
|
+
switch (parameter) {
|
|
96
|
+
case "app_segment_clean": {
|
|
97
|
+
this.adapter.log.debug("Start room cleaning");
|
|
98
|
+
|
|
99
|
+
const roomList = {};
|
|
100
|
+
roomList.segments = [];
|
|
101
|
+
const roomFloor = await this.adapter.getStateAsync(
|
|
102
|
+
`Devices.${duid}.deviceStatus.map_status`
|
|
103
|
+
);
|
|
104
|
+
const mappedRoomList = await sendCommandRequest(
|
|
105
|
+
"get_room_mapping",
|
|
106
|
+
[]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (mappedRoomList) {
|
|
110
|
+
const roomStates = await Promise.all(
|
|
111
|
+
Object.keys(mappedRoomList).map((mappedRoom) =>
|
|
112
|
+
this.adapter.getStateAsync(
|
|
113
|
+
`Devices.${duid}.floors.${roomFloor.val}.${mappedRoomList[mappedRoom][0]}`
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
Object.keys(mappedRoomList).forEach((mappedRoom, index) => {
|
|
119
|
+
if (roomStates[index].val) {
|
|
120
|
+
roomList.segments.push(mappedRoomList[mappedRoom][0]);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const cleanCount = await this.adapter.getStateAsync(
|
|
126
|
+
`Devices.${duid}.floors.cleanCount`
|
|
127
|
+
);
|
|
128
|
+
roomList["repeat"] = cleanCount.val;
|
|
129
|
+
|
|
130
|
+
const result = await sendCommandRequest("app_segment_clean", [
|
|
131
|
+
roomList,
|
|
132
|
+
]);
|
|
133
|
+
this.adapter.log.debug(
|
|
134
|
+
`app_segment_clean with roomIDs: ${JSON.stringify(roomList)} result: ${result}`
|
|
135
|
+
);
|
|
136
|
+
this.adapter.setStateAsync(`Devices.${duid}.floors.cleanCount`, {
|
|
137
|
+
val: 1,
|
|
138
|
+
ack: true,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "app_segment_clean_by_ids": {
|
|
144
|
+
const requestedSegments =
|
|
145
|
+
value && Array.isArray(value.segments) ? value.segments : [];
|
|
146
|
+
const segments = Array.from(
|
|
147
|
+
new Set(
|
|
148
|
+
requestedSegments
|
|
149
|
+
.map((segment) => Number(segment))
|
|
150
|
+
.filter((segment) => Number.isInteger(segment) && segment >= 0)
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
const repeat = Number(value && value.repeat);
|
|
154
|
+
const roomList = {
|
|
155
|
+
segments,
|
|
156
|
+
repeat: Number.isInteger(repeat) && repeat > 0 ? repeat : 1,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (roomList.segments.length === 0) {
|
|
160
|
+
this.adapter.log.warn(
|
|
161
|
+
`No room segments supplied for app_segment_clean_by_ids on ${duid}.`
|
|
162
|
+
);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result = await sendCommandRequest("app_segment_clean", [
|
|
167
|
+
roomList,
|
|
168
|
+
]);
|
|
169
|
+
this.adapter.log.debug(
|
|
170
|
+
`app_segment_clean_by_ids with roomIDs: ${JSON.stringify(roomList)} result: ${result}`
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case "reset_consumable":
|
|
176
|
+
await sendCommandRequest(parameter, [value]);
|
|
177
|
+
this.adapter.log.info(`Consumable ${parameter} successfully reset.`);
|
|
178
|
+
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case "app_set_dryer_status": {
|
|
182
|
+
const result = await sendCommandRequest(parameter, JSON.parse(value));
|
|
183
|
+
this.adapter.log.debug(`Command: ${parameter} result: ${result}`);
|
|
184
|
+
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case "app_goto_target":
|
|
188
|
+
case "app_zoned_clean": {
|
|
189
|
+
const result = await sendCommandRequest(parameter, value);
|
|
190
|
+
this.adapter.log.debug(
|
|
191
|
+
`Command: ${parameter} with value: ${JSON.stringify(value)} result: ${result}`
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case "load_multi_map": {
|
|
197
|
+
const mapId = Number(value);
|
|
198
|
+
if (!Number.isInteger(mapId) || mapId < 0) {
|
|
199
|
+
this.adapter.log.warn(
|
|
200
|
+
`Invalid map id '${value}' supplied for load_multi_map on ${duid}.`
|
|
201
|
+
);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const result = await sendCommandRequest(parameter, [mapId]);
|
|
206
|
+
this.adapter.log.debug(
|
|
207
|
+
`Command: ${parameter} with value: ${mapId} result: ${result}`
|
|
208
|
+
);
|
|
209
|
+
return await this.getParameter(duid, "get_room_mapping");
|
|
210
|
+
}
|
|
211
|
+
case "set_water_box_distance_off": {
|
|
212
|
+
const mappedValue = ((value - 1) / (30 - 1)) * (60 - 205) + 205;
|
|
213
|
+
const parameterValue = { distance_off: mappedValue };
|
|
214
|
+
|
|
215
|
+
const result = await sendCommandRequest(parameter, parameterValue);
|
|
216
|
+
this.adapter.log.debug(
|
|
217
|
+
`Command: ${parameter} with value: ${JSON.stringify(parameterValue)} result: ${result}`
|
|
218
|
+
);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
default:
|
|
222
|
+
if (value && typeof value !== "boolean") {
|
|
223
|
+
const valueType = typeof value;
|
|
224
|
+
|
|
225
|
+
if (valueType === "string") {
|
|
226
|
+
value = await JSON.parse(value);
|
|
227
|
+
} else if (valueType === "number") {
|
|
228
|
+
value = [value];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// await is important here!!! Wait for the command to finish before sending the request to update deviceConfig!!!
|
|
232
|
+
const result = await sendCommandRequest(parameter, value);
|
|
233
|
+
this.adapter.log.debug(
|
|
234
|
+
`Command: ${parameter} with value: ${JSON.stringify(value)} result: ${result}`
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// this is needed to update the states instantly after sending a command
|
|
238
|
+
const getCommand = parameter.replace("set", "get");
|
|
239
|
+
await this.getParameter(duid, getCommand);
|
|
240
|
+
return result;
|
|
241
|
+
} else {
|
|
242
|
+
const result = await sendCommandRequest(parameter, []);
|
|
243
|
+
this.adapter.log.debug(`Command: ${parameter} result: ${result}`);
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
this.adapter.catchError(error, parameter, duid, this.robotModel);
|
|
249
|
+
if (options.throwOnError) {
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async getServerTimers(duid) {
|
|
256
|
+
try {
|
|
257
|
+
return await this.adapter.messageQueueHandler.sendRequest(
|
|
258
|
+
duid,
|
|
259
|
+
"get_server_timer",
|
|
260
|
+
[]
|
|
261
|
+
);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
this.adapter.catchError(error, "get_server_timer", duid, this.robotModel);
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async updateServerTimer(duid, timerId, enabled) {
|
|
269
|
+
try {
|
|
270
|
+
return await this.adapter.messageQueueHandler.sendRequest(
|
|
271
|
+
duid,
|
|
272
|
+
"upd_server_timer",
|
|
273
|
+
[timerId, enabled ? "on" : "off"]
|
|
274
|
+
);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
this.adapter.catchError(error, "upd_server_timer", duid, this.robotModel);
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async getParameter(duid, parameter, attribute, options = {}) {
|
|
282
|
+
let mode;
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
if (parameter == "get_network_info") {
|
|
286
|
+
mode = parameter;
|
|
287
|
+
const networkInfo = await this.adapter.messageQueueHandler.sendRequest(
|
|
288
|
+
duid,
|
|
289
|
+
parameter,
|
|
290
|
+
[]
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
for (const attribute in networkInfo) {
|
|
294
|
+
if (
|
|
295
|
+
attribute == "ip" &&
|
|
296
|
+
!this.adapter.isCloudOnlyModeEnabled?.() &&
|
|
297
|
+
!(await this.adapter.isRemoteDevice(duid))
|
|
298
|
+
) {
|
|
299
|
+
this.adapter.localDevices[duid] = networkInfo[attribute];
|
|
300
|
+
}
|
|
301
|
+
this.adapter.setStateAsync(
|
|
302
|
+
`Devices.${duid}.networkInfo.${attribute}`,
|
|
303
|
+
{ val: networkInfo[attribute], ack: true }
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
} else if (parameter == "get_consumable") {
|
|
307
|
+
const consumables = (
|
|
308
|
+
await this.adapter.messageQueueHandler.sendRequest(
|
|
309
|
+
duid,
|
|
310
|
+
"get_consumable",
|
|
311
|
+
[]
|
|
312
|
+
)
|
|
313
|
+
)[0];
|
|
314
|
+
|
|
315
|
+
for (const consumable in consumables) {
|
|
316
|
+
const divider =
|
|
317
|
+
this.adapter.vacuums[duid].features.getConsumablesDivider(
|
|
318
|
+
consumable
|
|
319
|
+
);
|
|
320
|
+
if (divider) {
|
|
321
|
+
const consumable_val = divider
|
|
322
|
+
? Math.round(consumables[consumable] / divider)
|
|
323
|
+
: consumables[consumable];
|
|
324
|
+
|
|
325
|
+
this.adapter.setStateAsync(
|
|
326
|
+
`Devices.${duid}.consumables.${consumable}`,
|
|
327
|
+
{ val: consumable_val, ack: true }
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} else if (parameter == "get_status") {
|
|
332
|
+
const now = new Date();
|
|
333
|
+
const seconds = now.getSeconds();
|
|
334
|
+
const force = attribute == "force";
|
|
335
|
+
|
|
336
|
+
if (
|
|
337
|
+
force ||
|
|
338
|
+
this.adapter.socket ||
|
|
339
|
+
seconds % this.adapter.config.updateInterval == 0
|
|
340
|
+
) {
|
|
341
|
+
// only send status every minute or if websocket is connected
|
|
342
|
+
|
|
343
|
+
// const deviceStatus = await this.adapter.messageQueueHandler.sendRequest(duid, "get_status", []);
|
|
344
|
+
const requestOptions = options.preferCloud
|
|
345
|
+
? { preferCloud: true }
|
|
346
|
+
: {};
|
|
347
|
+
const deviceStatus =
|
|
348
|
+
await this.adapter.messageQueueHandler.sendRequest(
|
|
349
|
+
duid,
|
|
350
|
+
"get_prop",
|
|
351
|
+
["get_status"],
|
|
352
|
+
false,
|
|
353
|
+
false,
|
|
354
|
+
requestOptions
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
await this.updateDiagnosticSnapshot(duid, "lastStatus", {
|
|
358
|
+
method: "get_status",
|
|
359
|
+
status: deviceStatus[0] || null,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
for (const attribute in deviceStatus[0]) {
|
|
363
|
+
const isCleaning = this.adapter.isCleaning(
|
|
364
|
+
deviceStatus[0]["state"]
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
if (
|
|
368
|
+
!(await this.adapter.getObjectAsync(
|
|
369
|
+
`Devices.${duid}.deviceStatus.${attribute}`
|
|
370
|
+
))
|
|
371
|
+
) {
|
|
372
|
+
const isKnownStatusAttribute =
|
|
373
|
+
typeof this.adapter.vacuums[duid].features
|
|
374
|
+
.hasDeviceStatusAttribute === "function" &&
|
|
375
|
+
this.adapter.vacuums[duid].features.hasDeviceStatusAttribute(
|
|
376
|
+
attribute
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
if (isKnownStatusAttribute) {
|
|
380
|
+
this.adapter.log.debug(
|
|
381
|
+
`Skipping known get_status attribute without a Homebridge state object: ${attribute}. Model: ${this.robotModel}`
|
|
382
|
+
);
|
|
383
|
+
} else {
|
|
384
|
+
this.adapter.log.warn(
|
|
385
|
+
`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}`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
continue; // skip unsupported attributes
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const divider =
|
|
392
|
+
this.adapter.vacuums[duid].features.getStatusDivider(attribute);
|
|
393
|
+
if (divider) {
|
|
394
|
+
deviceStatus[0][attribute] = Math.round(
|
|
395
|
+
deviceStatus[0][attribute] / divider
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (typeof deviceStatus[0][attribute] == "object") {
|
|
400
|
+
deviceStatus[0][attribute] = JSON.stringify(
|
|
401
|
+
deviceStatus[0][attribute]
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
switch (attribute) {
|
|
406
|
+
case "dock_type":
|
|
407
|
+
this.adapter.vacuums[duid].features.processDockType(attribute);
|
|
408
|
+
break;
|
|
409
|
+
case "dss":
|
|
410
|
+
await this.adapter.createDockingStationObject(duid);
|
|
411
|
+
const dockingStationStatus =
|
|
412
|
+
await this.parseDockingStationStatus(
|
|
413
|
+
deviceStatus[0][attribute]
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
for (const state in dockingStationStatus) {
|
|
417
|
+
this.adapter.setStateAsync(
|
|
418
|
+
`Devices.${duid}.dockingStationStatus.${state}`,
|
|
419
|
+
{ val: parseInt(dockingStationStatus[state]), ack: true }
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
break;
|
|
423
|
+
case "map_status": {
|
|
424
|
+
deviceStatus[0][attribute] =
|
|
425
|
+
deviceStatus[0][attribute] >> 2 ?? -1; // to get the currently selected map perform bitwise right shift
|
|
426
|
+
|
|
427
|
+
if (isCleaning) {
|
|
428
|
+
this.adapter.startMapUpdater(duid);
|
|
429
|
+
} else if (!isCleaning) {
|
|
430
|
+
this.adapter.stopMapUpdater(duid);
|
|
431
|
+
} else {
|
|
432
|
+
const mapCount = await this.adapter.getStateAsync(
|
|
433
|
+
`Devices.${duid}.floors.multi_map_count`
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
// don't process load_multi_map for single level configuration
|
|
437
|
+
if (mapCount) {
|
|
438
|
+
// sometimes mapCount is not available shortly after first start of adapter
|
|
439
|
+
if (mapCount.val > 1) {
|
|
440
|
+
const currentMap = deviceStatus[0][attribute];
|
|
441
|
+
const mapFromCommand = await this.adapter.getState(
|
|
442
|
+
`Devices.${duid}.commands.load_multi_map`
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
if (mapFromCommand && mapFromCommand.val != currentMap) {
|
|
446
|
+
await this.adapter.setStateAsync(
|
|
447
|
+
`Devices.${duid}.commands.load_multi_map`,
|
|
448
|
+
currentMap,
|
|
449
|
+
true
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
case "state": {
|
|
459
|
+
if (this.adapter.socket) {
|
|
460
|
+
const sendValue = {
|
|
461
|
+
duid: duid,
|
|
462
|
+
command: "get_status",
|
|
463
|
+
parameters: { isCleaning: isCleaning },
|
|
464
|
+
};
|
|
465
|
+
this.adapter.socket.send(JSON.stringify(sendValue));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
case "last_clean_t":
|
|
471
|
+
deviceStatus[0][attribute] = new Date(
|
|
472
|
+
deviceStatus[0][attribute]
|
|
473
|
+
).toString();
|
|
474
|
+
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
this.adapter.setStateChangedAsync(
|
|
478
|
+
`Devices.${duid}.deviceStatus.${attribute}`,
|
|
479
|
+
{ val: deviceStatus[0][attribute], ack: true }
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
this.adapter.manageDeviceIntervals(duid);
|
|
483
|
+
}
|
|
484
|
+
} else if (parameter == "get_room_mapping") {
|
|
485
|
+
const deviceStatus = await this.adapter.messageQueueHandler.sendRequest(
|
|
486
|
+
duid,
|
|
487
|
+
"get_status",
|
|
488
|
+
[]
|
|
489
|
+
);
|
|
490
|
+
const mapStatus = Array.isArray(deviceStatus)
|
|
491
|
+
? deviceStatus[0]?.["map_status"]
|
|
492
|
+
: undefined;
|
|
493
|
+
// to get the currently selected map perform bitwise right shift
|
|
494
|
+
const roomFloor = typeof mapStatus === "number" ? mapStatus >> 2 : -1;
|
|
495
|
+
const mappedRooms = await this.adapter.messageQueueHandler.sendRequest(
|
|
496
|
+
duid,
|
|
497
|
+
"get_room_mapping",
|
|
498
|
+
[]
|
|
499
|
+
);
|
|
500
|
+
if (typeof this.adapter.updateRoomMappingCache === "function") {
|
|
501
|
+
this.adapter.updateRoomMappingCache(duid, roomFloor, mappedRooms);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// if no rooms have been named, processing them can't work
|
|
505
|
+
if (!Array.isArray(mappedRooms) || mappedRooms.length < 1) {
|
|
506
|
+
this.adapter.log.info(
|
|
507
|
+
`No room mappings returned for ${duid}. Room-based controls will stay unavailable until the Roborock app exposes named rooms.`
|
|
508
|
+
);
|
|
509
|
+
} else {
|
|
510
|
+
let unnamedRooms = 0;
|
|
511
|
+
for (const mappedRoom of mappedRooms) {
|
|
512
|
+
const roomID = mappedRoom[1];
|
|
513
|
+
const roomName = this.adapter.roomIDs[roomID] || `Room ${roomID}`;
|
|
514
|
+
|
|
515
|
+
if (!this.adapter.roomIDs[roomID]) {
|
|
516
|
+
unnamedRooms++;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
this.adapter.log.debug(
|
|
520
|
+
`Mapped room matched: ${roomID} with name: ${roomName}`
|
|
521
|
+
);
|
|
522
|
+
const objectString = `Devices.${duid}.floors.${roomFloor}.${mappedRoom[0]}`;
|
|
523
|
+
await this.adapter.createStateObjectHelper(
|
|
524
|
+
objectString,
|
|
525
|
+
roomName,
|
|
526
|
+
"boolean",
|
|
527
|
+
null,
|
|
528
|
+
true,
|
|
529
|
+
"value",
|
|
530
|
+
true,
|
|
531
|
+
true
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (unnamedRooms > 0) {
|
|
536
|
+
this.adapter.log.info(
|
|
537
|
+
`${unnamedRooms} room(s) for ${duid} were missing names from HomeData. Using fallback labels like 'Room <id>' until the Roborock app syncs names.`
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const objectString = `Devices.${duid}.floors.cleanCount`;
|
|
543
|
+
await this.adapter.createStateObjectHelper(
|
|
544
|
+
objectString,
|
|
545
|
+
"Clean count",
|
|
546
|
+
"number",
|
|
547
|
+
null,
|
|
548
|
+
1,
|
|
549
|
+
"value",
|
|
550
|
+
true,
|
|
551
|
+
true
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
return mappedRooms;
|
|
555
|
+
} else if (parameter == "get_multi_maps_list") {
|
|
556
|
+
const mapList = await this.adapter.messageQueueHandler.sendRequest(
|
|
557
|
+
duid,
|
|
558
|
+
"get_multi_maps_list",
|
|
559
|
+
[]
|
|
560
|
+
);
|
|
561
|
+
const mapInfo = mapList[0]?.map_info || [];
|
|
562
|
+
const maps = {};
|
|
563
|
+
|
|
564
|
+
if (typeof this.adapter.updateMapListCache === "function") {
|
|
565
|
+
this.adapter.updateMapListCache(duid, mapInfo);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Set states for numeric parameters
|
|
569
|
+
for (const mapParameter in mapList[0] || {}) {
|
|
570
|
+
if (typeof mapList[0][mapParameter] === "number") {
|
|
571
|
+
const statePath = `Devices.${duid}.floors.${mapParameter}`;
|
|
572
|
+
this.adapter.setStateAsync(statePath, {
|
|
573
|
+
val: mapList[0][mapParameter],
|
|
574
|
+
ack: true,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Create map folders
|
|
580
|
+
for (const map in mapInfo) {
|
|
581
|
+
const roomFloor = mapInfo[map]["mapFlag"];
|
|
582
|
+
const mapName = mapInfo[map]["name"];
|
|
583
|
+
maps[roomFloor] = mapName;
|
|
584
|
+
|
|
585
|
+
const objectPath = `Devices.${duid}.floors.${roomFloor}`;
|
|
586
|
+
this.adapter.setObjectAsync(objectPath, {
|
|
587
|
+
type: "folder",
|
|
588
|
+
common: {
|
|
589
|
+
name: mapName,
|
|
590
|
+
},
|
|
591
|
+
native: {},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Handle the load_multi_map command
|
|
596
|
+
const commandPath = `Devices.${duid}.commands.load_multi_map`;
|
|
597
|
+
if ((mapList[0]?.max_multi_map || 0) > 1) {
|
|
598
|
+
await this.adapter.createStateObjectHelper(
|
|
599
|
+
commandPath,
|
|
600
|
+
"Load map",
|
|
601
|
+
"number",
|
|
602
|
+
null,
|
|
603
|
+
0,
|
|
604
|
+
"value",
|
|
605
|
+
true,
|
|
606
|
+
true,
|
|
607
|
+
maps
|
|
608
|
+
);
|
|
609
|
+
} else {
|
|
610
|
+
this.adapter.delObjectAsync(commandPath);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return mapInfo;
|
|
614
|
+
} else if (parameter == "get_fw_features") {
|
|
615
|
+
const firmwareFeatures =
|
|
616
|
+
await this.adapter.messageQueueHandler.sendRequest(
|
|
617
|
+
duid,
|
|
618
|
+
parameter,
|
|
619
|
+
[]
|
|
620
|
+
);
|
|
621
|
+
for (const firmwareFeature in firmwareFeatures) {
|
|
622
|
+
const featureID = firmwareFeatures[firmwareFeature];
|
|
623
|
+
|
|
624
|
+
const objectString = `Devices.${duid}.firmwareFeatures.${firmwareFeature}`;
|
|
625
|
+
await this.adapter.createStateObjectHelper(
|
|
626
|
+
objectString,
|
|
627
|
+
featureID.toString(),
|
|
628
|
+
"string",
|
|
629
|
+
null,
|
|
630
|
+
null,
|
|
631
|
+
"value",
|
|
632
|
+
true,
|
|
633
|
+
false
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
const featureName =
|
|
637
|
+
this.adapter.vacuums[duid].features.getFirmwareFeature(featureID);
|
|
638
|
+
|
|
639
|
+
// this dynamically processes robot features by ID if they are supported
|
|
640
|
+
if (
|
|
641
|
+
typeof this.adapter.vacuums[duid].features[featureName] ===
|
|
642
|
+
"function"
|
|
643
|
+
) {
|
|
644
|
+
this.adapter.vacuums[duid].features[featureName]();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
this.adapter.setStateAsync(objectString, {
|
|
648
|
+
val: featureName,
|
|
649
|
+
ack: true,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
} else if (parameter == "get_server_timer") {
|
|
653
|
+
if (this.adapter.config.debug) {
|
|
654
|
+
const serverTimers =
|
|
655
|
+
await this.adapter.messageQueueHandler.sendRequest(
|
|
656
|
+
duid,
|
|
657
|
+
parameter,
|
|
658
|
+
[]
|
|
659
|
+
);
|
|
660
|
+
await this.updateDiagnosticSnapshot(duid, "lastServerTimer", {
|
|
661
|
+
method: parameter,
|
|
662
|
+
response: serverTimers,
|
|
663
|
+
});
|
|
664
|
+
this.adapter.log.debug(
|
|
665
|
+
`Roborock ${parameter} diagnostic for ${duid}: ${JSON.stringify(this.adapter.compactDiagnosticPayload(serverTimers))}`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
} else if (parameter == "get_timer") {
|
|
669
|
+
if (this.adapter.config.debug) {
|
|
670
|
+
const timers = await this.adapter.messageQueueHandler.sendRequest(
|
|
671
|
+
duid,
|
|
672
|
+
parameter,
|
|
673
|
+
[]
|
|
674
|
+
);
|
|
675
|
+
await this.updateDiagnosticSnapshot(duid, "lastTimer", {
|
|
676
|
+
method: parameter,
|
|
677
|
+
response: timers,
|
|
678
|
+
});
|
|
679
|
+
this.adapter.log.debug(
|
|
680
|
+
`Roborock ${parameter} diagnostic for ${duid}: ${JSON.stringify(this.adapter.compactDiagnosticPayload(timers))}`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
} else if (parameter == "get_photo") {
|
|
684
|
+
const photoresponse =
|
|
685
|
+
await this.adapter.messageQueueHandler.sendRequest(
|
|
686
|
+
duid,
|
|
687
|
+
"get_photo",
|
|
688
|
+
attribute,
|
|
689
|
+
true,
|
|
690
|
+
true
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
if (this.isGZIP(photoresponse)) {
|
|
694
|
+
this.adapter.log.debug(`gzipped photo found.`);
|
|
695
|
+
this.adapter.log.debug(JSON.stringify(photoresponse));
|
|
696
|
+
|
|
697
|
+
this.unzipBuffer(photoresponse, (error, photoData) => {
|
|
698
|
+
if (error) {
|
|
699
|
+
this.adapter.catchError(
|
|
700
|
+
error,
|
|
701
|
+
"get_photo",
|
|
702
|
+
duid,
|
|
703
|
+
this.robotModel
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
if (
|
|
707
|
+
this.adapter.supportsFeature &&
|
|
708
|
+
this.adapter.supportsFeature("PLUGINS")
|
|
709
|
+
) {
|
|
710
|
+
if (this.adapter.sentryInstance) {
|
|
711
|
+
this.adapter.sentryInstance
|
|
712
|
+
.getSentryObject()
|
|
713
|
+
.captureException(
|
|
714
|
+
`Failed to extract gzip: ${JSON.stringify(error)}`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} else {
|
|
719
|
+
const extractedPhoto = this.extractPhoto(photoData);
|
|
720
|
+
|
|
721
|
+
if (extractedPhoto) {
|
|
722
|
+
const photo = {};
|
|
723
|
+
photo.duid = duid;
|
|
724
|
+
photo.command = "get_photo";
|
|
725
|
+
photo.image = `data:image/jpeg;base64,${extractedPhoto.toString("base64")}`;
|
|
726
|
+
|
|
727
|
+
if (this.adapter.socket) {
|
|
728
|
+
this.adapter.socket.send(JSON.stringify(photo));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
} else if (
|
|
735
|
+
parameter == "get_dust_collection_switch_status" ||
|
|
736
|
+
parameter == "get_wash_towel_mode" ||
|
|
737
|
+
parameter == "get_smart_wash_params" ||
|
|
738
|
+
parameter == "get_dust_collection_mode"
|
|
739
|
+
) {
|
|
740
|
+
const attribute_val = JSON.stringify(
|
|
741
|
+
await this.adapter.messageQueueHandler.sendRequest(
|
|
742
|
+
duid,
|
|
743
|
+
parameter,
|
|
744
|
+
{}
|
|
745
|
+
)
|
|
746
|
+
);
|
|
747
|
+
this.adapter.setStateAsync(
|
|
748
|
+
`Devices.${duid}.commands.${parameter.replace("get", "set")}`,
|
|
749
|
+
{ val: attribute_val, ack: true }
|
|
750
|
+
);
|
|
751
|
+
} else if (parameter == "app_get_dryer_setting") {
|
|
752
|
+
const attribute_val =
|
|
753
|
+
await this.adapter.messageQueueHandler.sendRequest(
|
|
754
|
+
duid,
|
|
755
|
+
parameter,
|
|
756
|
+
{}
|
|
757
|
+
);
|
|
758
|
+
const actualVal = JSON.stringify({
|
|
759
|
+
on: { dry_time: attribute_val.on.dry_time },
|
|
760
|
+
status: attribute_val.status,
|
|
761
|
+
});
|
|
762
|
+
this.adapter.setStateAsync(
|
|
763
|
+
`Devices.${duid}.commands.${parameter.replace("get", "set")}`,
|
|
764
|
+
{ val: actualVal, ack: true }
|
|
765
|
+
);
|
|
766
|
+
} else if (this.parameterFolders[parameter]) {
|
|
767
|
+
mode = parameter.substring(4);
|
|
768
|
+
const attribute_val =
|
|
769
|
+
await this.adapter.messageQueueHandler.sendRequest(
|
|
770
|
+
duid,
|
|
771
|
+
parameter,
|
|
772
|
+
[]
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
if (typeof attribute_val[0] == "object") {
|
|
776
|
+
attribute_val[0] = JSON.stringify(attribute_val[0]);
|
|
777
|
+
}
|
|
778
|
+
const targetFolder = this.parameterFolders[parameter];
|
|
779
|
+
this.adapter.setStateAsync(`Devices.${duid}.${targetFolder}.${mode}`, {
|
|
780
|
+
val: attribute_val[0],
|
|
781
|
+
ack: true,
|
|
782
|
+
});
|
|
783
|
+
} else {
|
|
784
|
+
// unknown parameter
|
|
785
|
+
const unknown_parameter_val =
|
|
786
|
+
await this.adapter.messageQueueHandler.sendRequest(
|
|
787
|
+
duid,
|
|
788
|
+
parameter,
|
|
789
|
+
[]
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
// this.adapter.setStateAsync("Devices." + duid + "." + targetFolder + "." + mode, { val: attribute_val[0], ack: true });
|
|
793
|
+
if (typeof unknown_parameter_val == "object") {
|
|
794
|
+
if (typeof unknown_parameter_val[0] != "number") {
|
|
795
|
+
this.adapter.catchError(
|
|
796
|
+
`Unknown parameter: ${JSON.stringify(unknown_parameter_val)}`,
|
|
797
|
+
parameter,
|
|
798
|
+
duid,
|
|
799
|
+
this.robotModel
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
} else {
|
|
803
|
+
this.adapter.catchError(
|
|
804
|
+
`Unknown parameter: ${unknown_parameter_val}`,
|
|
805
|
+
parameter,
|
|
806
|
+
duid,
|
|
807
|
+
this.robotModel
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
} catch (error) {
|
|
812
|
+
this.adapter.catchError(error, parameter, duid, this.robotModel);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async setUpObjects(duid) {
|
|
817
|
+
await this.adapter.setObjectAsync("Devices." + duid, {
|
|
818
|
+
type: "device",
|
|
819
|
+
common: {
|
|
820
|
+
name: this.adapter.vacuums[duid].name,
|
|
821
|
+
statusStates: {
|
|
822
|
+
onlineId: `${this.adapter.name}.${this.adapter.instance}.Devices.${duid}.deviceInfo.online`,
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
native: {},
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async parseDockingStationStatus(dss) {
|
|
830
|
+
return {
|
|
831
|
+
cleanFluidStatus: (dss >> 10) & 0b11,
|
|
832
|
+
waterBoxFilterStatus: (dss >> 8) & 0b11,
|
|
833
|
+
dustBagStatus: (dss >> 6) & 0b11,
|
|
834
|
+
dirtyWaterBoxStatus: (dss >> 4) & 0b11,
|
|
835
|
+
clearWaterBoxStatus: (dss >> 2) & 0b11,
|
|
836
|
+
isUpdownWaterReady: dss & 0b11,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
async getCleanSummary(duid) {
|
|
840
|
+
try {
|
|
841
|
+
const cleaningAttributes =
|
|
842
|
+
await this.adapter.messageQueueHandler.sendRequest(
|
|
843
|
+
duid,
|
|
844
|
+
"get_clean_summary",
|
|
845
|
+
[]
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
for (const cleaningAttribute in cleaningAttributes) {
|
|
849
|
+
const mappedAttribute =
|
|
850
|
+
mappedCleanSummary[cleaningAttribute] || cleaningAttribute;
|
|
851
|
+
|
|
852
|
+
if (
|
|
853
|
+
["clean_time", "clean_area", "clean_count"].includes(mappedAttribute)
|
|
854
|
+
) {
|
|
855
|
+
await this.adapter.setStateAsync(
|
|
856
|
+
`Devices.${duid}.cleaningInfo.${cleaningAttribute}`,
|
|
857
|
+
{
|
|
858
|
+
val: this.calculateCleaningValue(
|
|
859
|
+
mappedAttribute,
|
|
860
|
+
cleaningAttributes[cleaningAttribute]
|
|
861
|
+
),
|
|
862
|
+
ack: true,
|
|
863
|
+
}
|
|
864
|
+
);
|
|
865
|
+
} else if (mappedAttribute == "records") {
|
|
866
|
+
const cleaningRecordsJSON = [];
|
|
867
|
+
const cleaningRecordIndexes = Object.keys(
|
|
868
|
+
cleaningAttributes[cleaningAttribute]
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
const allCleaningRecordAttributes = await Promise.all(
|
|
872
|
+
cleaningRecordIndexes.map((cleaningRecord) => {
|
|
873
|
+
const cleaningRecordID =
|
|
874
|
+
cleaningAttributes[cleaningAttribute][cleaningRecord];
|
|
875
|
+
return this.adapter.messageQueueHandler
|
|
876
|
+
.sendRequest(duid, "get_clean_record", [cleaningRecordID])
|
|
877
|
+
.then((result) => result[0]);
|
|
878
|
+
})
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
for (let i = 0; i < cleaningRecordIndexes.length; i++) {
|
|
882
|
+
const cleaningRecord = cleaningRecordIndexes[i];
|
|
883
|
+
const cleaningRecordAttributes = allCleaningRecordAttributes[i];
|
|
884
|
+
|
|
885
|
+
cleaningRecordsJSON[cleaningRecord] = cleaningRecordAttributes;
|
|
886
|
+
|
|
887
|
+
for (const cleaningRecordAttribute in cleaningRecordAttributes) {
|
|
888
|
+
const mappedRecordAttribute =
|
|
889
|
+
mappedCleaningRecordAttribute[cleaningRecordAttribute] ||
|
|
890
|
+
cleaningRecordAttribute;
|
|
891
|
+
await this.adapter.setStateAsync(
|
|
892
|
+
`Devices.${duid}.cleaningInfo.records.${cleaningRecord}.${mappedRecordAttribute}`,
|
|
893
|
+
{
|
|
894
|
+
val: this.calculateRecordValue(
|
|
895
|
+
mappedRecordAttribute,
|
|
896
|
+
cleaningRecordAttributes[cleaningRecordAttribute]
|
|
897
|
+
),
|
|
898
|
+
ack: true,
|
|
899
|
+
}
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const objectString = `Devices.${duid}.cleaningInfo.JSON`;
|
|
905
|
+
await this.adapter.createStateObjectHelper(
|
|
906
|
+
objectString,
|
|
907
|
+
"cleaningInfoJSON",
|
|
908
|
+
"string",
|
|
909
|
+
null,
|
|
910
|
+
null,
|
|
911
|
+
"json",
|
|
912
|
+
true,
|
|
913
|
+
false
|
|
914
|
+
);
|
|
915
|
+
this.adapter.setStateAsync(`Devices.${duid}.cleaningInfo.JSON`, {
|
|
916
|
+
val: JSON.stringify(cleaningRecordsJSON),
|
|
917
|
+
ack: true,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
} catch (error) {
|
|
922
|
+
this.adapter.catchError(
|
|
923
|
+
error,
|
|
924
|
+
"get_clean_summary",
|
|
925
|
+
duid,
|
|
926
|
+
this.robotModel
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
calculateCleaningValue(attribute, value) {
|
|
932
|
+
switch (attribute) {
|
|
933
|
+
case "clean_time":
|
|
934
|
+
return Math.round(value / 60 / 60);
|
|
935
|
+
case "clean_area":
|
|
936
|
+
return Math.round(value / 1000 / 1000);
|
|
937
|
+
default:
|
|
938
|
+
return value;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
calculateRecordValue(attribute, value) {
|
|
943
|
+
switch (attribute) {
|
|
944
|
+
case "begin":
|
|
945
|
+
case "end":
|
|
946
|
+
return new Date(value * 1000).toString();
|
|
947
|
+
case "duration":
|
|
948
|
+
return Math.round(value / 60);
|
|
949
|
+
case "area":
|
|
950
|
+
return Math.round(value / 1000 / 1000);
|
|
951
|
+
default:
|
|
952
|
+
return value;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
unzipBuffer(buffer, callback) {
|
|
957
|
+
zlib.gunzip(buffer, callback);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
isGZIP(buffer) {
|
|
961
|
+
return buffer.length >= 2 && buffer[0] == 31 && buffer[1] == 139;
|
|
962
|
+
}
|
|
963
|
+
extractPhoto(buffer) {
|
|
964
|
+
// Verify that the buffer is long enough to hold the header
|
|
965
|
+
if (buffer.length < 10) {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Check the signature
|
|
970
|
+
if (
|
|
971
|
+
buffer[26] == 74 &&
|
|
972
|
+
buffer[27] == 70 &&
|
|
973
|
+
buffer[28] == 73 &&
|
|
974
|
+
buffer[29] == 70
|
|
975
|
+
) {
|
|
976
|
+
return buffer.slice(20);
|
|
977
|
+
} else if (
|
|
978
|
+
buffer[42] == 74 &&
|
|
979
|
+
buffer[43] == 70 &&
|
|
980
|
+
buffer[44] == 73 &&
|
|
981
|
+
buffer[45] == 70
|
|
982
|
+
) {
|
|
983
|
+
return buffer.slice(36);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
module.exports = {
|
|
991
|
+
vacuum,
|
|
992
|
+
};
|