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
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
- await this.roborockService.activateDeviceNotify(robot.device);
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.map((room) => {
10
- return {
11
- id: room.iot_name_id,
12
- name: room.iot_name,
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
- this.logger?.debug('Device status: ', debugStringify(response));
29
- return new DeviceStatus(response);
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 (version !== '1.0' && version !== 'A01') {
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
  }
@@ -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
- return new MapInfo(response[0]);
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
- async activateDeviceNotify(device) {
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.logger.debug('Local client disconnected for device', duid);
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,7 +1,7 @@
1
1
  {
2
2
  "name": "matterbridge-roborock-vacuum-plugin",
3
3
  "type": "DynamicPlatform",
4
- "version": "1.1.0-rc10",
4
+ "version": "1.1.0-rc11",
5
5
  "whiteList": [],
6
6
  "blackList": [],
7
7
  "useInterval": true,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "title": "Matterbridge Roborock Vacuum Plugin",
3
- "description": "matterbridge-roborock-vacuum-plugin v. 1.1.0-rc10 by https://github.com/RinDevJunior",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge-roborock-vacuum-plugin",
3
- "version": "1.1.0-rc10",
3
+ "version": "1.1.0-rc11",
4
4
  "description": "Matterbridge Roborock Vacuum Plugin",
5
5
  "author": "https://github.com/RinDevJunior",
6
6
  "license": "MIT",
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
- await this.roborockService.activateDeviceNotify(robot.device);
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); // can be v3 also
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
  }
@@ -36,6 +36,7 @@ export interface Device {
36
36
  model: string;
37
37
  category: string;
38
38
  batteryLevel: number;
39
+ // schema: DeviceSchema[];
39
40
  };
40
41
 
41
42
  scenes: Scene[];
@@ -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: map.rooms.map((room: RoomInformation) => {
15
- return {
16
- id: room.iot_name_id,
17
- name: room.iot_name,
18
- } as unknown as Room;
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
  }
@@ -7,9 +7,6 @@ export interface UserData {
7
7
  country: string;
8
8
  nickname: string;
9
9
  rriot: Rriot;
10
-
11
- // For caching purposes
12
- baseUrl: string;
13
10
  }
14
11
 
15
12
  export interface Rriot {
@@ -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 {
@@ -15,5 +15,5 @@ export interface Client {
15
15
 
16
16
  send(duid: string, request: RequestMessage): Promise<void>;
17
17
 
18
- get<T>(duid: string, request: RequestMessage): Promise<T>;
18
+ get<T>(duid: string, request: RequestMessage): Promise<T | undefined>;
19
19
  }
@@ -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
- this.logger?.debug('Device status: ', debugStringify(response));
47
- return new DeviceStatus(response);
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 (version !== '1.0' && version !== 'A01') {
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
  }