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,447 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+
5
+ const TYPES = {
6
+ CHARGER_LOCATION: 1,
7
+ IMAGE: 2,
8
+ PATH: 3,
9
+ GOTO_PATH: 4,
10
+ GOTO_PREDICTED_PATH: 5,
11
+ CURRENTLY_CLEANED_ZONES: 6,
12
+ GOTO_TARGET: 7,
13
+ ROBOT_POSITION: 8,
14
+ FORBIDDEN_ZONES: 9,
15
+ VIRTUAL_WALLS: 10,
16
+ CURRENTLY_CLEANED_BLOCKS: 11,
17
+ NO_MOP_ZONE: 12,
18
+ OBSTACLES: 13,
19
+ IGNORED_OBSTACLES: 14,
20
+ OBSTACLES2: 15,
21
+ IGNORED_OBSTACLES2: 16,
22
+ CARPET_MAP: 17,
23
+ MOP_PATH: 18,
24
+ CARPET_FORBIDDEN_ZONE: 19,
25
+ SMART_ZONE_PATH_TYPE: 20,
26
+ SMART_ZONE: 21,
27
+ CUSTOM_CARPET: 22,
28
+ CL_FORBIDDEN_ZONES: 23,
29
+ FLOOR_MAP: 24,
30
+ FURNITURES: 25,
31
+ DOCK_TYPE: 26,
32
+ ENEMIES: 27,
33
+ DS_FORBIDDEN_ZONES: 28, // WTF IS DS???
34
+ STUCK_POINTS: 29, // not currently processed
35
+ CLF_FORBIDDEN_ZONES: 30, // WTF IS CLF???
36
+ SMART_DS: 31, // not currently processed
37
+ FLOOR_DIRECTION: 32, // not 100% sure this FLOOR_DIRECTION but Roborock defined this as flDirec
38
+ DATE: 33, // not currently processed
39
+ NONCEDATA: 34,
40
+ EXT_ZONES: 36, // not currently processed
41
+ PATROL: 37, // not currently processed
42
+ PET_PATROL: 38, // not currently processed
43
+ MODE_CARPET: 39, // not currently processed
44
+ STROY_PT: 41, // not currently processed
45
+ DIRTY_RECT: 42, // not currently processed
46
+ IGNORE_DIRTY_RECT: 43, // not currently processed
47
+ BRUSH_PT: 44, // not currently processed
48
+ DIRTY_NEW: 45, // not currently processed
49
+ DIGEST: 1024,
50
+ };
51
+ const TYPES_REVERSE = Object.fromEntries(Object.entries(TYPES).map(([key, value]) => [value, key]));
52
+
53
+ const OFFSETS = {
54
+ HLENGTH: 0x02,
55
+ LENGTH: 0x04,
56
+ TYPE_COUNT: 0x08,
57
+ TARGET_X: 0x08,
58
+ ANGLE: 0x10,
59
+ PATH: 0x14,
60
+ TARGET_Y: 0x0a,
61
+ BLOCKS: 0x0c,
62
+ };
63
+
64
+ class RRMapParser {
65
+ constructor(adapter) {
66
+ this.adapter = adapter;
67
+ }
68
+
69
+ BytesToInt(buffer, offset, len) {
70
+ let result = 0;
71
+
72
+ for (let i = 0; i < len; i++) {
73
+ result |= (0x000000FF & parseInt(buffer[i + offset])) << 8 * i;
74
+ }
75
+
76
+ return result;
77
+ }
78
+
79
+ async parsedata(buf) {
80
+ const metaData = this.PARSE(buf);
81
+ if (!metaData.map_index) {
82
+ this.adapter.log.error(`RRMapParser: Failed to parse map data. map_index was missing`);
83
+ return {};
84
+ }
85
+
86
+ if (metaData.SHA1 != metaData.expectedSHA1) {
87
+ this.adapter.log.error(`Invalid map hash!`);
88
+ return {};
89
+ }
90
+
91
+ let dataPosition = 0x14; // Skip header
92
+
93
+ const result = {};
94
+ result.metaData = metaData;
95
+
96
+ while (dataPosition < metaData.data_length) {
97
+ const type = buf.readUInt16LE(dataPosition);
98
+ const hlength = buf.readUInt16LE(dataPosition + OFFSETS.HLENGTH);
99
+ const length = buf.readUInt32LE(dataPosition + OFFSETS.LENGTH);
100
+
101
+ const blockBuffer = buf.slice(dataPosition, dataPosition + hlength + length);
102
+ const [offset1, offset2] = this.getTwoByteOffsets(blockBuffer);
103
+
104
+ // this.adapter.log.debug("Known values: type=" + type + ", hlength=" + hlength + ", length=" + length);
105
+
106
+ if (TYPES_REVERSE[type]) {
107
+
108
+ // this.adapter.log.debug("Test length: " + TYPES_REVERSE[type] + " " + length);
109
+ // if (length < 100) this.adapter.log.debug("Test data type: " + TYPES_REVERSE[type] + " " + buf.toString("hex", dataPosition, dataPosition + length));
110
+
111
+ // this.adapter.log.warn(`Block type buffer data: ${TYPES_REVERSE[type]} ${JSON.stringify(blockBuffer)}`);
112
+ // this.adapter.log.warn(`Block type hex data: ${TYPES_REVERSE[type]} ${blockBuffer.toString("hex")}`);
113
+
114
+ switch (type) {
115
+ case TYPES.ROBOT_POSITION:
116
+ case TYPES.CHARGER_LOCATION: {
117
+ const position = this.getXYPositions(blockBuffer, offset1, offset2);
118
+ const angle = length >= 12 ? this.getAngle(blockBuffer) : 0; // gen3+
119
+
120
+ result[TYPES_REVERSE[type]] = {
121
+ position,
122
+ angle,
123
+ };
124
+ break;
125
+ }
126
+ case TYPES.IMAGE: {
127
+ const offset = this.getSingleByteOffset(blockBuffer);
128
+ const [left, top, width, height] = this.getMapSizes(blockBuffer, offset1);
129
+
130
+ let parameters = {};
131
+ parameters = {
132
+ segments: {
133
+ count: hlength > 24 ? this.getCount(blockBuffer) : 0,
134
+ id: [],
135
+ },
136
+ position: {
137
+ top: top,
138
+ left: left,
139
+ },
140
+ dimensions: {
141
+ height: height,
142
+ width: width,
143
+ },
144
+ pixels: {
145
+ floor: [],
146
+ obstacle: [],
147
+ segments: [],
148
+ },
149
+ };
150
+
151
+ if (parameters.dimensions.height > 0 && parameters.dimensions.width > 0) {
152
+ let segmenetID = 0;
153
+
154
+ for (let i = 0; i < length; i++) {
155
+ const pixelType = this.getPixelType(buf, dataPosition + i + offset1);
156
+
157
+ if (pixelType == 1) {
158
+ // Obstacle
159
+ parameters.pixels.obstacle.push(i);
160
+ } else if (pixelType != 0) {
161
+ // Floor
162
+ parameters.pixels.floor.push(i);
163
+
164
+ segmenetID = (buf.readUInt8(offset + dataPosition + i) & 248) >> 3;
165
+ if (segmenetID !== 0) {
166
+ if (!parameters.segments.id.includes(segmenetID)) parameters.segments.id.push(segmenetID); // Add segment ID to array if it doesn't exist
167
+
168
+ parameters.pixels.segments.push(i | (segmenetID << 21)); // Add segment ID to pixel
169
+ }
170
+ }
171
+ }
172
+ }
173
+ result[TYPES_REVERSE[type]] = parameters;
174
+ break;
175
+ }
176
+ case TYPES.CARPET_MAP: {
177
+ result[TYPES_REVERSE[type]] = [];
178
+
179
+ for (let i = 0; i < length; i++) {
180
+ // Only add the pixel index to the carpet array if it is a carpet pixel
181
+ if (this.getPixelType(buf, dataPosition + i) == 1) {
182
+ result[TYPES_REVERSE[type]].push(i);
183
+ }
184
+ }
185
+
186
+ break;
187
+ }
188
+ case TYPES.MOP_PATH: {
189
+ result[TYPES_REVERSE[type]] = [];
190
+
191
+ for (let i = 0; i < length; i++) {
192
+ result[TYPES_REVERSE[type]].push(...this.readUInt8(buf, dataPosition + i, OFFSETS.PATH, 1));
193
+ }
194
+
195
+ break;
196
+ }
197
+ case TYPES.PATH:
198
+ case TYPES.GOTO_PATH:
199
+ case TYPES.GOTO_PREDICTED_PATH: {
200
+ const pathType = TYPES_REVERSE[type];
201
+ result[pathType] = {
202
+ current_angle: this.getAngle(blockBuffer),
203
+ points: [],
204
+ };
205
+
206
+ for (let i = 0; i < length; i = i + 4) {
207
+ result[pathType].points.push(this.getPointInPath(buf, dataPosition + i));
208
+ }
209
+
210
+ if (result[pathType].points.length >= 2) {
211
+ const lastPoint = result[pathType].points[result[pathType].points.length - 1];
212
+ const secondLastPoint = result[pathType].points[result[pathType].points.length - 2];
213
+
214
+ result[pathType].current_angle =
215
+ (Math.atan2(
216
+ // Calculate the angle between the last two points
217
+ lastPoint[1] - secondLastPoint[1],
218
+ lastPoint[0] - secondLastPoint[0]
219
+ ) *
220
+ 180) /
221
+ Math.PI;
222
+ }
223
+ break;
224
+ }
225
+
226
+ case TYPES.GOTO_TARGET:
227
+ result[TYPES_REVERSE[type]] = this.getGoToTarget(blockBuffer);
228
+ break;
229
+
230
+ case TYPES.CURRENTLY_CLEANED_ZONES:
231
+ case TYPES.VIRTUAL_WALLS: {
232
+ const wallCount = buf.readUInt32LE(0x08 + dataPosition);
233
+ result[TYPES_REVERSE[type]] = [];
234
+
235
+ for (let i = 0; i < wallCount; i++) {
236
+ const wallDataPosition = dataPosition + i * 8; // 8 Bytes pro Wand
237
+ result[TYPES_REVERSE[type]].push(this.readUInt16LE(buf, wallDataPosition, offset1, 4));
238
+ }
239
+ break;
240
+ }
241
+ case TYPES.FORBIDDEN_ZONES:
242
+ case TYPES.NO_MOP_ZONE:
243
+ case TYPES.CARPET_FORBIDDEN_ZONE:
244
+ case TYPES.DS_FORBIDDEN_ZONES:
245
+ case TYPES.CLF_FORBIDDEN_ZONES:
246
+ case TYPES.MODE_CARPET: {
247
+ const zoneCount = this.getCount(blockBuffer);
248
+ result[TYPES_REVERSE[type]] = [];
249
+ for (let i = 0; i < zoneCount; i++) {
250
+ const zoneDataPosition = dataPosition + i * 16; // 16 Bytes pro Zone
251
+ result[TYPES_REVERSE[type]].push(this.getForbiddenZone(buf, zoneDataPosition, offset1));
252
+ }
253
+ break;
254
+ }
255
+ case TYPES.OBSTACLES2:
256
+ result[TYPES_REVERSE[type]] = this.extractObstacles(blockBuffer, offset1);
257
+ break;
258
+ case TYPES.CURRENTLY_CLEANED_BLOCKS: {
259
+ const blockCount = this.getCount(blockBuffer);
260
+ result[TYPES_REVERSE[type]] = [];
261
+
262
+ for (let i = 0; i < blockCount; i++) {
263
+ result[TYPES_REVERSE[type]].push(buf.readUInt8(OFFSETS.BLOCKS + dataPosition + i));
264
+ }
265
+ break;
266
+ }
267
+ case TYPES.NONCEDATA:
268
+ result[TYPES_REVERSE[type]] = this.getNonceData(blockBuffer);
269
+ this.adapter.log.debug(`Block type 34 debug: ${JSON.stringify(result[TYPES_REVERSE[type]])}`);
270
+ break;
271
+ }
272
+ } else {
273
+ this.adapter.log.warn(`Unknown block type! Please report this to the developer. Block type is: ${type} and a length of ${length}`);
274
+ this.adapter.log.warn(`Unknown block type hex data: ${TYPES_REVERSE[type]} ${blockBuffer.toString("hex")}`);
275
+ this.adapter.log.warn(`Unknown block type buffer data: ${TYPES_REVERSE[type]} ${JSON.stringify(blockBuffer)}`);
276
+ }
277
+ dataPosition = dataPosition + length + hlength;
278
+ }
279
+
280
+ return result;
281
+ }
282
+
283
+ /**
284
+ *
285
+ * @param mapBuf {Buffer} Should contain map in RRMap Format
286
+ * @return {object}
287
+ */
288
+ PARSE(mapBuf) {
289
+ if (mapBuf && mapBuf[0x00] === 0x72 && mapBuf[0x01] === 0x72) {
290
+ return {
291
+ header_length: mapBuf.readUInt16LE(OFFSETS.HLENGTH),
292
+ data_length: mapBuf.readUInt32LE(OFFSETS.LENGTH),
293
+ version: {
294
+ major: mapBuf.readUInt16LE(0x08),
295
+ minor: mapBuf.readUInt16LE(0x0a),
296
+ },
297
+ map_index: mapBuf.readUInt32LE(0x0C),
298
+ map_sequence: mapBuf.readUInt32LE(0x10),
299
+ SHA1: crypto.createHash("sha1").update(Uint8Array.prototype.slice.call(mapBuf, 0, mapBuf.length - 20)).digest("hex"),
300
+ expectedSHA1: Buffer.from(Uint8Array.prototype.slice.call(mapBuf, mapBuf.length - 20)).toString("hex"),
301
+ };
302
+ } else {
303
+ return {};
304
+ }
305
+ }
306
+
307
+ extractObstacles(buf, offset) {
308
+ const obstacleCount = this.getCount(buf);
309
+ const obstacles = [];
310
+
311
+ for (let i = 0; i < obstacleCount * 28; i += 28) {
312
+ const obstacle = [
313
+ buf.readUInt16LE(offset + i), // x
314
+ buf.readUInt16LE(offset + i + 2), // y
315
+ buf.readUInt16LE(offset + i + 4), // obstacle type
316
+ buf.readUInt16LE(offset + i + 6), // confidence level
317
+ buf.readUInt16LE(offset + i + 8), // unknown
318
+ buf.readUInt16LE(offset + i + 10), // unknown
319
+ buf.toString("utf-8", offset + i + 12, offset + i + 12 + 16), // photo id
320
+ ];
321
+ obstacles.push(obstacle);
322
+ }
323
+
324
+ return obstacles;
325
+ }
326
+
327
+ getXYPositions(buf, xOffset, yOffset) {
328
+ const xPosition = buf.readInt32LE(xOffset);
329
+ const yPosition = buf.readInt32LE(yOffset);
330
+
331
+ return [xPosition, yPosition];
332
+ }
333
+
334
+ getMapSizes(buf, offset) {
335
+ const top = buf.readInt32LE(offset - 0x10);
336
+ const left = buf.readInt32LE(offset - 0x0c);
337
+ const height = buf.readInt32LE(offset - 0x08);
338
+ const width = buf.readInt32LE(offset - 0x04);
339
+
340
+ return [left, top, width, height];
341
+ }
342
+
343
+ getPointInPath(buf, dataPosition) {
344
+ const result = [];
345
+ for (let i = 0; i < 2; i++) {
346
+ result.push(buf.readUInt16LE(dataPosition + OFFSETS.PATH + i * 2));
347
+ }
348
+
349
+ return result;
350
+ }
351
+
352
+ getCount(buf) {
353
+ return buf.readUInt32LE(OFFSETS.TYPE_COUNT);
354
+ }
355
+
356
+ getPixelType(buf, dataPosition) {
357
+ // Get the pixel type with bitwise AND operation of 0x07
358
+ return buf.readUInt8(dataPosition) & 0x07;
359
+ }
360
+
361
+ getAngle(buf) {
362
+ return buf.readInt32LE(OFFSETS.ANGLE);
363
+ }
364
+
365
+ getGoToTarget(buf) {
366
+ return [buf.readUInt16LE(OFFSETS.TARGET_X), buf.readUInt16LE(OFFSETS.TARGET_Y)];
367
+ }
368
+
369
+ getForbiddenZone(buf, dataPosition, offset) {
370
+ return this.readUInt16LE(buf, dataPosition, offset, 8);
371
+ }
372
+
373
+ getSingleByteOffset(buf) {
374
+ return buf.readUInt8(2);
375
+ }
376
+
377
+ getTwoByteOffsets(buf) {
378
+ return [buf.readUInt8(2), buf.readUInt8(4)];
379
+ }
380
+
381
+ getDatatype(buf, offset) {
382
+ // Get the first byte of the block
383
+ const byte = buf[offset];
384
+
385
+ // Check the byte value
386
+ if (byte >= 0x00 && byte <= 0xff) {
387
+ // It's an unsigned byte
388
+ return "UInt8";
389
+ } else if (byte >= 0x00 && byte <= 0xffff) {
390
+ // It's an unsigned 16-bit little-endian integer
391
+ return "UInt16LE";
392
+ } else if (byte >= 0x00 && byte <= 0xffffffff) {
393
+ // It's an unsigned 32-bit little-endian integer
394
+ return "UInt32LE";
395
+ } else {
396
+ // It's an unknown type
397
+ return "Unknown";
398
+ }
399
+ }
400
+
401
+ getNonceData(buf) {
402
+ const sections = [];
403
+
404
+ for (let i = 12; i < buf.length; i += 5) {
405
+ const type = buf[i];
406
+ const unixTime = buf.readUInt32LE(i + 1);
407
+
408
+ sections.push({ type, unixTime });
409
+ }
410
+
411
+ return sections;
412
+ }
413
+
414
+ readUInt16LE(buf, dataPosition, offset, count) {
415
+ const result = [];
416
+ for (let j = 0; j < count; j++) {
417
+ result.push(buf.readUInt16LE(dataPosition + offset + j * 2));
418
+ }
419
+ return result;
420
+ }
421
+
422
+ readInt32LE(buf, dataPosition, offset, count) {
423
+ const array = [];
424
+ for (let j = 0; j < count; j++) {
425
+ array.push(buf.readInt32LE(offset + dataPosition + j * 4));
426
+ }
427
+ return array;
428
+ }
429
+
430
+ readUInt32LE(buf, dataPosition, offset, count) {
431
+ const array = [];
432
+ for (let j = 0; j < count; j++) {
433
+ array.push(buf.readUInt32LE(offset + dataPosition + j * 4));
434
+ }
435
+ return array;
436
+ }
437
+
438
+ readUInt8(buf, dataPosition, offset, count) {
439
+ const array = [];
440
+ for (let j = 0; j < count; j++) {
441
+ array.push(buf.readUInt8(offset + dataPosition + j));
442
+ }
443
+ return array;
444
+ }
445
+ }
446
+
447
+ module.exports = RRMapParser;