matterbridge-roborock-vacuum-plugin 1.1.0-rc10 โ†’ 1.1.0-rc11

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 (37) hide show
  1. package/dist/platform.js +1 -4
  2. package/dist/roborockCommunication/RESTAPI/roborockIoTApi.js +11 -0
  3. package/dist/roborockCommunication/Zmodel/mapInfo.js +8 -6
  4. package/dist/roborockCommunication/broadcast/abstractClient.js +8 -1
  5. package/dist/roborockCommunication/broadcast/listener/implementation/syncMessageListener.js +3 -2
  6. package/dist/roborockCommunication/broadcast/messageProcessor.js +6 -3
  7. package/dist/roborockCommunication/helper/messageDeserializer.js +7 -1
  8. package/dist/roborockCommunication/helper/messageSerializer.js +6 -0
  9. package/dist/roborockService.js +55 -6
  10. package/eslint.config.js +1 -1
  11. package/matterbridge-roborock-vacuum-plugin.config.json +1 -1
  12. package/matterbridge-roborock-vacuum-plugin.schema.json +1 -1
  13. package/package.json +1 -1
  14. package/src/platform.ts +1 -5
  15. package/src/roborockCommunication/RESTAPI/roborockIoTApi.ts +13 -1
  16. package/src/roborockCommunication/Zmodel/device.ts +1 -0
  17. package/src/roborockCommunication/Zmodel/mapInfo.ts +9 -6
  18. package/src/roborockCommunication/Zmodel/userData.ts +0 -3
  19. package/src/roborockCommunication/broadcast/abstractClient.ts +10 -3
  20. package/src/roborockCommunication/broadcast/client.ts +1 -1
  21. package/src/roborockCommunication/broadcast/clientRouter.ts +1 -1
  22. package/src/roborockCommunication/broadcast/listener/implementation/syncMessageListener.ts +4 -3
  23. package/src/roborockCommunication/broadcast/messageProcessor.ts +9 -5
  24. package/src/roborockCommunication/helper/messageDeserializer.ts +7 -1
  25. package/src/roborockCommunication/helper/messageSerializer.ts +6 -0
  26. package/src/roborockService.ts +64 -13
  27. package/src/tests/roborockCommunication/broadcast/listener/implementation/syncMessageListener.test.ts +5 -4
  28. package/web-for-testing/README.md +47 -0
  29. package/web-for-testing/nodemon.json +7 -0
  30. package/web-for-testing/package-lock.json +6598 -0
  31. package/web-for-testing/package.json +36 -0
  32. package/web-for-testing/src/accountStore.ts +8 -0
  33. package/web-for-testing/src/app.ts +194 -0
  34. package/web-for-testing/tsconfig-ext.json +19 -0
  35. package/web-for-testing/tsconfig.json +23 -0
  36. package/web-for-testing/views/index.ejs +172 -0
  37. package/web-for-testing/watch.mjs +93 -0
@@ -39,6 +39,7 @@ export default class RoborockService {
39
39
  messageProcessorMap = new Map<string, MessageProcessor>();
40
40
  ipMap = new Map<string, string>();
41
41
  localClientMap = new Map<string, Client>();
42
+ mqttAlwaysOnDevices = new Map<string, boolean>();
42
43
  clientManager: ClientManager;
43
44
  refreshInterval: number;
44
45
  requestDeviceStatusInterval: NodeJS.Timeout | undefined;
@@ -48,6 +49,8 @@ export default class RoborockService {
48
49
  private supportedRoutines = new Map<string, ServiceArea.Area[]>();
49
50
  private selectedAreas = new Map<string, number[]>();
50
51
 
52
+ private readonly vacuumNeedAPIV3 = ['roborock.vacuum.ss07'];
53
+
51
54
  constructor(
52
55
  authenticateApiSupplier: Factory<void, RoborockAuthenticateApi> = (logger) => new RoborockAuthenticateApi(logger),
53
56
  iotApiSupplier: Factory<UserData, RoborockIoTApi> = (logger, ud) => new RoborockIoTApi(ud, logger),
@@ -117,15 +120,16 @@ export default class RoborockService {
117
120
  }
118
121
 
119
122
  public async getRoomIdFromMap(duid: string): Promise<number | undefined> {
120
- const data = (await this.customGet(duid, new RequestMessage({ method: 'get_map_v1' }))) as { vacuumRoom: number | undefined };
123
+ const data = (await this.customGet(duid, new RequestMessage({ method: 'get_map_v1' }))) as { vacuumRoom?: number };
121
124
  return data?.vacuumRoom;
122
125
  }
123
126
 
124
- public async getMapInformation(duid: string): Promise<MapInfo> {
127
+ public async getMapInformation(duid: string): Promise<MapInfo | undefined> {
125
128
  this.logger.debug('RoborockService - getMapInformation', duid);
126
129
  assert(this.messageClient !== undefined);
127
- return this.messageClient.get<MultipleMap[]>(duid, new RequestMessage({ method: 'get_multi_maps_list' })).then((response) => {
128
- return new MapInfo(response[0]);
130
+ return this.messageClient.get<MultipleMap[] | undefined>(duid, new RequestMessage({ method: 'get_multi_maps_list' })).then((response) => {
131
+ this.logger.debug('RoborockService - getMapInformation response', debugStringify(response ?? []));
132
+ return response ? new MapInfo(response[0]) : undefined;
129
133
  });
130
134
  }
131
135
 
@@ -212,6 +216,17 @@ export default class RoborockService {
212
216
  return this.getMessageProcessor(duid)?.sendCustomMessage(duid, request);
213
217
  }
214
218
 
219
+ public async getCustomAPI(url: string): Promise<unknown> {
220
+ this.logger.debug('RoborockService - getCustomAPI', url);
221
+ assert(this.iotApi !== undefined);
222
+ try {
223
+ return await this.iotApi.getCustom(url);
224
+ } catch (error) {
225
+ this.logger.error(`Failed to get custom API with url ${url}: ${error ? debugStringify(error) : 'undefined'}`);
226
+ return { result: undefined, error: `Failed to get custom API with url ${url}` };
227
+ }
228
+ }
229
+
215
230
  public stopService(): void {
216
231
  if (this.messageClient) {
217
232
  this.messageClient.disconnect();
@@ -245,15 +260,15 @@ export default class RoborockService {
245
260
  this.deviceNotify = callback;
246
261
  }
247
262
 
248
- public async activateDeviceNotify(device: Device): Promise<void> {
263
+ public activateDeviceNotify(device: Device): void {
249
264
  // eslint-disable-next-line @typescript-eslint/no-this-alias
250
265
  const self = this;
251
266
  this.logger.debug('Requesting device info for device', device.duid);
252
267
  const messageProcessor = this.getMessageProcessor(device.duid);
253
268
  this.requestDeviceStatusInterval = setInterval(async () => {
254
269
  if (messageProcessor) {
255
- await messageProcessor.getDeviceStatus(device.duid).then((response: DeviceStatus) => {
256
- if (self.deviceNotify) {
270
+ await messageProcessor.getDeviceStatus(device.duid).then((response: DeviceStatus | undefined) => {
271
+ if (self.deviceNotify && response) {
257
272
  const message = { duid: device.duid, ...response.errorStatus, ...response.message } as DeviceStatusNotify;
258
273
  self.logger.debug('Device status update', debugStringify(message));
259
274
  self.deviceNotify(NotifyMessageTypes.LocalMessage, message);
@@ -284,6 +299,16 @@ export default class RoborockService {
284
299
  const products = new Map<string, string>();
285
300
  homeData.products.forEach((p) => products.set(p.id, p.model));
286
301
 
302
+ if (homeData.products.some((p) => this.vacuumNeedAPIV3.includes(p.model))) {
303
+ this.logger.debug('Using v3 API for home data retrieval');
304
+ const homeDataV3 = await this.iotApi.getHomev3(homeDetails.rrHomeId);
305
+ if (!homeDataV3) {
306
+ throw new Error('Failed to retrieve the home data from v3 API');
307
+ }
308
+ homeData.devices = [...homeData.devices, ...homeDataV3.devices.filter((d) => !homeData.devices.some((x) => x.duid === d.duid))];
309
+ homeData.receivedDevices = [...homeData.receivedDevices, ...homeDataV3.receivedDevices.filter((d) => !homeData.receivedDevices.some((x) => x.duid === d.duid))];
310
+ }
311
+
287
312
  // Try to get rooms from v2 API if rooms are empty
288
313
  if (homeData.rooms.length === 0) {
289
314
  const homeDataV2 = await this.iotApi.getHomev2(homeDetails.rrHomeId);
@@ -298,7 +323,6 @@ export default class RoborockService {
298
323
  }
299
324
 
300
325
  const devices: Device[] = [...homeData.devices, ...homeData.receivedDevices];
301
- // homeData.devices.length > 0 ? homeData.devices : homeData.receivedDevices;
302
326
 
303
327
  const result = devices.map((device) => {
304
328
  return {
@@ -316,6 +340,7 @@ export default class RoborockService {
316
340
  model: homeData.products.find((p) => p.id === device.productId)?.model,
317
341
  category: homeData.products.find((p) => p.id === device.productId)?.category,
318
342
  batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100,
343
+ schema: homeData.products.find((p) => p.id === device.productId)?.schema,
319
344
  },
320
345
 
321
346
  store: {
@@ -344,6 +369,18 @@ export default class RoborockService {
344
369
  homeData.products.forEach((p) => products.set(p.id, p.model));
345
370
  const devices: Device[] = homeData.devices.length > 0 ? homeData.devices : homeData.receivedDevices;
346
371
 
372
+ if (homeData.rooms.length === 0) {
373
+ const homeDataV3 = await this.iotApi.getHomev3(homeid);
374
+ if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
375
+ homeData.rooms = homeDataV3.rooms;
376
+ } else {
377
+ const homeDataV1 = await this.iotApi.getHome(homeid);
378
+ if (homeDataV1 && homeDataV1.rooms && homeDataV1.rooms.length > 0) {
379
+ homeData.rooms = homeDataV1.rooms;
380
+ }
381
+ }
382
+ }
383
+
347
384
  const dvs = devices.map((device) => {
348
385
  return {
349
386
  ...device,
@@ -357,6 +394,7 @@ export default class RoborockService {
357
394
  model: homeData.products.find((p) => p.id === device.productId)?.model,
358
395
  category: homeData.products.find((p) => p.id === device.productId)?.category,
359
396
  batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100,
397
+ schema: homeData.products.find((p) => p.id === device.productId)?.schema,
360
398
  },
361
399
 
362
400
  store: {
@@ -384,13 +422,13 @@ export default class RoborockService {
384
422
  return this.iotApi.startScene(sceneId);
385
423
  }
386
424
 
387
- public getRoomMappings(duid: string): Promise<number[][]> | undefined {
425
+ public getRoomMappings(duid: string): Promise<number[][] | undefined> {
388
426
  if (!this.messageClient) {
389
427
  this.logger.warn('messageClient not initialized. Waititing for next execution');
390
- return undefined;
428
+ return Promise.resolve(undefined);
391
429
  }
392
430
 
393
- return this.messageClient.get(duid, new RequestMessage({ method: 'get_room_mapping' }));
431
+ return this.messageClient.get(duid, new RequestMessage({ method: 'get_room_mapping', secure: this.isRequestSecure(duid) }));
394
432
  }
395
433
 
396
434
  public async initializeMessageClient(username: string, device: Device, userdata: UserData): Promise<void> {
@@ -434,6 +472,7 @@ export default class RoborockService {
434
472
  this.logger.error('messageClient not initialized');
435
473
  return false;
436
474
  }
475
+
437
476
  // eslint-disable-next-line @typescript-eslint/no-this-alias
438
477
  const self = this;
439
478
 
@@ -462,6 +501,15 @@ export default class RoborockService {
462
501
 
463
502
  this.messageProcessorMap.set(device.duid, messageProcessor);
464
503
 
504
+ this.logger.debug('Checking if device supports local connection', device.pv, device.data.model, device.duid);
505
+ if (device.pv === 'B01') {
506
+ this.logger.warn('Device does not support local connection', device.duid);
507
+ this.mqttAlwaysOnDevices.set(device.duid, true);
508
+ return true;
509
+ } else {
510
+ this.mqttAlwaysOnDevices.set(device.duid, false);
511
+ }
512
+
465
513
  this.logger.debug('Local device', device.duid);
466
514
  let localIp = this.ipMap.get(device.duid);
467
515
  try {
@@ -507,8 +555,7 @@ export default class RoborockService {
507
555
  }
508
556
 
509
557
  private onLocalClientDisconnect(duid: string): void {
510
- // this.mqttAlwaysOnDevices.set(duid, true);
511
- this.logger.debug('Local client disconnected for device', duid);
558
+ this.mqttAlwaysOnDevices.set(duid, true);
512
559
  }
513
560
 
514
561
  private sleep(ms: number): Promise<void> {
@@ -520,4 +567,8 @@ export default class RoborockService {
520
567
  this.iotApi = this.iotApiFactory(this.logger, userdata);
521
568
  return userdata;
522
569
  }
570
+
571
+ private isRequestSecure(duid: string): boolean {
572
+ return this.mqttAlwaysOnDevices.get(duid) ?? false;
573
+ }
523
574
  }
@@ -1,5 +1,6 @@
1
1
  import { SyncMessageListener } from '../../../../../roborockCommunication/broadcast/listener/implementation/syncMessageListener';
2
2
  import { Protocol } from '../../../../../roborockCommunication/broadcast/model/protocol';
3
+ import { RequestMessage } from '../../../../../roborockCommunication/broadcast/model/requestMessage';
3
4
 
4
5
  describe('SyncMessageListener', () => {
5
6
  let listener: SyncMessageListener;
@@ -20,7 +21,7 @@ describe('SyncMessageListener', () => {
20
21
  const resolve = jest.fn();
21
22
  const reject = jest.fn();
22
23
  const messageId = 123;
23
- listener.waitFor(messageId, resolve, reject);
24
+ listener.waitFor(messageId, { method: 'test' } as RequestMessage, resolve, reject);
24
25
 
25
26
  const dps = { id: messageId, result: { foo: 'bar' } };
26
27
  const message = {
@@ -38,7 +39,7 @@ describe('SyncMessageListener', () => {
38
39
  const resolve = jest.fn();
39
40
  const reject = jest.fn();
40
41
  const messageId = 456;
41
- listener.waitFor(messageId, resolve, reject);
42
+ listener.waitFor(messageId, { method: 'test' } as RequestMessage, resolve, reject);
42
43
 
43
44
  const dps = { id: messageId, result: ['ok'] };
44
45
  const message = {
@@ -56,7 +57,7 @@ describe('SyncMessageListener', () => {
56
57
  const resolve = jest.fn();
57
58
  const reject = jest.fn();
58
59
  const messageId = 789;
59
- listener.waitFor(messageId, resolve, reject);
60
+ listener.waitFor(messageId, { method: 'test' } as RequestMessage, resolve, reject);
60
61
 
61
62
  const dps = { id: messageId };
62
63
  const message = {
@@ -73,7 +74,7 @@ describe('SyncMessageListener', () => {
73
74
  const resolve = jest.fn();
74
75
  const reject = jest.fn();
75
76
  const messageId = 321;
76
- listener.waitFor(messageId, resolve, reject);
77
+ listener.waitFor(messageId, { method: 'test' } as RequestMessage, resolve, reject);
77
78
 
78
79
  expect(listener['pending'].has(messageId)).toBe(true);
79
80
 
@@ -0,0 +1,47 @@
1
+ # Web Testing Interface
2
+
3
+ This is a web-based testing interface for the Matterbridge Roborock Vacuum Plugin.
4
+
5
+ ## ๐Ÿš€ Quick Start
6
+
7
+ ### Prerequisites
8
+ - Node.js installed on your system
9
+ - Access to the main plugin source code
10
+
11
+ ### Installation & Setup
12
+
13
+ 1. **Copy source files:**
14
+ ```bash
15
+ npm run copy-src
16
+ ```
17
+
18
+ 2. **Build the project:**
19
+ ```bash
20
+ npm run build
21
+ ```
22
+
23
+ 3. **Start the development server:**
24
+ ```bash
25
+ npm run dev
26
+ ```
27
+
28
+ ## ๐ŸŒ Access
29
+
30
+ Once the server is running, open your browser and navigate to:
31
+ ```
32
+ http://localhost:3000
33
+ ```
34
+
35
+ ## ๐Ÿ” Authentication
36
+
37
+ Log in using your Roborock account credentials:
38
+ - **Username:** Your Roborock account username/email
39
+ - **Password:** Your Roborock account password
40
+
41
+ ## ๐Ÿงช Testing
42
+
43
+ After successful authentication, you can start testing the plugin functionality through the web interface.
44
+
45
+ ---
46
+
47
+ **Note:** This is a development/testing tool and should not be used in production environments.
@@ -0,0 +1,7 @@
1
+ {
2
+ "watch": [
3
+ "src"
4
+ ],
5
+ "ext": "ts",
6
+ "exec": "node --loader ts-node/esm --experimental-specifier-resolution=node ./src/app.ts"
7
+ }