matterbridge-roborock-vacuum-plugin 1.1.0-rc09 → 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 (51) hide show
  1. package/dist/platform.js +6 -4
  2. package/dist/roborockCommunication/RESTAPI/roborockIoTApi.js +22 -0
  3. package/dist/roborockCommunication/Zmodel/map.js +1 -0
  4. package/dist/roborockCommunication/Zmodel/mapInfo.js +26 -0
  5. package/dist/roborockCommunication/Zmodel/multipleMap.js +1 -0
  6. package/dist/roborockCommunication/broadcast/abstractClient.js +10 -2
  7. package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +22 -2
  8. package/dist/roborockCommunication/broadcast/client/MQTTClient.js +4 -0
  9. package/dist/roborockCommunication/broadcast/clientRouter.js +2 -2
  10. package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js +9 -2
  11. package/dist/roborockCommunication/broadcast/listener/implementation/syncMessageListener.js +3 -2
  12. package/dist/roborockCommunication/broadcast/messageProcessor.js +6 -3
  13. package/dist/roborockCommunication/helper/messageDeserializer.js +9 -3
  14. package/dist/roborockCommunication/helper/messageSerializer.js +6 -0
  15. package/dist/roborockCommunication/index.js +1 -0
  16. package/dist/roborockService.js +84 -13
  17. package/eslint.config.js +1 -1
  18. package/matterbridge-roborock-vacuum-plugin.config.json +1 -1
  19. package/matterbridge-roborock-vacuum-plugin.schema.json +1 -1
  20. package/package.json +1 -1
  21. package/src/platform.ts +7 -5
  22. package/src/roborockCommunication/RESTAPI/roborockIoTApi.ts +25 -1
  23. package/src/roborockCommunication/Zmodel/device.ts +1 -0
  24. package/src/roborockCommunication/Zmodel/map.ts +14 -0
  25. package/src/roborockCommunication/Zmodel/mapInfo.ts +34 -0
  26. package/src/roborockCommunication/Zmodel/multipleMap.ts +8 -0
  27. package/src/roborockCommunication/Zmodel/userData.ts +0 -3
  28. package/src/roborockCommunication/broadcast/abstractClient.ts +13 -4
  29. package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +26 -2
  30. package/src/roborockCommunication/broadcast/client/MQTTClient.ts +4 -0
  31. package/src/roborockCommunication/broadcast/client.ts +1 -1
  32. package/src/roborockCommunication/broadcast/clientRouter.ts +3 -3
  33. package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts +10 -2
  34. package/src/roborockCommunication/broadcast/listener/implementation/syncMessageListener.ts +4 -3
  35. package/src/roborockCommunication/broadcast/messageProcessor.ts +9 -5
  36. package/src/roborockCommunication/helper/messageDeserializer.ts +9 -3
  37. package/src/roborockCommunication/helper/messageSerializer.ts +6 -0
  38. package/src/roborockCommunication/index.ts +2 -0
  39. package/src/roborockService.ts +96 -17
  40. package/src/tests/roborockCommunication/broadcast/listener/implementation/syncMessageListener.test.ts +5 -4
  41. package/src/tests/roborockService.test.ts +3 -11
  42. package/web-for-testing/README.md +47 -0
  43. package/web-for-testing/nodemon.json +7 -0
  44. package/web-for-testing/package-lock.json +6598 -0
  45. package/web-for-testing/package.json +36 -0
  46. package/web-for-testing/src/accountStore.ts +8 -0
  47. package/web-for-testing/src/app.ts +194 -0
  48. package/web-for-testing/tsconfig-ext.json +19 -0
  49. package/web-for-testing/tsconfig.json +23 -0
  50. package/web-for-testing/views/index.ejs +172 -0
  51. 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');
@@ -156,6 +156,11 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
156
156
  this.log.error(`Failed to connect to local network for device: ${vacuum.name} (${vacuum.duid})`);
157
157
  return false;
158
158
  }
159
+ if (vacuum.rooms === undefined || vacuum.rooms.length === 0) {
160
+ const map_info = await this.roborockService.getMapInformation(vacuum.duid);
161
+ const rooms = map_info?.maps?.[0]?.rooms ?? [];
162
+ vacuum.rooms = rooms;
163
+ }
159
164
  const roomMap = await this.platformRunner.getRoomMapFromDevice(vacuum);
160
165
  this.log.debug('Initializing - roomMap: ', debugStringify(roomMap));
161
166
  const behaviorHandler = configurateBehavior(vacuum.data.model, vacuum.duid, this.roborockService, this.cleanModeSettings, this.enableExperimentalFeature?.advancedFeature?.forceRunAtDefault ?? false, this.log);
@@ -191,7 +196,4 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
191
196
  this.log.logLevel = logLevel;
192
197
  return Promise.resolve();
193
198
  }
194
- sleep(ms) {
195
- return new Promise((resolve) => setTimeout(resolve, ms));
196
- }
197
199
  }
@@ -48,6 +48,17 @@ export class RoborockIoTApi {
48
48
  return undefined;
49
49
  }
50
50
  }
51
+ async getHomev3(homeId) {
52
+ const result = await this.api.get('v3/user/homes/' + homeId);
53
+ const apiResponse = result.data;
54
+ if (apiResponse.result) {
55
+ return apiResponse.result;
56
+ }
57
+ else {
58
+ this.logger.error('Failed to retrieve the home data');
59
+ return undefined;
60
+ }
61
+ }
51
62
  async getScenes(homeId) {
52
63
  const result = await this.api.get('user/scene/home/' + homeId);
53
64
  const apiResponse = result.data;
@@ -70,4 +81,15 @@ export class RoborockIoTApi {
70
81
  return undefined;
71
82
  }
72
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
+ }
73
95
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import decodeComponent from '../helper/nameDecoder.js';
2
+ export class MapInfo {
3
+ maps = [];
4
+ constructor(multimap) {
5
+ multimap.map_info.forEach((map) => {
6
+ this.maps.push({
7
+ id: map.mapFlag,
8
+ name: decodeComponent(map.name)?.toLowerCase(),
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
+ : [],
17
+ });
18
+ });
19
+ }
20
+ getById(id) {
21
+ return this.maps.find((m) => m.id === id)?.name;
22
+ }
23
+ getByName(name) {
24
+ return this.maps.find((m) => m.name === name.toLowerCase())?.id;
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -6,6 +6,7 @@ import { SyncMessageListener } from './listener/implementation/syncMessageListen
6
6
  import { ConnectionStateListener } from './listener/implementation/connectionStateListener.js';
7
7
  export class AbstractClient {
8
8
  isInDisconnectingStep = false;
9
+ retryCount = 0;
9
10
  connectionListeners = new ChainedConnectionListener();
10
11
  messageListeners = new ChainedMessageListener();
11
12
  serializer;
@@ -23,13 +24,20 @@ export class AbstractClient {
23
24
  this.logger = logger;
24
25
  }
25
26
  initializeConnectionStateListener() {
26
- const connectionStateListener = new ConnectionStateListener(this.logger, this, this.clientName, this.shouldReconnect);
27
+ const connectionStateListener = new ConnectionStateListener(this.logger, this, this.clientName, this.changeToSecureConnection, this.shouldReconnect);
27
28
  this.connectionListeners.register(connectionStateListener);
28
29
  }
29
30
  async get(duid, request) {
30
31
  return new Promise((resolve, reject) => {
31
- this.syncMessageListener.waitFor(request.messageId, (response) => resolve(response), reject);
32
+ this.syncMessageListener.waitFor(request.messageId, request, (response) => resolve(response), reject);
32
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;
33
41
  });
34
42
  }
35
43
  registerDevice(duid, localKey, pv) {
@@ -7,6 +7,7 @@ import { AbstractClient } from '../abstractClient.js';
7
7
  import { Sequence } from '../../helper/sequence.js';
8
8
  import { ChunkBuffer } from '../../helper/chunkBuffer.js';
9
9
  export class LocalNetworkClient extends AbstractClient {
10
+ changeToSecureConnection;
10
11
  clientName = 'LocalNetworkClient';
11
12
  shouldReconnect = true;
12
13
  socket = undefined;
@@ -15,17 +16,23 @@ export class LocalNetworkClient extends AbstractClient {
15
16
  pingInterval;
16
17
  duid;
17
18
  ip;
18
- constructor(logger, context, duid, ip) {
19
+ constructor(logger, context, duid, ip, inject) {
19
20
  super(logger, context);
20
21
  this.duid = duid;
21
22
  this.ip = ip;
22
23
  this.messageIdSeq = new Sequence(100000, 999999);
23
24
  this.initializeConnectionStateListener();
25
+ this.changeToSecureConnection = inject;
24
26
  }
25
27
  connect() {
28
+ if (this.socket) {
29
+ this.socket.destroy();
30
+ this.socket = undefined;
31
+ return;
32
+ }
26
33
  this.socket = new Socket();
27
34
  this.socket.on('close', this.onDisconnect.bind(this));
28
- this.socket.on('end', this.onDisconnect.bind(this));
35
+ this.socket.on('end', this.onEnd.bind(this));
29
36
  this.socket.on('error', this.onError.bind(this));
30
37
  this.socket.on('data', this.onMessage.bind(this));
31
38
  this.socket.connect(58867, this.ip, this.onConnect.bind(this));
@@ -58,6 +65,19 @@ export class LocalNetworkClient extends AbstractClient {
58
65
  await this.sendHelloMessage();
59
66
  this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
60
67
  await this.connectionListeners.onConnected(this.duid);
68
+ this.retryCount = 0;
69
+ }
70
+ async onEnd() {
71
+ this.logger.notice('LocalNetworkClient: Socket has ended.');
72
+ this.connected = false;
73
+ if (this.socket) {
74
+ this.socket.destroy();
75
+ this.socket = undefined;
76
+ }
77
+ if (this.pingInterval) {
78
+ clearInterval(this.pingInterval);
79
+ }
80
+ await this.connectionListeners.onDisconnected(this.duid);
61
81
  }
62
82
  async onDisconnect() {
63
83
  this.logger.notice('LocalNetworkClient: Socket has disconnected.');
@@ -3,6 +3,7 @@ import * as CryptoUtils from '../../helper/cryptoHelper.js';
3
3
  import { AbstractClient } from '../abstractClient.js';
4
4
  import { debugStringify } from 'matterbridge/logger';
5
5
  export class MQTTClient extends AbstractClient {
6
+ changeToSecureConnection;
6
7
  clientName = 'MQTTClient';
7
8
  shouldReconnect = false;
8
9
  rriot;
@@ -15,6 +16,9 @@ export class MQTTClient extends AbstractClient {
15
16
  this.mqttUsername = CryptoUtils.md5hex(userdata.rriot.u + ':' + userdata.rriot.k).substring(2, 10);
16
17
  this.mqttPassword = CryptoUtils.md5hex(userdata.rriot.s + ':' + userdata.rriot.k).substring(16);
17
18
  this.initializeConnectionStateListener();
19
+ this.changeToSecureConnection = (duid) => {
20
+ this.logger.info(`MqttClient for ${duid} has been disconnected`);
21
+ };
18
22
  }
19
23
  connect() {
20
24
  if (this.client) {
@@ -20,8 +20,8 @@ export class ClientRouter {
20
20
  registerDevice(duid, localKey, pv) {
21
21
  this.context.registerDevice(duid, localKey, pv);
22
22
  }
23
- registerClient(duid, ip) {
24
- const localClient = new LocalNetworkClient(this.logger, this.context, duid, ip);
23
+ registerClient(duid, ip, onDisconnect) {
24
+ const localClient = new LocalNetworkClient(this.logger, this.context, duid, ip, onDisconnect);
25
25
  localClient.registerConnectionListener(this.connectionListeners);
26
26
  localClient.registerMessageListener(this.messageListeners);
27
27
  this.localClients.set(duid, localClient);
@@ -3,11 +3,13 @@ export class ConnectionStateListener {
3
3
  client;
4
4
  clientName;
5
5
  shouldReconnect;
6
- constructor(logger, client, clientName, shouldReconnect = false) {
6
+ changeToSecureConnection;
7
+ constructor(logger, client, clientName, changeToSecureConnection, shouldReconnect = false) {
7
8
  this.logger = logger;
8
9
  this.client = client;
9
10
  this.clientName = clientName;
10
11
  this.shouldReconnect = shouldReconnect;
12
+ this.changeToSecureConnection = changeToSecureConnection;
11
13
  }
12
14
  async onConnected(duid) {
13
15
  this.logger.notice(`Device ${duid} connected to ${this.clientName}`);
@@ -17,7 +19,12 @@ export class ConnectionStateListener {
17
19
  this.logger.notice(`Device ${duid} disconnected from ${this.clientName}, but re-registration is disabled.`);
18
20
  return;
19
21
  }
20
- this.logger.notice(`Device ${duid} disconnected from ${this.clientName}`);
22
+ if (this.client.retryCount > 10) {
23
+ this.logger.error(`Device with DUID ${duid} has exceeded retry limit, not re-registering.`);
24
+ this.changeToSecureConnection(duid);
25
+ return;
26
+ }
27
+ this.client.retryCount++;
21
28
  const isInDisconnectingStep = this.client.isInDisconnectingStep;
22
29
  if (isInDisconnectingStep) {
23
30
  this.logger.info(`Device with DUID ${duid} is in disconnecting step, skipping re-registration.`);
@@ -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,18 +53,23 @@ 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
  }
58
64
  if (data.protocol == Protocol.rpc_response || data.protocol == Protocol.general_request) {
59
- return this.deserializeProtocolRpcResponse(duid, data);
65
+ return this.deserializeRpcResponse(duid, data);
60
66
  }
61
67
  else {
62
68
  this.logger.error('unknown protocol: ' + data.protocol);
63
69
  return new ResponseMessage(duid, { dps: { id: 0, result: null } });
64
70
  }
65
71
  }
66
- deserializeProtocolRpcResponse(duid, data) {
72
+ deserializeRpcResponse(duid, data) {
67
73
  const payload = JSON.parse(data.payload.toString());
68
74
  const dps = payload.dps;
69
75
  this.parseJsonInDps(dps, Protocol.general_request);
@@ -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
  }
@@ -7,4 +7,5 @@ export { Protocol } from './broadcast/model/protocol.js';
7
7
  export { ClientRouter } from './broadcast/clientRouter.js';
8
8
  export { DeviceStatus } from './Zmodel/deviceStatus.js';
9
9
  export { ResponseMessage } from './broadcast/model/responseMessage.js';
10
+ export { MapInfo } from './Zmodel/mapInfo.js';
10
11
  export { Scene } from './Zmodel/scene.js';
@@ -2,7 +2,7 @@ import assert from 'node:assert';
2
2
  import { debugStringify } from 'matterbridge/logger';
3
3
  import { NotifyMessageTypes } from './notifyMessageTypes.js';
4
4
  import { clearInterval } from 'node:timers';
5
- import { RoborockAuthenticateApi, RoborockIoTApi, MessageProcessor, Protocol, RequestMessage, ResponseMessage, } from './roborockCommunication/index.js';
5
+ import { RoborockAuthenticateApi, RoborockIoTApi, MessageProcessor, Protocol, RequestMessage, ResponseMessage, MapInfo, } from './roborockCommunication/index.js';
6
6
  export default class RoborockService {
7
7
  loginApi;
8
8
  logger;
@@ -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);
@@ -69,6 +71,18 @@ export default class RoborockService {
69
71
  }
70
72
  return data;
71
73
  }
74
+ async getRoomIdFromMap(duid) {
75
+ const data = (await this.customGet(duid, new RequestMessage({ method: 'get_map_v1' })));
76
+ return data?.vacuumRoom;
77
+ }
78
+ async getMapInformation(duid) {
79
+ this.logger.debug('RoborockService - getMapInformation', duid);
80
+ assert(this.messageClient !== undefined);
81
+ return this.messageClient.get(duid, new RequestMessage({ method: 'get_multi_maps_list' })).then((response) => {
82
+ this.logger.debug('RoborockService - getMapInformation response', debugStringify(response ?? []));
83
+ return response ? new MapInfo(response[0]) : undefined;
84
+ });
85
+ }
72
86
  async changeCleanMode(duid, { suctionPower, waterFlow, distance_off, mopRoute }) {
73
87
  this.logger.notice('RoborockService - changeCleanMode');
74
88
  return this.getMessageProcessor(duid)?.changeCleanMode(duid, suctionPower, waterFlow, mopRoute, distance_off);
@@ -129,17 +143,24 @@ export default class RoborockService {
129
143
  this.logger.debug('RoborockService - findMe');
130
144
  await this.getMessageProcessor(duid)?.findMyRobot(duid);
131
145
  }
132
- async customGet(duid, method) {
133
- this.logger.debug('RoborockService - customSend-message', method);
134
- return this.getMessageProcessor(duid)?.getCustomMessage(duid, new RequestMessage({ method }));
135
- }
136
- async customGetInSecure(duid, method) {
137
- this.logger.debug('RoborockService - customGetInSecure-message', method);
138
- return this.getMessageProcessor(duid)?.getCustomMessage(duid, new RequestMessage({ method, secure: true }));
146
+ async customGet(duid, request) {
147
+ this.logger.debug('RoborockService - customSend-message', request.method, request.params, request.secure);
148
+ return this.getMessageProcessor(duid)?.getCustomMessage(duid, request);
139
149
  }
140
150
  async customSend(duid, request) {
141
151
  return this.getMessageProcessor(duid)?.sendCustomMessage(duid, request);
142
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
+ }
143
164
  stopService() {
144
165
  if (this.messageClient) {
145
166
  this.messageClient.disconnect();
@@ -168,14 +189,14 @@ export default class RoborockService {
168
189
  setDeviceNotify(callback) {
169
190
  this.deviceNotify = callback;
170
191
  }
171
- async activateDeviceNotify(device) {
192
+ activateDeviceNotify(device) {
172
193
  const self = this;
173
194
  this.logger.debug('Requesting device info for device', device.duid);
174
195
  const messageProcessor = this.getMessageProcessor(device.duid);
175
196
  this.requestDeviceStatusInterval = setInterval(async () => {
176
197
  if (messageProcessor) {
177
198
  await messageProcessor.getDeviceStatus(device.duid).then((response) => {
178
- if (self.deviceNotify) {
199
+ if (self.deviceNotify && response) {
179
200
  const message = { duid: device.duid, ...response.errorStatus, ...response.message };
180
201
  self.logger.debug('Device status update', debugStringify(message));
181
202
  self.deviceNotify(NotifyMessageTypes.LocalMessage, message);
@@ -201,6 +222,27 @@ export default class RoborockService {
201
222
  const scenes = (await this.iotApi.getScenes(homeDetails.rrHomeId)) ?? [];
202
223
  const products = new Map();
203
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
+ }
234
+ if (homeData.rooms.length === 0) {
235
+ const homeDataV2 = await this.iotApi.getHomev2(homeDetails.rrHomeId);
236
+ if (homeDataV2 && homeDataV2.rooms && homeDataV2.rooms.length > 0) {
237
+ homeData.rooms = homeDataV2.rooms;
238
+ }
239
+ else {
240
+ const homeDataV3 = await this.iotApi.getHomev3(homeDetails.rrHomeId);
241
+ if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
242
+ homeData.rooms = homeDataV3.rooms;
243
+ }
244
+ }
245
+ }
204
246
  const devices = [...homeData.devices, ...homeData.receivedDevices];
205
247
  const result = devices.map((device) => {
206
248
  return {
@@ -218,6 +260,7 @@ export default class RoborockService {
218
260
  model: homeData.products.find((p) => p.id === device.productId)?.model,
219
261
  category: homeData.products.find((p) => p.id === device.productId)?.category,
220
262
  batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100,
263
+ schema: homeData.products.find((p) => p.id === device.productId)?.schema,
221
264
  },
222
265
  store: {
223
266
  username: username,
@@ -240,6 +283,18 @@ export default class RoborockService {
240
283
  const products = new Map();
241
284
  homeData.products.forEach((p) => products.set(p.id, p.model));
242
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
+ }
243
298
  const dvs = devices.map((device) => {
244
299
  return {
245
300
  ...device,
@@ -253,6 +308,7 @@ export default class RoborockService {
253
308
  model: homeData.products.find((p) => p.id === device.productId)?.model,
254
309
  category: homeData.products.find((p) => p.id === device.productId)?.category,
255
310
  batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100,
311
+ schema: homeData.products.find((p) => p.id === device.productId)?.schema,
256
312
  },
257
313
  store: {
258
314
  userData: this.userdata,
@@ -278,9 +334,9 @@ export default class RoborockService {
278
334
  getRoomMappings(duid) {
279
335
  if (!this.messageClient) {
280
336
  this.logger.warn('messageClient not initialized. Waititing for next execution');
281
- return undefined;
337
+ return Promise.resolve(undefined);
282
338
  }
283
- 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) }));
284
340
  }
285
341
  async initializeMessageClient(username, device, userdata) {
286
342
  if (this.clientManager === undefined) {
@@ -332,6 +388,15 @@ export default class RoborockService {
332
388
  },
333
389
  });
334
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
+ }
335
400
  this.logger.debug('Local device', device.duid);
336
401
  let localIp = this.ipMap.get(device.duid);
337
402
  try {
@@ -347,7 +412,7 @@ export default class RoborockService {
347
412
  }
348
413
  if (localIp) {
349
414
  this.logger.debug('initializing the local connection for this client towards ' + localIp);
350
- const localClient = this.messageClient.registerClient(device.duid, localIp);
415
+ const localClient = this.messageClient.registerClient(device.duid, localIp, this.onLocalClientDisconnect);
351
416
  localClient.connect();
352
417
  let count = 0;
353
418
  while (!localClient.isConnected() && count < 20) {
@@ -369,6 +434,9 @@ export default class RoborockService {
369
434
  }
370
435
  return true;
371
436
  }
437
+ onLocalClientDisconnect(duid) {
438
+ this.mqttAlwaysOnDevices.set(duid, true);
439
+ }
372
440
  sleep(ms) {
373
441
  return new Promise((resolve) => setTimeout(resolve, ms));
374
442
  }
@@ -377,4 +445,7 @@ export default class RoborockService {
377
445
  this.iotApi = this.iotApiFactory(this.logger, userdata);
378
446
  return userdata;
379
447
  }
448
+ isRequestSecure(duid) {
449
+ return this.mqttAlwaysOnDevices.get(duid) ?? false;
450
+ }
380
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-rc09",
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-rc09 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-rc09",
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();
@@ -223,6 +223,12 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
223
223
  return false;
224
224
  }
225
225
 
226
+ if (vacuum.rooms === undefined || vacuum.rooms.length === 0) {
227
+ const map_info = await this.roborockService.getMapInformation(vacuum.duid);
228
+ const rooms = map_info?.maps?.[0]?.rooms ?? [];
229
+ vacuum.rooms = rooms;
230
+ }
231
+
226
232
  const roomMap = await this.platformRunner.getRoomMapFromDevice(vacuum);
227
233
 
228
234
  this.log.debug('Initializing - roomMap: ', debugStringify(roomMap));
@@ -273,8 +279,4 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
273
279
  this.log.logLevel = logLevel;
274
280
  return Promise.resolve();
275
281
  }
276
-
277
- private sleep(ms: number): Promise<void> {
278
- return new Promise((resolve) => setTimeout(resolve, ms));
279
- }
280
282
  }
@@ -46,7 +46,19 @@ 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
+
51
+ const apiResponse: ApiResponse<Home> = result.data;
52
+ if (apiResponse.result) {
53
+ return apiResponse.result;
54
+ } else {
55
+ this.logger.error('Failed to retrieve the home data');
56
+ return undefined;
57
+ }
58
+ }
59
+
60
+ public async getHomev3(homeId: number): Promise<Home | undefined> {
61
+ const result = await this.api.get('v3/user/homes/' + homeId); // can be v3 also
50
62
 
51
63
  const apiResponse: ApiResponse<Home> = result.data;
52
64
  if (apiResponse.result) {
@@ -80,4 +92,16 @@ export class RoborockIoTApi {
80
92
  return undefined;
81
93
  }
82
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
+ }
83
107
  }