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/dist/platform.js
CHANGED
|
@@ -140,7 +140,7 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
140
140
|
if (!configurateSuccess.get(duid)) {
|
|
141
141
|
continue;
|
|
142
142
|
}
|
|
143
|
-
|
|
143
|
+
this.roborockService.activateDeviceNotify(robot.device);
|
|
144
144
|
}
|
|
145
145
|
await this.platformRunner?.requestHomeData();
|
|
146
146
|
this.log.info('onConfigurateDevice finished');
|
|
@@ -196,7 +196,4 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
196
196
|
this.log.logLevel = logLevel;
|
|
197
197
|
return Promise.resolve();
|
|
198
198
|
}
|
|
199
|
-
sleep(ms) {
|
|
200
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
201
|
-
}
|
|
202
199
|
}
|
|
@@ -81,4 +81,15 @@ export class RoborockIoTApi {
|
|
|
81
81
|
return undefined;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
async getCustom(url) {
|
|
85
|
+
const result = await this.api.get(url);
|
|
86
|
+
const apiResponse = result.data;
|
|
87
|
+
if (apiResponse.result) {
|
|
88
|
+
return apiResponse.result;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.logger.error('Failed to execute scene');
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
84
95
|
}
|
|
@@ -6,12 +6,14 @@ export class MapInfo {
|
|
|
6
6
|
this.maps.push({
|
|
7
7
|
id: map.mapFlag,
|
|
8
8
|
name: decodeComponent(map.name)?.toLowerCase(),
|
|
9
|
-
rooms: map.rooms.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
rooms: map.rooms && map.rooms.length > 0
|
|
10
|
+
? map.rooms.map((room) => {
|
|
11
|
+
return {
|
|
12
|
+
id: room.iot_name_id,
|
|
13
|
+
name: room.iot_name,
|
|
14
|
+
};
|
|
15
|
+
})
|
|
16
|
+
: [],
|
|
15
17
|
});
|
|
16
18
|
});
|
|
17
19
|
}
|
|
@@ -29,8 +29,15 @@ export class AbstractClient {
|
|
|
29
29
|
}
|
|
30
30
|
async get(duid, request) {
|
|
31
31
|
return new Promise((resolve, reject) => {
|
|
32
|
-
this.syncMessageListener.waitFor(request.messageId, (response) => resolve(response), reject);
|
|
32
|
+
this.syncMessageListener.waitFor(request.messageId, request, (response) => resolve(response), reject);
|
|
33
33
|
this.send(duid, request);
|
|
34
|
+
})
|
|
35
|
+
.then((result) => {
|
|
36
|
+
return result;
|
|
37
|
+
})
|
|
38
|
+
.catch((error) => {
|
|
39
|
+
this.logger.error(error.message);
|
|
40
|
+
return undefined;
|
|
34
41
|
});
|
|
35
42
|
}
|
|
36
43
|
registerDevice(duid, localKey, pv) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { debugStringify } from 'matterbridge/logger';
|
|
1
2
|
import { Protocol } from '../../model/protocol.js';
|
|
2
3
|
export class SyncMessageListener {
|
|
3
4
|
pending = new Map();
|
|
@@ -5,11 +6,11 @@ export class SyncMessageListener {
|
|
|
5
6
|
constructor(logger) {
|
|
6
7
|
this.logger = logger;
|
|
7
8
|
}
|
|
8
|
-
waitFor(messageId, resolve, reject) {
|
|
9
|
+
waitFor(messageId, request, resolve, reject) {
|
|
9
10
|
this.pending.set(messageId, resolve);
|
|
10
11
|
setTimeout(() => {
|
|
11
12
|
this.pending.delete(messageId);
|
|
12
|
-
reject();
|
|
13
|
+
reject(new Error(`Message timeout for messageId: ${messageId}, request: ${debugStringify(request)}`));
|
|
13
14
|
}, 10000);
|
|
14
15
|
}
|
|
15
16
|
async onMessage(message) {
|
|
@@ -25,12 +25,15 @@ export class MessageProcessor {
|
|
|
25
25
|
async getDeviceStatus(duid) {
|
|
26
26
|
const request = new RequestMessage({ method: 'get_status' });
|
|
27
27
|
const response = await this.client.get(duid, request);
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
if (response) {
|
|
29
|
+
this.logger?.debug('Device status: ', debugStringify(response));
|
|
30
|
+
return new DeviceStatus(response);
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
30
33
|
}
|
|
31
34
|
async getRooms(duid, rooms) {
|
|
32
35
|
const request = new RequestMessage({ method: 'get_room_mapping' });
|
|
33
|
-
return this.client.get(duid, request).then((response) => new RoomInfo(rooms, response));
|
|
36
|
+
return this.client.get(duid, request).then((response) => new RoomInfo(rooms, response ?? []));
|
|
34
37
|
}
|
|
35
38
|
async gotoDock(duid) {
|
|
36
39
|
const request = new RequestMessage({ method: 'app_charge' });
|
|
@@ -8,6 +8,7 @@ export class MessageDeserializer {
|
|
|
8
8
|
context;
|
|
9
9
|
messageParser;
|
|
10
10
|
logger;
|
|
11
|
+
supportedVersions = ['1.0', 'A01', 'B01'];
|
|
11
12
|
constructor(context, logger) {
|
|
12
13
|
this.context = context;
|
|
13
14
|
this.logger = logger;
|
|
@@ -28,7 +29,7 @@ export class MessageDeserializer {
|
|
|
28
29
|
}
|
|
29
30
|
deserialize(duid, message) {
|
|
30
31
|
const version = message.toString('latin1', 0, 3);
|
|
31
|
-
if (
|
|
32
|
+
if (!this.supportedVersions.includes(version)) {
|
|
32
33
|
throw new Error('unknown protocol version ' + version);
|
|
33
34
|
}
|
|
34
35
|
const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0;
|
|
@@ -52,6 +53,11 @@ export class MessageDeserializer {
|
|
|
52
53
|
const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
|
|
53
54
|
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
54
55
|
}
|
|
56
|
+
else if (version == 'B01') {
|
|
57
|
+
const iv = CryptoUtils.md5hex(data.random.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25);
|
|
58
|
+
const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
|
|
59
|
+
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
60
|
+
}
|
|
55
61
|
if (data.protocol == Protocol.map_response) {
|
|
56
62
|
return new ResponseMessage(duid, { dps: { id: 0, result: null } });
|
|
57
63
|
}
|
|
@@ -54,6 +54,12 @@ export class MessageSerializer {
|
|
|
54
54
|
const cipher = crypto.createCipheriv('aes-128-cbc', encoder.encode(localKey), iv);
|
|
55
55
|
encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
56
56
|
}
|
|
57
|
+
else if (version == 'B01') {
|
|
58
|
+
const encoder = new TextEncoder();
|
|
59
|
+
const iv = CryptoUtils.md5hex(this.random.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25);
|
|
60
|
+
const cipher = crypto.createCipheriv('aes-128-cbc', encoder.encode(localKey), iv);
|
|
61
|
+
encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
62
|
+
}
|
|
57
63
|
else {
|
|
58
64
|
throw new Error('unable to build the message: unsupported protocol version: ' + version);
|
|
59
65
|
}
|
package/dist/roborockService.js
CHANGED
|
@@ -15,12 +15,14 @@ export default class RoborockService {
|
|
|
15
15
|
messageProcessorMap = new Map();
|
|
16
16
|
ipMap = new Map();
|
|
17
17
|
localClientMap = new Map();
|
|
18
|
+
mqttAlwaysOnDevices = new Map();
|
|
18
19
|
clientManager;
|
|
19
20
|
refreshInterval;
|
|
20
21
|
requestDeviceStatusInterval;
|
|
21
22
|
supportedAreas = new Map();
|
|
22
23
|
supportedRoutines = new Map();
|
|
23
24
|
selectedAreas = new Map();
|
|
25
|
+
vacuumNeedAPIV3 = ['roborock.vacuum.ss07'];
|
|
24
26
|
constructor(authenticateApiSupplier = (logger) => new RoborockAuthenticateApi(logger), iotApiSupplier = (logger, ud) => new RoborockIoTApi(ud, logger), refreshInterval, clientManager, logger) {
|
|
25
27
|
this.logger = logger;
|
|
26
28
|
this.loginApi = authenticateApiSupplier(logger);
|
|
@@ -77,7 +79,8 @@ export default class RoborockService {
|
|
|
77
79
|
this.logger.debug('RoborockService - getMapInformation', duid);
|
|
78
80
|
assert(this.messageClient !== undefined);
|
|
79
81
|
return this.messageClient.get(duid, new RequestMessage({ method: 'get_multi_maps_list' })).then((response) => {
|
|
80
|
-
|
|
82
|
+
this.logger.debug('RoborockService - getMapInformation response', debugStringify(response ?? []));
|
|
83
|
+
return response ? new MapInfo(response[0]) : undefined;
|
|
81
84
|
});
|
|
82
85
|
}
|
|
83
86
|
async changeCleanMode(duid, { suctionPower, waterFlow, distance_off, mopRoute }) {
|
|
@@ -147,6 +150,17 @@ export default class RoborockService {
|
|
|
147
150
|
async customSend(duid, request) {
|
|
148
151
|
return this.getMessageProcessor(duid)?.sendCustomMessage(duid, request);
|
|
149
152
|
}
|
|
153
|
+
async getCustomAPI(url) {
|
|
154
|
+
this.logger.debug('RoborockService - getCustomAPI', url);
|
|
155
|
+
assert(this.iotApi !== undefined);
|
|
156
|
+
try {
|
|
157
|
+
return await this.iotApi.getCustom(url);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
this.logger.error(`Failed to get custom API with url ${url}: ${error ? debugStringify(error) : 'undefined'}`);
|
|
161
|
+
return { result: undefined, error: `Failed to get custom API with url ${url}` };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
150
164
|
stopService() {
|
|
151
165
|
if (this.messageClient) {
|
|
152
166
|
this.messageClient.disconnect();
|
|
@@ -175,14 +189,14 @@ export default class RoborockService {
|
|
|
175
189
|
setDeviceNotify(callback) {
|
|
176
190
|
this.deviceNotify = callback;
|
|
177
191
|
}
|
|
178
|
-
|
|
192
|
+
activateDeviceNotify(device) {
|
|
179
193
|
const self = this;
|
|
180
194
|
this.logger.debug('Requesting device info for device', device.duid);
|
|
181
195
|
const messageProcessor = this.getMessageProcessor(device.duid);
|
|
182
196
|
this.requestDeviceStatusInterval = setInterval(async () => {
|
|
183
197
|
if (messageProcessor) {
|
|
184
198
|
await messageProcessor.getDeviceStatus(device.duid).then((response) => {
|
|
185
|
-
if (self.deviceNotify) {
|
|
199
|
+
if (self.deviceNotify && response) {
|
|
186
200
|
const message = { duid: device.duid, ...response.errorStatus, ...response.message };
|
|
187
201
|
self.logger.debug('Device status update', debugStringify(message));
|
|
188
202
|
self.deviceNotify(NotifyMessageTypes.LocalMessage, message);
|
|
@@ -208,6 +222,15 @@ export default class RoborockService {
|
|
|
208
222
|
const scenes = (await this.iotApi.getScenes(homeDetails.rrHomeId)) ?? [];
|
|
209
223
|
const products = new Map();
|
|
210
224
|
homeData.products.forEach((p) => products.set(p.id, p.model));
|
|
225
|
+
if (homeData.products.some((p) => this.vacuumNeedAPIV3.includes(p.model))) {
|
|
226
|
+
this.logger.debug('Using v3 API for home data retrieval');
|
|
227
|
+
const homeDataV3 = await this.iotApi.getHomev3(homeDetails.rrHomeId);
|
|
228
|
+
if (!homeDataV3) {
|
|
229
|
+
throw new Error('Failed to retrieve the home data from v3 API');
|
|
230
|
+
}
|
|
231
|
+
homeData.devices = [...homeData.devices, ...homeDataV3.devices.filter((d) => !homeData.devices.some((x) => x.duid === d.duid))];
|
|
232
|
+
homeData.receivedDevices = [...homeData.receivedDevices, ...homeDataV3.receivedDevices.filter((d) => !homeData.receivedDevices.some((x) => x.duid === d.duid))];
|
|
233
|
+
}
|
|
211
234
|
if (homeData.rooms.length === 0) {
|
|
212
235
|
const homeDataV2 = await this.iotApi.getHomev2(homeDetails.rrHomeId);
|
|
213
236
|
if (homeDataV2 && homeDataV2.rooms && homeDataV2.rooms.length > 0) {
|
|
@@ -237,6 +260,7 @@ export default class RoborockService {
|
|
|
237
260
|
model: homeData.products.find((p) => p.id === device.productId)?.model,
|
|
238
261
|
category: homeData.products.find((p) => p.id === device.productId)?.category,
|
|
239
262
|
batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100,
|
|
263
|
+
schema: homeData.products.find((p) => p.id === device.productId)?.schema,
|
|
240
264
|
},
|
|
241
265
|
store: {
|
|
242
266
|
username: username,
|
|
@@ -259,6 +283,18 @@ export default class RoborockService {
|
|
|
259
283
|
const products = new Map();
|
|
260
284
|
homeData.products.forEach((p) => products.set(p.id, p.model));
|
|
261
285
|
const devices = homeData.devices.length > 0 ? homeData.devices : homeData.receivedDevices;
|
|
286
|
+
if (homeData.rooms.length === 0) {
|
|
287
|
+
const homeDataV3 = await this.iotApi.getHomev3(homeid);
|
|
288
|
+
if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
|
|
289
|
+
homeData.rooms = homeDataV3.rooms;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const homeDataV1 = await this.iotApi.getHome(homeid);
|
|
293
|
+
if (homeDataV1 && homeDataV1.rooms && homeDataV1.rooms.length > 0) {
|
|
294
|
+
homeData.rooms = homeDataV1.rooms;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
262
298
|
const dvs = devices.map((device) => {
|
|
263
299
|
return {
|
|
264
300
|
...device,
|
|
@@ -272,6 +308,7 @@ export default class RoborockService {
|
|
|
272
308
|
model: homeData.products.find((p) => p.id === device.productId)?.model,
|
|
273
309
|
category: homeData.products.find((p) => p.id === device.productId)?.category,
|
|
274
310
|
batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100,
|
|
311
|
+
schema: homeData.products.find((p) => p.id === device.productId)?.schema,
|
|
275
312
|
},
|
|
276
313
|
store: {
|
|
277
314
|
userData: this.userdata,
|
|
@@ -297,9 +334,9 @@ export default class RoborockService {
|
|
|
297
334
|
getRoomMappings(duid) {
|
|
298
335
|
if (!this.messageClient) {
|
|
299
336
|
this.logger.warn('messageClient not initialized. Waititing for next execution');
|
|
300
|
-
return undefined;
|
|
337
|
+
return Promise.resolve(undefined);
|
|
301
338
|
}
|
|
302
|
-
return this.messageClient.get(duid, new RequestMessage({ method: 'get_room_mapping' }));
|
|
339
|
+
return this.messageClient.get(duid, new RequestMessage({ method: 'get_room_mapping', secure: this.isRequestSecure(duid) }));
|
|
303
340
|
}
|
|
304
341
|
async initializeMessageClient(username, device, userdata) {
|
|
305
342
|
if (this.clientManager === undefined) {
|
|
@@ -351,6 +388,15 @@ export default class RoborockService {
|
|
|
351
388
|
},
|
|
352
389
|
});
|
|
353
390
|
this.messageProcessorMap.set(device.duid, messageProcessor);
|
|
391
|
+
this.logger.debug('Checking if device supports local connection', device.pv, device.data.model, device.duid);
|
|
392
|
+
if (device.pv === 'B01') {
|
|
393
|
+
this.logger.warn('Device does not support local connection', device.duid);
|
|
394
|
+
this.mqttAlwaysOnDevices.set(device.duid, true);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
this.mqttAlwaysOnDevices.set(device.duid, false);
|
|
399
|
+
}
|
|
354
400
|
this.logger.debug('Local device', device.duid);
|
|
355
401
|
let localIp = this.ipMap.get(device.duid);
|
|
356
402
|
try {
|
|
@@ -389,7 +435,7 @@ export default class RoborockService {
|
|
|
389
435
|
return true;
|
|
390
436
|
}
|
|
391
437
|
onLocalClientDisconnect(duid) {
|
|
392
|
-
this.
|
|
438
|
+
this.mqttAlwaysOnDevices.set(duid, true);
|
|
393
439
|
}
|
|
394
440
|
sleep(ms) {
|
|
395
441
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -399,4 +445,7 @@ export default class RoborockService {
|
|
|
399
445
|
this.iotApi = this.iotApiFactory(this.logger, userdata);
|
|
400
446
|
return userdata;
|
|
401
447
|
}
|
|
448
|
+
isRequestSecure(duid) {
|
|
449
|
+
return this.mqttAlwaysOnDevices.get(duid) ?? false;
|
|
450
|
+
}
|
|
402
451
|
}
|
package/eslint.config.js
CHANGED
|
@@ -9,7 +9,7 @@ import eslintPluginN from 'eslint-plugin-n';
|
|
|
9
9
|
export default [
|
|
10
10
|
{
|
|
11
11
|
name: 'global ignores',
|
|
12
|
-
ignores: ['dist/', 'build/', 'node_modules/', 'coverage/', 'frontend/', 'rock-s0/', 'webui/', 'exampleData/', '.shouldnotcommit/'],
|
|
12
|
+
ignores: ['dist/', 'build/', 'node_modules/', 'coverage/', 'frontend/', 'rock-s0/', 'webui/', 'exampleData/', '.shouldnotcommit/', 'web-for-testing/'],
|
|
13
13
|
},
|
|
14
14
|
eslint.configs.recommended,
|
|
15
15
|
...tseslint.configs.strict,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "Matterbridge Roborock Vacuum Plugin",
|
|
3
|
-
"description": "matterbridge-roborock-vacuum-plugin v. 1.1.0-
|
|
3
|
+
"description": "matterbridge-roborock-vacuum-plugin v. 1.1.0-rc11 by https://github.com/RinDevJunior",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"required": ["username", "password"],
|
|
6
6
|
"properties": {
|
package/package.json
CHANGED
package/src/platform.ts
CHANGED
|
@@ -199,7 +199,7 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
199
199
|
if (!configurateSuccess.get(duid)) {
|
|
200
200
|
continue;
|
|
201
201
|
}
|
|
202
|
-
|
|
202
|
+
this.roborockService.activateDeviceNotify(robot.device);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
await this.platformRunner?.requestHomeData();
|
|
@@ -279,8 +279,4 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
279
279
|
this.log.logLevel = logLevel;
|
|
280
280
|
return Promise.resolve();
|
|
281
281
|
}
|
|
282
|
-
|
|
283
|
-
private sleep(ms: number): Promise<void> {
|
|
284
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
285
|
-
}
|
|
286
282
|
}
|
|
@@ -46,7 +46,7 @@ export class RoborockIoTApi {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
public async getHomev2(homeId: number): Promise<Home | undefined> {
|
|
49
|
-
const result = await this.api.get('v2/user/homes/' + homeId);
|
|
49
|
+
const result = await this.api.get('v2/user/homes/' + homeId);
|
|
50
50
|
|
|
51
51
|
const apiResponse: ApiResponse<Home> = result.data;
|
|
52
52
|
if (apiResponse.result) {
|
|
@@ -92,4 +92,16 @@ export class RoborockIoTApi {
|
|
|
92
92
|
return undefined;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
+
|
|
96
|
+
public async getCustom(url: string): Promise<unknown> {
|
|
97
|
+
const result = await this.api.get(url);
|
|
98
|
+
const apiResponse: ApiResponse<unknown> = result.data;
|
|
99
|
+
|
|
100
|
+
if (apiResponse.result) {
|
|
101
|
+
return apiResponse.result;
|
|
102
|
+
} else {
|
|
103
|
+
this.logger.error('Failed to execute scene');
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
95
107
|
}
|
|
@@ -11,12 +11,15 @@ export class MapInfo {
|
|
|
11
11
|
this.maps.push({
|
|
12
12
|
id: map.mapFlag,
|
|
13
13
|
name: decodeComponent(map.name)?.toLowerCase(),
|
|
14
|
-
rooms:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
rooms:
|
|
15
|
+
map.rooms && map.rooms.length > 0
|
|
16
|
+
? map.rooms.map((room: RoomInformation) => {
|
|
17
|
+
return {
|
|
18
|
+
id: room.iot_name_id,
|
|
19
|
+
name: room.iot_name,
|
|
20
|
+
} as unknown as Room;
|
|
21
|
+
})
|
|
22
|
+
: [],
|
|
20
23
|
});
|
|
21
24
|
});
|
|
22
25
|
}
|
|
@@ -49,11 +49,18 @@ export abstract class AbstractClient implements Client {
|
|
|
49
49
|
abstract disconnect(): Promise<void>;
|
|
50
50
|
abstract send(duid: string, request: RequestMessage): Promise<void>;
|
|
51
51
|
|
|
52
|
-
public async get<T>(duid: string, request: RequestMessage): Promise<T> {
|
|
52
|
+
public async get<T>(duid: string, request: RequestMessage): Promise<T | undefined> {
|
|
53
53
|
return new Promise<T>((resolve, reject) => {
|
|
54
|
-
this.syncMessageListener.waitFor(request.messageId, (response: ResponseMessage) => resolve(response as unknown as T), reject);
|
|
54
|
+
this.syncMessageListener.waitFor(request.messageId, request, (response: ResponseMessage) => resolve(response as unknown as T), reject);
|
|
55
55
|
this.send(duid, request);
|
|
56
|
-
})
|
|
56
|
+
})
|
|
57
|
+
.then((result: T) => {
|
|
58
|
+
return result;
|
|
59
|
+
})
|
|
60
|
+
.catch((error: Error) => {
|
|
61
|
+
this.logger.error(error.message);
|
|
62
|
+
return undefined;
|
|
63
|
+
});
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
public registerDevice(duid: string, localKey: string, pv: string): void {
|
|
@@ -81,7 +81,7 @@ export class ClientRouter implements Client {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
async get<T>(duid: string, request: RequestMessage): Promise<T> {
|
|
84
|
+
async get<T>(duid: string, request: RequestMessage): Promise<T | undefined> {
|
|
85
85
|
if (request.secure) {
|
|
86
86
|
return await this.mqttClient.get(duid, request);
|
|
87
87
|
} else {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { AnsiLogger } from 'matterbridge/logger';
|
|
1
|
+
import { AnsiLogger, debugStringify } from 'matterbridge/logger';
|
|
2
2
|
import { DpsPayload } from '../../model/dps.js';
|
|
3
3
|
import { Protocol } from '../../model/protocol.js';
|
|
4
4
|
import { ResponseMessage } from '../../model/responseMessage.js';
|
|
5
5
|
import { AbstractMessageListener } from '../index.js';
|
|
6
|
+
import { RequestMessage } from '../../model/requestMessage.js';
|
|
6
7
|
|
|
7
8
|
export class SyncMessageListener implements AbstractMessageListener {
|
|
8
9
|
private readonly pending = new Map<number, (response: ResponseMessage) => void>();
|
|
@@ -12,12 +13,12 @@ export class SyncMessageListener implements AbstractMessageListener {
|
|
|
12
13
|
this.logger = logger;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
public waitFor(messageId: number, resolve: (response: ResponseMessage) => void, reject: () => void): void {
|
|
16
|
+
public waitFor(messageId: number, request: RequestMessage, resolve: (response: ResponseMessage) => void, reject: (error?: Error) => void): void {
|
|
16
17
|
this.pending.set(messageId, resolve);
|
|
17
18
|
|
|
18
19
|
setTimeout(() => {
|
|
19
20
|
this.pending.delete(messageId);
|
|
20
|
-
reject();
|
|
21
|
+
reject(new Error(`Message timeout for messageId: ${messageId}, request: ${debugStringify(request)}`));
|
|
21
22
|
}, 10000);
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -29,7 +29,7 @@ export class MessageProcessor {
|
|
|
29
29
|
this.messageListener.registerListener(listener);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
public async getNetworkInfo(duid: string): Promise<NetworkInfo> {
|
|
32
|
+
public async getNetworkInfo(duid: string): Promise<NetworkInfo | undefined> {
|
|
33
33
|
const request = new RequestMessage({ method: 'get_network_info' });
|
|
34
34
|
return await this.client.get(duid, request);
|
|
35
35
|
}
|
|
@@ -39,17 +39,21 @@ export class MessageProcessor {
|
|
|
39
39
|
// return this.client.get<CloudMessageResult[]>(duid, request).then((response) => new DeviceStatus(response[0]));
|
|
40
40
|
// }
|
|
41
41
|
|
|
42
|
-
public async getDeviceStatus(duid: string): Promise<DeviceStatus> {
|
|
42
|
+
public async getDeviceStatus(duid: string): Promise<DeviceStatus | undefined> {
|
|
43
43
|
const request = new RequestMessage({ method: 'get_status' });
|
|
44
44
|
const response = await this.client.get<CloudMessageResult[]>(duid, request);
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
if (response) {
|
|
47
|
+
this.logger?.debug('Device status: ', debugStringify(response));
|
|
48
|
+
return new DeviceStatus(response);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return undefined;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
public async getRooms(duid: string, rooms: Room[]): Promise<RoomInfo> {
|
|
51
55
|
const request = new RequestMessage({ method: 'get_room_mapping' });
|
|
52
|
-
return this.client.get<number[][]>(duid, request).then((response) => new RoomInfo(rooms, response));
|
|
56
|
+
return this.client.get<number[][] | undefined>(duid, request).then((response) => new RoomInfo(rooms, response ?? []));
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
public async gotoDock(duid: string): Promise<void> {
|
|
@@ -23,6 +23,7 @@ export class MessageDeserializer {
|
|
|
23
23
|
private readonly context: MessageContext;
|
|
24
24
|
private readonly messageParser: Parser;
|
|
25
25
|
private readonly logger: AnsiLogger;
|
|
26
|
+
private readonly supportedVersions: string[] = ['1.0', 'A01', 'B01'];
|
|
26
27
|
|
|
27
28
|
constructor(context: MessageContext, logger: AnsiLogger) {
|
|
28
29
|
this.context = context;
|
|
@@ -46,7 +47,7 @@ export class MessageDeserializer {
|
|
|
46
47
|
|
|
47
48
|
public deserialize(duid: string, message: Buffer<ArrayBufferLike>): ResponseMessage {
|
|
48
49
|
const version = message.toString('latin1', 0, 3);
|
|
49
|
-
if (
|
|
50
|
+
if (!this.supportedVersions.includes(version)) {
|
|
50
51
|
throw new Error('unknown protocol version ' + version);
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -71,6 +72,11 @@ export class MessageDeserializer {
|
|
|
71
72
|
const iv = CryptoUtils.md5hex(data.random.toString(16).padStart(8, '0') + '726f626f726f636b2d67a6d6da').substring(8, 24);
|
|
72
73
|
const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
|
|
73
74
|
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
75
|
+
} else if (version == 'B01') {
|
|
76
|
+
const iv = CryptoUtils.md5hex(data.random.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25);
|
|
77
|
+
const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
|
|
78
|
+
// unpad ??
|
|
79
|
+
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
// map visualization not support
|
|
@@ -66,6 +66,12 @@ export class MessageSerializer {
|
|
|
66
66
|
const iv = CryptoUtils.md5hex(this.random.toString(16).padStart(8, '0') + '726f626f726f636b2d67a6d6da').substring(8, 24);
|
|
67
67
|
const cipher = crypto.createCipheriv('aes-128-cbc', encoder.encode(localKey), iv);
|
|
68
68
|
encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
69
|
+
} else if (version == 'B01') {
|
|
70
|
+
const encoder = new TextEncoder();
|
|
71
|
+
const iv = CryptoUtils.md5hex(this.random.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25);
|
|
72
|
+
const cipher = crypto.createCipheriv('aes-128-cbc', encoder.encode(localKey), iv);
|
|
73
|
+
// pad ??
|
|
74
|
+
encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
69
75
|
} else {
|
|
70
76
|
throw new Error('unable to build the message: unsupported protocol version: ' + version);
|
|
71
77
|
}
|