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.
- package/dist/platform.js +1 -4
- package/dist/roborockCommunication/RESTAPI/roborockIoTApi.js +11 -0
- package/dist/roborockCommunication/Zmodel/mapInfo.js +8 -6
- package/dist/roborockCommunication/broadcast/abstractClient.js +8 -1
- package/dist/roborockCommunication/broadcast/listener/implementation/syncMessageListener.js +3 -2
- package/dist/roborockCommunication/broadcast/messageProcessor.js +6 -3
- package/dist/roborockCommunication/helper/messageDeserializer.js +7 -1
- package/dist/roborockCommunication/helper/messageSerializer.js +6 -0
- package/dist/roborockService.js +55 -6
- package/eslint.config.js +1 -1
- package/matterbridge-roborock-vacuum-plugin.config.json +1 -1
- package/matterbridge-roborock-vacuum-plugin.schema.json +1 -1
- package/package.json +1 -1
- package/src/platform.ts +1 -5
- package/src/roborockCommunication/RESTAPI/roborockIoTApi.ts +13 -1
- package/src/roborockCommunication/Zmodel/device.ts +1 -0
- package/src/roborockCommunication/Zmodel/mapInfo.ts +9 -6
- package/src/roborockCommunication/Zmodel/userData.ts +0 -3
- package/src/roborockCommunication/broadcast/abstractClient.ts +10 -3
- package/src/roborockCommunication/broadcast/client.ts +1 -1
- package/src/roborockCommunication/broadcast/clientRouter.ts +1 -1
- package/src/roborockCommunication/broadcast/listener/implementation/syncMessageListener.ts +4 -3
- package/src/roborockCommunication/broadcast/messageProcessor.ts +9 -5
- package/src/roborockCommunication/helper/messageDeserializer.ts +7 -1
- package/src/roborockCommunication/helper/messageSerializer.ts +6 -0
- package/src/roborockService.ts +64 -13
- package/src/tests/roborockCommunication/broadcast/listener/implementation/syncMessageListener.test.ts +5 -4
- package/web-for-testing/README.md +47 -0
- package/web-for-testing/nodemon.json +7 -0
- package/web-for-testing/package-lock.json +6598 -0
- package/web-for-testing/package.json +36 -0
- package/web-for-testing/src/accountStore.ts +8 -0
- package/web-for-testing/src/app.ts +194 -0
- package/web-for-testing/tsconfig-ext.json +19 -0
- package/web-for-testing/tsconfig.json +23 -0
- package/web-for-testing/views/index.ejs +172 -0
- package/web-for-testing/watch.mjs +93 -0
package/src/roborockService.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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[][]
|
|
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
|
-
|
|
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.
|