matterbridge-roborock-vacuum-plugin 1.1.0-rc08 → 1.1.0-rc10

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 (30) hide show
  1. package/dist/platform.js +5 -0
  2. package/dist/roborockCommunication/RESTAPI/roborockIoTApi.js +11 -0
  3. package/dist/roborockCommunication/Zmodel/map.js +1 -0
  4. package/dist/roborockCommunication/Zmodel/mapInfo.js +24 -0
  5. package/dist/roborockCommunication/Zmodel/multipleMap.js +1 -0
  6. package/dist/roborockCommunication/broadcast/abstractClient.js +5 -3
  7. package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +25 -2
  8. package/dist/roborockCommunication/broadcast/client/MQTTClient.js +7 -0
  9. package/dist/roborockCommunication/broadcast/clientRouter.js +2 -2
  10. package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js +19 -4
  11. package/dist/roborockCommunication/helper/messageDeserializer.js +2 -2
  12. package/dist/roborockCommunication/index.js +1 -0
  13. package/dist/roborockService.js +31 -9
  14. package/matterbridge-roborock-vacuum-plugin.config.json +1 -1
  15. package/matterbridge-roborock-vacuum-plugin.schema.json +1 -1
  16. package/package.json +1 -1
  17. package/src/platform.ts +6 -0
  18. package/src/roborockCommunication/RESTAPI/roborockIoTApi.ts +12 -0
  19. package/src/roborockCommunication/Zmodel/map.ts +14 -0
  20. package/src/roborockCommunication/Zmodel/mapInfo.ts +31 -0
  21. package/src/roborockCommunication/Zmodel/multipleMap.ts +8 -0
  22. package/src/roborockCommunication/broadcast/abstractClient.ts +10 -5
  23. package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +31 -2
  24. package/src/roborockCommunication/broadcast/client/MQTTClient.ts +9 -0
  25. package/src/roborockCommunication/broadcast/clientRouter.ts +2 -2
  26. package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts +22 -4
  27. package/src/roborockCommunication/helper/messageDeserializer.ts +2 -2
  28. package/src/roborockCommunication/index.ts +2 -0
  29. package/src/roborockService.ts +38 -10
  30. package/src/tests/roborockService.test.ts +3 -11
package/dist/platform.js CHANGED
@@ -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);
@@ -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;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
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((room) => {
10
+ return {
11
+ id: room.iot_name_id,
12
+ name: room.iot_name,
13
+ };
14
+ }),
15
+ });
16
+ });
17
+ }
18
+ getById(id) {
19
+ return this.maps.find((m) => m.id === id)?.name;
20
+ }
21
+ getByName(name) {
22
+ return this.maps.find((m) => m.name === name.toLowerCase())?.id;
23
+ }
24
+ }
@@ -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;
@@ -14,17 +15,18 @@ export class AbstractClient {
14
15
  logger;
15
16
  context;
16
17
  syncMessageListener;
17
- connectionStateListener;
18
18
  constructor(logger, context) {
19
19
  this.context = context;
20
20
  this.serializer = new MessageSerializer(this.context, logger);
21
21
  this.deserializer = new MessageDeserializer(this.context, logger);
22
22
  this.syncMessageListener = new SyncMessageListener(logger);
23
23
  this.messageListeners.register(this.syncMessageListener);
24
- this.connectionStateListener = new ConnectionStateListener(logger, this);
25
- this.connectionListeners.register(this.connectionStateListener);
26
24
  this.logger = logger;
27
25
  }
26
+ initializeConnectionStateListener() {
27
+ const connectionStateListener = new ConnectionStateListener(this.logger, this, this.clientName, this.changeToSecureConnection, this.shouldReconnect);
28
+ this.connectionListeners.register(connectionStateListener);
29
+ }
28
30
  async get(duid, request) {
29
31
  return new Promise((resolve, reject) => {
30
32
  this.syncMessageListener.waitFor(request.messageId, (response) => resolve(response), reject);
@@ -7,22 +7,32 @@ 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;
11
+ clientName = 'LocalNetworkClient';
12
+ shouldReconnect = true;
10
13
  socket = undefined;
11
14
  buffer = new ChunkBuffer();
12
15
  messageIdSeq;
13
16
  pingInterval;
14
17
  duid;
15
18
  ip;
16
- constructor(logger, context, duid, ip) {
19
+ constructor(logger, context, duid, ip, inject) {
17
20
  super(logger, context);
18
21
  this.duid = duid;
19
22
  this.ip = ip;
20
23
  this.messageIdSeq = new Sequence(100000, 999999);
24
+ this.initializeConnectionStateListener();
25
+ this.changeToSecureConnection = inject;
21
26
  }
22
27
  connect() {
28
+ if (this.socket) {
29
+ this.socket.destroy();
30
+ this.socket = undefined;
31
+ return;
32
+ }
23
33
  this.socket = new Socket();
24
34
  this.socket.on('close', this.onDisconnect.bind(this));
25
- this.socket.on('end', this.onDisconnect.bind(this));
35
+ this.socket.on('end', this.onEnd.bind(this));
26
36
  this.socket.on('error', this.onError.bind(this));
27
37
  this.socket.on('data', this.onMessage.bind(this));
28
38
  this.socket.connect(58867, this.ip, this.onConnect.bind(this));
@@ -55,6 +65,19 @@ export class LocalNetworkClient extends AbstractClient {
55
65
  await this.sendHelloMessage();
56
66
  this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
57
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);
58
81
  }
59
82
  async onDisconnect() {
60
83
  this.logger.notice('LocalNetworkClient: Socket has disconnected.');
@@ -3,6 +3,9 @@ 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;
7
+ clientName = 'MQTTClient';
8
+ shouldReconnect = false;
6
9
  rriot;
7
10
  mqttUsername;
8
11
  mqttPassword;
@@ -12,6 +15,10 @@ export class MQTTClient extends AbstractClient {
12
15
  this.rriot = userdata.rriot;
13
16
  this.mqttUsername = CryptoUtils.md5hex(userdata.rriot.u + ':' + userdata.rriot.k).substring(2, 10);
14
17
  this.mqttPassword = CryptoUtils.md5hex(userdata.rriot.s + ':' + userdata.rriot.k).substring(16);
18
+ this.initializeConnectionStateListener();
19
+ this.changeToSecureConnection = (duid) => {
20
+ this.logger.info(`MqttClient for ${duid} has been disconnected`);
21
+ };
15
22
  }
16
23
  connect() {
17
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);
@@ -1,21 +1,36 @@
1
1
  export class ConnectionStateListener {
2
2
  logger;
3
3
  client;
4
- constructor(logger, client) {
4
+ clientName;
5
+ shouldReconnect;
6
+ changeToSecureConnection;
7
+ constructor(logger, client, clientName, changeToSecureConnection, shouldReconnect = false) {
5
8
  this.logger = logger;
6
9
  this.client = client;
10
+ this.clientName = clientName;
11
+ this.shouldReconnect = shouldReconnect;
12
+ this.changeToSecureConnection = changeToSecureConnection;
7
13
  }
8
14
  async onConnected(duid) {
9
- this.logger.notice(`Device ${duid} connected to MQTT broker`);
15
+ this.logger.notice(`Device ${duid} connected to ${this.clientName}`);
10
16
  }
11
17
  async onDisconnected(duid) {
12
- this.logger.notice(`Device ${duid} disconnected from MQTT broker`);
18
+ if (!this.shouldReconnect) {
19
+ this.logger.notice(`Device ${duid} disconnected from ${this.clientName}, but re-registration is disabled.`);
20
+ return;
21
+ }
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++;
13
28
  const isInDisconnectingStep = this.client.isInDisconnectingStep;
14
29
  if (isInDisconnectingStep) {
15
30
  this.logger.info(`Device with DUID ${duid} is in disconnecting step, skipping re-registration.`);
16
31
  return;
17
32
  }
18
- this.logger.info(`Re-registering device with DUID ${duid} to MQTT broker`);
33
+ this.logger.info(`Re-registering device with DUID ${duid} to ${this.clientName}`);
19
34
  this.client.connect();
20
35
  this.client.isInDisconnectingStep = false;
21
36
  }
@@ -56,14 +56,14 @@ export class MessageDeserializer {
56
56
  return new ResponseMessage(duid, { dps: { id: 0, result: null } });
57
57
  }
58
58
  if (data.protocol == Protocol.rpc_response || data.protocol == Protocol.general_request) {
59
- return this.deserializeProtocolRpcResponse(duid, data);
59
+ return this.deserializeRpcResponse(duid, data);
60
60
  }
61
61
  else {
62
62
  this.logger.error('unknown protocol: ' + data.protocol);
63
63
  return new ResponseMessage(duid, { dps: { id: 0, result: null } });
64
64
  }
65
65
  }
66
- deserializeProtocolRpcResponse(duid, data) {
66
+ deserializeRpcResponse(duid, data) {
67
67
  const payload = JSON.parse(data.payload.toString());
68
68
  const dps = payload.dps;
69
69
  this.parseJsonInDps(dps, Protocol.general_request);
@@ -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;
@@ -69,6 +69,17 @@ export default class RoborockService {
69
69
  }
70
70
  return data;
71
71
  }
72
+ async getRoomIdFromMap(duid) {
73
+ const data = (await this.customGet(duid, new RequestMessage({ method: 'get_map_v1' })));
74
+ return data?.vacuumRoom;
75
+ }
76
+ async getMapInformation(duid) {
77
+ this.logger.debug('RoborockService - getMapInformation', duid);
78
+ assert(this.messageClient !== undefined);
79
+ return this.messageClient.get(duid, new RequestMessage({ method: 'get_multi_maps_list' })).then((response) => {
80
+ return new MapInfo(response[0]);
81
+ });
82
+ }
72
83
  async changeCleanMode(duid, { suctionPower, waterFlow, distance_off, mopRoute }) {
73
84
  this.logger.notice('RoborockService - changeCleanMode');
74
85
  return this.getMessageProcessor(duid)?.changeCleanMode(duid, suctionPower, waterFlow, mopRoute, distance_off);
@@ -129,13 +140,9 @@ export default class RoborockService {
129
140
  this.logger.debug('RoborockService - findMe');
130
141
  await this.getMessageProcessor(duid)?.findMyRobot(duid);
131
142
  }
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 }));
143
+ async customGet(duid, request) {
144
+ this.logger.debug('RoborockService - customSend-message', request.method, request.params, request.secure);
145
+ return this.getMessageProcessor(duid)?.getCustomMessage(duid, request);
139
146
  }
140
147
  async customSend(duid, request) {
141
148
  return this.getMessageProcessor(duid)?.sendCustomMessage(duid, request);
@@ -201,6 +208,18 @@ export default class RoborockService {
201
208
  const scenes = (await this.iotApi.getScenes(homeDetails.rrHomeId)) ?? [];
202
209
  const products = new Map();
203
210
  homeData.products.forEach((p) => products.set(p.id, p.model));
211
+ if (homeData.rooms.length === 0) {
212
+ const homeDataV2 = await this.iotApi.getHomev2(homeDetails.rrHomeId);
213
+ if (homeDataV2 && homeDataV2.rooms && homeDataV2.rooms.length > 0) {
214
+ homeData.rooms = homeDataV2.rooms;
215
+ }
216
+ else {
217
+ const homeDataV3 = await this.iotApi.getHomev3(homeDetails.rrHomeId);
218
+ if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
219
+ homeData.rooms = homeDataV3.rooms;
220
+ }
221
+ }
222
+ }
204
223
  const devices = [...homeData.devices, ...homeData.receivedDevices];
205
224
  const result = devices.map((device) => {
206
225
  return {
@@ -347,7 +366,7 @@ export default class RoborockService {
347
366
  }
348
367
  if (localIp) {
349
368
  this.logger.debug('initializing the local connection for this client towards ' + localIp);
350
- const localClient = this.messageClient.registerClient(device.duid, localIp);
369
+ const localClient = this.messageClient.registerClient(device.duid, localIp, this.onLocalClientDisconnect);
351
370
  localClient.connect();
352
371
  let count = 0;
353
372
  while (!localClient.isConnected() && count < 20) {
@@ -369,6 +388,9 @@ export default class RoborockService {
369
388
  }
370
389
  return true;
371
390
  }
391
+ onLocalClientDisconnect(duid) {
392
+ this.logger.debug('Local client disconnected for device', duid);
393
+ }
372
394
  sleep(ms) {
373
395
  return new Promise((resolve) => setTimeout(resolve, ms));
374
396
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "matterbridge-roborock-vacuum-plugin",
3
3
  "type": "DynamicPlatform",
4
- "version": "1.1.0-rc08",
4
+ "version": "1.1.0-rc10",
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-rc08 by https://github.com/RinDevJunior",
3
+ "description": "matterbridge-roborock-vacuum-plugin v. 1.1.0-rc10 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-rc08",
3
+ "version": "1.1.0-rc10",
4
4
  "description": "Matterbridge Roborock Vacuum Plugin",
5
5
  "author": "https://github.com/RinDevJunior",
6
6
  "license": "MIT",
package/src/platform.ts CHANGED
@@ -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));
@@ -57,6 +57,18 @@ export class RoborockIoTApi {
57
57
  }
58
58
  }
59
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
62
+
63
+ const apiResponse: ApiResponse<Home> = result.data;
64
+ if (apiResponse.result) {
65
+ return apiResponse.result;
66
+ } else {
67
+ this.logger.error('Failed to retrieve the home data');
68
+ return undefined;
69
+ }
70
+ }
71
+
60
72
  public async getScenes(homeId: number): Promise<Scene[] | undefined> {
61
73
  const result = await this.api.get('user/scene/home/' + homeId);
62
74
 
@@ -0,0 +1,14 @@
1
+ export interface Map {
2
+ mapFlag: number;
3
+ add_time: number;
4
+ length: number;
5
+ name: string;
6
+ rooms: RoomInformation[];
7
+ }
8
+
9
+ export interface RoomInformation {
10
+ id: number;
11
+ tag: number;
12
+ iot_name_id: string;
13
+ iot_name: string;
14
+ }
@@ -0,0 +1,31 @@
1
+ import decodeComponent from '../helper/nameDecoder.js';
2
+ import { RoomInformation } from './map.js';
3
+ import type { MultipleMap } from './multipleMap.js';
4
+ import { Room } from './room.js';
5
+
6
+ export class MapInfo {
7
+ readonly maps: { id: number; name: string | undefined; rooms: Room[] }[] = [];
8
+
9
+ constructor(multimap: MultipleMap) {
10
+ multimap.map_info.forEach((map) => {
11
+ this.maps.push({
12
+ id: map.mapFlag,
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
+ }),
20
+ });
21
+ });
22
+ }
23
+
24
+ getById(id: number): string | undefined {
25
+ return this.maps.find((m) => m.id === id)?.name;
26
+ }
27
+
28
+ getByName(name: string): number | undefined {
29
+ return this.maps.find((m) => m.name === name.toLowerCase())?.id;
30
+ }
31
+ }
@@ -0,0 +1,8 @@
1
+ import { Map } from './map.js';
2
+
3
+ export interface MultipleMap {
4
+ max_multi_map: number;
5
+ max_bak_map: number;
6
+ multi_map_count: number;
7
+ map_info: Map[];
8
+ }
@@ -14,6 +14,7 @@ import { ConnectionStateListener } from './listener/implementation/connectionSta
14
14
 
15
15
  export abstract class AbstractClient implements Client {
16
16
  public isInDisconnectingStep = false;
17
+ public retryCount = 0;
17
18
 
18
19
  protected readonly connectionListeners = new ChainedConnectionListener();
19
20
  protected readonly messageListeners = new ChainedMessageListener();
@@ -22,9 +23,12 @@ export abstract class AbstractClient implements Client {
22
23
  protected connected = false;
23
24
  protected logger: AnsiLogger;
24
25
 
26
+ protected abstract clientName: string;
27
+ protected abstract shouldReconnect: boolean;
28
+ protected abstract changeToSecureConnection: (duid: string) => void;
29
+
25
30
  private readonly context: MessageContext;
26
31
  private readonly syncMessageListener: SyncMessageListener;
27
- private readonly connectionStateListener: ConnectionStateListener;
28
32
 
29
33
  protected constructor(logger: AnsiLogger, context: MessageContext) {
30
34
  this.context = context;
@@ -33,13 +37,14 @@ export abstract class AbstractClient implements Client {
33
37
 
34
38
  this.syncMessageListener = new SyncMessageListener(logger);
35
39
  this.messageListeners.register(this.syncMessageListener);
36
-
37
- this.connectionStateListener = new ConnectionStateListener(logger, this);
38
- this.connectionListeners.register(this.connectionStateListener);
39
-
40
40
  this.logger = logger;
41
41
  }
42
42
 
43
+ protected initializeConnectionStateListener() {
44
+ const connectionStateListener = new ConnectionStateListener(this.logger, this, this.clientName, this.changeToSecureConnection, this.shouldReconnect);
45
+ this.connectionListeners.register(connectionStateListener);
46
+ }
47
+
43
48
  abstract connect(): void;
44
49
  abstract disconnect(): Promise<void>;
45
50
  abstract send(duid: string, request: RequestMessage): Promise<void>;
@@ -9,6 +9,10 @@ import { Sequence } from '../../helper/sequence.js';
9
9
  import { ChunkBuffer } from '../../helper/chunkBuffer.js';
10
10
 
11
11
  export class LocalNetworkClient extends AbstractClient {
12
+ protected override changeToSecureConnection: (duid: string) => void;
13
+ protected override clientName = 'LocalNetworkClient';
14
+ protected override shouldReconnect = true;
15
+
12
16
  private socket: Socket | undefined = undefined;
13
17
  private buffer: ChunkBuffer = new ChunkBuffer();
14
18
  private messageIdSeq: Sequence;
@@ -16,17 +20,26 @@ export class LocalNetworkClient extends AbstractClient {
16
20
  duid: string;
17
21
  ip: string;
18
22
 
19
- public constructor(logger: AnsiLogger, context: MessageContext, duid: string, ip: string) {
23
+ constructor(logger: AnsiLogger, context: MessageContext, duid: string, ip: string, inject: (duid: string) => void) {
20
24
  super(logger, context);
21
25
  this.duid = duid;
22
26
  this.ip = ip;
23
27
  this.messageIdSeq = new Sequence(100000, 999999);
28
+
29
+ this.initializeConnectionStateListener();
30
+ this.changeToSecureConnection = inject;
24
31
  }
25
32
 
26
33
  public connect(): void {
34
+ if (this.socket) {
35
+ this.socket.destroy();
36
+ this.socket = undefined;
37
+ return;
38
+ }
39
+
27
40
  this.socket = new Socket();
28
41
  this.socket.on('close', this.onDisconnect.bind(this));
29
- this.socket.on('end', this.onDisconnect.bind(this));
42
+ this.socket.on('end', this.onEnd.bind(this));
30
43
  this.socket.on('error', this.onError.bind(this));
31
44
  this.socket.on('data', this.onMessage.bind(this));
32
45
  this.socket.connect(58867, this.ip, this.onConnect.bind(this));
@@ -66,6 +79,22 @@ export class LocalNetworkClient extends AbstractClient {
66
79
  await this.sendHelloMessage();
67
80
  this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
68
81
  await this.connectionListeners.onConnected(this.duid);
82
+ this.retryCount = 0;
83
+ }
84
+
85
+ private async onEnd(): Promise<void> {
86
+ this.logger.notice('LocalNetworkClient: Socket has ended.');
87
+ this.connected = false;
88
+
89
+ if (this.socket) {
90
+ this.socket.destroy();
91
+ this.socket = undefined;
92
+ }
93
+ if (this.pingInterval) {
94
+ clearInterval(this.pingInterval);
95
+ }
96
+
97
+ await this.connectionListeners.onDisconnected(this.duid);
69
98
  }
70
99
 
71
100
  private async onDisconnect(): Promise<void> {
@@ -7,6 +7,10 @@ import { Rriot, UserData } from '../../Zmodel/userData.js';
7
7
  import { AnsiLogger, debugStringify } from 'matterbridge/logger';
8
8
 
9
9
  export class MQTTClient extends AbstractClient {
10
+ protected override changeToSecureConnection: (duid: string) => void;
11
+ protected override clientName = 'MQTTClient';
12
+ protected override shouldReconnect = false;
13
+
10
14
  private readonly rriot: Rriot;
11
15
  private readonly mqttUsername: string;
12
16
  private readonly mqttPassword: string;
@@ -18,6 +22,11 @@ export class MQTTClient extends AbstractClient {
18
22
 
19
23
  this.mqttUsername = CryptoUtils.md5hex(userdata.rriot.u + ':' + userdata.rriot.k).substring(2, 10);
20
24
  this.mqttPassword = CryptoUtils.md5hex(userdata.rriot.s + ':' + userdata.rriot.k).substring(16);
25
+
26
+ this.initializeConnectionStateListener();
27
+ this.changeToSecureConnection = (duid: string) => {
28
+ this.logger.info(`MqttClient for ${duid} has been disconnected`);
29
+ };
21
30
  }
22
31
 
23
32
  public connect(): void {
@@ -32,8 +32,8 @@ export class ClientRouter implements Client {
32
32
  this.context.registerDevice(duid, localKey, pv);
33
33
  }
34
34
 
35
- public registerClient(duid: string, ip: string): Client {
36
- const localClient = new LocalNetworkClient(this.logger, this.context, duid, ip);
35
+ public registerClient(duid: string, ip: string, onDisconnect: (duid: string) => void): Client {
36
+ const localClient = new LocalNetworkClient(this.logger, this.context, duid, ip, onDisconnect);
37
37
  localClient.registerConnectionListener(this.connectionListeners);
38
38
  localClient.registerMessageListener(this.messageListeners);
39
39
 
@@ -5,17 +5,35 @@ import { AbstractClient } from '../../abstractClient.js';
5
5
  export class ConnectionStateListener implements AbstractConnectionListener {
6
6
  protected logger: AnsiLogger;
7
7
  protected client: AbstractClient;
8
- constructor(logger: AnsiLogger, client: AbstractClient) {
8
+ protected clientName: string;
9
+ protected shouldReconnect: boolean;
10
+ protected changeToSecureConnection: (duid: string) => void;
11
+
12
+ constructor(logger: AnsiLogger, client: AbstractClient, clientName: string, changeToSecureConnection: (duid: string) => void, shouldReconnect = false) {
9
13
  this.logger = logger;
10
14
  this.client = client;
15
+ this.clientName = clientName;
16
+ this.shouldReconnect = shouldReconnect;
17
+ this.changeToSecureConnection = changeToSecureConnection;
11
18
  }
12
19
 
13
20
  public async onConnected(duid: string): Promise<void> {
14
- this.logger.notice(`Device ${duid} connected to MQTT broker`);
21
+ this.logger.notice(`Device ${duid} connected to ${this.clientName}`);
15
22
  }
16
23
 
17
24
  public async onDisconnected(duid: string): Promise<void> {
18
- this.logger.notice(`Device ${duid} disconnected from MQTT broker`);
25
+ if (!this.shouldReconnect) {
26
+ this.logger.notice(`Device ${duid} disconnected from ${this.clientName}, but re-registration is disabled.`);
27
+ return;
28
+ }
29
+
30
+ if (this.client.retryCount > 10) {
31
+ this.logger.error(`Device with DUID ${duid} has exceeded retry limit, not re-registering.`);
32
+ this.changeToSecureConnection(duid);
33
+ return;
34
+ }
35
+
36
+ this.client.retryCount++;
19
37
 
20
38
  const isInDisconnectingStep = this.client.isInDisconnectingStep;
21
39
  if (isInDisconnectingStep) {
@@ -23,7 +41,7 @@ export class ConnectionStateListener implements AbstractConnectionListener {
23
41
  return;
24
42
  }
25
43
 
26
- this.logger.info(`Re-registering device with DUID ${duid} to MQTT broker`);
44
+ this.logger.info(`Re-registering device with DUID ${duid} to ${this.clientName}`);
27
45
  this.client.connect();
28
46
 
29
47
  this.client.isInDisconnectingStep = false;
@@ -79,14 +79,14 @@ export class MessageDeserializer {
79
79
  }
80
80
 
81
81
  if (data.protocol == Protocol.rpc_response || data.protocol == Protocol.general_request) {
82
- return this.deserializeProtocolRpcResponse(duid, data);
82
+ return this.deserializeRpcResponse(duid, data);
83
83
  } else {
84
84
  this.logger.error('unknown protocol: ' + data.protocol);
85
85
  return new ResponseMessage(duid, { dps: { id: 0, result: null } });
86
86
  }
87
87
  }
88
88
 
89
- private deserializeProtocolRpcResponse(duid: string, data: Message): ResponseMessage {
89
+ private deserializeRpcResponse(duid: string, data: Message): ResponseMessage {
90
90
  const payload = JSON.parse(data.payload.toString());
91
91
  const dps = payload.dps;
92
92
  this.parseJsonInDps(dps, Protocol.general_request);
@@ -7,6 +7,7 @@ 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
 
11
12
  export { Scene } from './Zmodel/scene.js';
12
13
 
@@ -18,3 +19,4 @@ export type { Home } from './Zmodel/home.js';
18
19
  export type { Client } from './broadcast/client.js';
19
20
  export type { SceneParam } from './Zmodel/scene.js';
20
21
  export type { BatteryMessage, DeviceErrorMessage, DeviceStatusNotify } from './Zmodel/batteryMessage.js';
22
+ export type { MultipleMap } from './Zmodel/multipleMap.js';
@@ -19,8 +19,9 @@ import {
19
19
  ResponseMessage,
20
20
  Scene,
21
21
  SceneParam,
22
+ MapInfo,
22
23
  } from './roborockCommunication/index.js';
23
- import type { AbstractMessageHandler, AbstractMessageListener, BatteryMessage, DeviceErrorMessage, DeviceStatusNotify } from './roborockCommunication/index.js';
24
+ import type { AbstractMessageHandler, AbstractMessageListener, BatteryMessage, DeviceErrorMessage, DeviceStatusNotify, MultipleMap } from './roborockCommunication/index.js';
24
25
  import { ServiceArea } from 'matterbridge/matter/clusters';
25
26
  import { LocalNetworkClient } from './roborockCommunication/broadcast/client/LocalNetworkClient.js';
26
27
  export type Factory<A, T> = (logger: AnsiLogger, arg: A) => T;
@@ -115,6 +116,19 @@ export default class RoborockService {
115
116
  return data;
116
117
  }
117
118
 
119
+ public async getRoomIdFromMap(duid: string): Promise<number | undefined> {
120
+ const data = (await this.customGet(duid, new RequestMessage({ method: 'get_map_v1' }))) as { vacuumRoom: number | undefined };
121
+ return data?.vacuumRoom;
122
+ }
123
+
124
+ public async getMapInformation(duid: string): Promise<MapInfo> {
125
+ this.logger.debug('RoborockService - getMapInformation', duid);
126
+ assert(this.messageClient !== undefined);
127
+ return this.messageClient.get<MultipleMap[]>(duid, new RequestMessage({ method: 'get_multi_maps_list' })).then((response) => {
128
+ return new MapInfo(response[0]);
129
+ });
130
+ }
131
+
118
132
  public async changeCleanMode(
119
133
  duid: string,
120
134
  { suctionPower, waterFlow, distance_off, mopRoute }: { suctionPower: number; waterFlow: number; distance_off: number; mopRoute: number },
@@ -189,14 +203,9 @@ export default class RoborockService {
189
203
  await this.getMessageProcessor(duid)?.findMyRobot(duid);
190
204
  }
191
205
 
192
- public async customGet(duid: string, method: string): Promise<unknown> {
193
- this.logger.debug('RoborockService - customSend-message', method);
194
- return this.getMessageProcessor(duid)?.getCustomMessage(duid, new RequestMessage({ method }));
195
- }
196
-
197
- public async customGetInSecure(duid: string, method: string): Promise<unknown> {
198
- this.logger.debug('RoborockService - customGetInSecure-message', method);
199
- return this.getMessageProcessor(duid)?.getCustomMessage(duid, new RequestMessage({ method, secure: true }));
206
+ public async customGet(duid: string, request: RequestMessage): Promise<unknown> {
207
+ this.logger.debug('RoborockService - customSend-message', request.method, request.params, request.secure);
208
+ return this.getMessageProcessor(duid)?.getCustomMessage(duid, request);
200
209
  }
201
210
 
202
211
  public async customSend(duid: string, request: RequestMessage): Promise<void> {
@@ -274,6 +283,20 @@ export default class RoborockService {
274
283
 
275
284
  const products = new Map<string, string>();
276
285
  homeData.products.forEach((p) => products.set(p.id, p.model));
286
+
287
+ // Try to get rooms from v2 API if rooms are empty
288
+ if (homeData.rooms.length === 0) {
289
+ const homeDataV2 = await this.iotApi.getHomev2(homeDetails.rrHomeId);
290
+ if (homeDataV2 && homeDataV2.rooms && homeDataV2.rooms.length > 0) {
291
+ homeData.rooms = homeDataV2.rooms;
292
+ } else {
293
+ const homeDataV3 = await this.iotApi.getHomev3(homeDetails.rrHomeId);
294
+ if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
295
+ homeData.rooms = homeDataV3.rooms;
296
+ }
297
+ }
298
+ }
299
+
277
300
  const devices: Device[] = [...homeData.devices, ...homeData.receivedDevices];
278
301
  // homeData.devices.length > 0 ? homeData.devices : homeData.receivedDevices;
279
302
 
@@ -457,7 +480,7 @@ export default class RoborockService {
457
480
 
458
481
  if (localIp) {
459
482
  this.logger.debug('initializing the local connection for this client towards ' + localIp);
460
- const localClient = this.messageClient.registerClient(device.duid, localIp) as LocalNetworkClient;
483
+ const localClient = this.messageClient.registerClient(device.duid, localIp, this.onLocalClientDisconnect) as LocalNetworkClient;
461
484
  localClient.connect();
462
485
 
463
486
  let count = 0;
@@ -483,6 +506,11 @@ export default class RoborockService {
483
506
  return true;
484
507
  }
485
508
 
509
+ private onLocalClientDisconnect(duid: string): void {
510
+ // this.mqttAlwaysOnDevices.set(duid, true);
511
+ this.logger.debug('Local client disconnected for device', duid);
512
+ }
513
+
486
514
  private sleep(ms: number): Promise<void> {
487
515
  return new Promise((resolve) => setTimeout(resolve, ms));
488
516
  }
@@ -2,7 +2,7 @@ import { AnsiLogger } from 'matterbridge/logger';
2
2
  import { ServiceArea } from 'matterbridge/matter/clusters';
3
3
  import RoborockService from '../roborockService';
4
4
  import { MessageProcessor } from '../roborockCommunication/broadcast/messageProcessor';
5
- import { Device } from '../roborockCommunication';
5
+ import { Device, RequestMessage } from '../roborockCommunication';
6
6
 
7
7
  describe('RoborockService - startClean', () => {
8
8
  let roborockService: RoborockService;
@@ -316,20 +316,12 @@ describe('RoborockService - customGet/customGetInSecure/customSend', () => {
316
316
 
317
317
  it('customGet should call getCustomMessage', async () => {
318
318
  mockMessageProcessor.getCustomMessage.mockResolvedValue('result');
319
- const result = await roborockService.customGet('duid', 'method');
320
- expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - customSend-message', 'method');
319
+ const result = await roborockService.customGet('duid', { method: 'method', params: undefined, secure: true } as RequestMessage);
320
+ expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - customSend-message', 'method', undefined, true);
321
321
  expect(mockMessageProcessor.getCustomMessage).toHaveBeenCalledWith('duid', expect.any(Object));
322
322
  expect(result).toBe('result');
323
323
  });
324
324
 
325
- it('customGetInSecure should call getCustomMessage with secure', async () => {
326
- mockMessageProcessor.getCustomMessage.mockResolvedValue('secureResult');
327
- const result = await roborockService.customGetInSecure('duid', 'method');
328
- expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - customGetInSecure-message', 'method');
329
- expect(mockMessageProcessor.getCustomMessage).toHaveBeenCalledWith('duid', expect.objectContaining({ secure: true }));
330
- expect(result).toBe('secureResult');
331
- });
332
-
333
325
  it('customSend should call sendCustomMessage', async () => {
334
326
  const req = { foo: 'bar' } as any;
335
327
  await roborockService.customSend('duid', req);