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