matterbridge-roborock-vacuum-plugin 1.1.0-rc07 → 1.1.0-rc09

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 (32) hide show
  1. package/README.md +4 -1
  2. package/README_DEV.md +13 -7
  3. package/dist/model/ExperimentalFeatureSetting.js +1 -0
  4. package/dist/platform.js +16 -3
  5. package/dist/platformRunner.js +1 -1
  6. package/dist/roborockCommunication/RESTAPI/roborockAuthenticateApi.js +4 -0
  7. package/dist/roborockCommunication/broadcast/abstractClient.js +9 -3
  8. package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +7 -3
  9. package/dist/roborockCommunication/broadcast/client/MQTTClient.js +9 -4
  10. package/dist/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.js +6 -6
  11. package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js +33 -0
  12. package/dist/roborockService.js +13 -14
  13. package/matterbridge-roborock-vacuum-plugin.config.json +4 -3
  14. package/matterbridge-roborock-vacuum-plugin.schema.json +15 -37
  15. package/package.json +1 -1
  16. package/src/model/ExperimentalFeatureSetting.ts +2 -0
  17. package/src/platform.ts +24 -4
  18. package/src/platformRunner.ts +1 -1
  19. package/src/roborockCommunication/RESTAPI/roborockAuthenticateApi.ts +7 -2
  20. package/src/roborockCommunication/Zmodel/userData.ts +3 -0
  21. package/src/roborockCommunication/broadcast/abstractClient.ts +14 -4
  22. package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +10 -3
  23. package/src/roborockCommunication/broadcast/client/MQTTClient.ts +11 -4
  24. package/src/roborockCommunication/broadcast/listener/abstractConnectionListener.ts +3 -3
  25. package/src/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.ts +8 -7
  26. package/src/roborockCommunication/broadcast/listener/implementation/chainedMessageListener.ts +2 -2
  27. package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts +45 -0
  28. package/src/roborockService.ts +21 -22
  29. package/src/tests/roborockCommunication/broadcast/client/LocalNetworkClient.test.ts +1 -1
  30. package/src/tests/roborockCommunication/broadcast/client/MQTTClient.test.ts +1 -1
  31. package/src/tests/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.test.ts +8 -8
  32. package/src/tests/roborockService.test.ts +1 -1
package/README.md CHANGED
@@ -70,7 +70,10 @@ To get the **DUID** for your devices, you have two options:
70
70
 
71
71
  - **Under active development**
72
72
  - Requires **`matterbridge@3.1.3`**
73
- - ⚠️ **Known Issue:** Vacuum may appear as **two devices** in Apple Home
73
+ - ⚠️ **Known Issue:**
74
+ + Vacuum may appear as **two devices** in Apple Home
75
+ + Error: Wrong CRC32 2241274590, expected 0 -> this is normal error and not impact to plugin functionality
76
+ +
74
77
 
75
78
  ---
76
79
 
package/README_DEV.md CHANGED
@@ -8,7 +8,7 @@ Follow these steps to add support for a new device:
8
8
 
9
9
  ## 1. Check the Model
10
10
 
11
- - Open [`src/roborock/features/DeviceModel.ts`](src/roborock/features/DeviceModel.ts).
11
+ - Open [`src/roborockCommunication/Zmodel/deviceModel.ts`](src/roborockCommunication/Zmodel/deviceModel.ts).
12
12
  - If your model does not exist, **create a new entry** for it.
13
13
 
14
14
  ---
@@ -17,21 +17,27 @@ Follow these steps to add support for a new device:
17
17
 
18
18
  - Create a new folder under [`src/behaviors/roborock.vacuum`](src/behaviors/roborock.vacuum) named after the market name of your vacuum.
19
19
  - Inside this folder:
20
- - **Create a `model.ts` file**
21
- _Example:_ [`src/behaviors/roborock.vacuum/QREVO_EDGE_5V1/a187.ts`](src/behaviors/roborock.vacuum/QREVO_EDGE_5V1/a187.ts)
22
- This file handles sending requests to control your vacuum (start, pause, resume, go home, etc.).
23
- - **Create an `initalData.ts` file**
24
- This file defines functions that return initial data for your device.
20
+ - **Create a `initalData.ts` file**
21
+ _Example:_ [`src/behaviors/roborock.vacuum/smart/initalData.ts`](src/behaviors/roborock.vacuum/smart/initalData.ts)
22
+ This file defines functions that return initial data for your device. (You can inherit from default or create your own)
23
+ - **Create an `runtimes.ts` file**
24
+ _Example:_ [`src/behaviors/roborock.vacuum/smart/runtimes.ts`](src/behaviors/roborock.vacuum/smart/runtimes.ts)
25
+ In case you define your own initial data. Create new method that override the default method.
26
+ - **Create an `abcyxz.ts` file**
27
+ _Example:_ [`src/behaviors/roborock.vacuum/smart/smart.ts`](src/behaviors/roborock.vacuum/smart/smart.ts)
28
+ Define matterbridge command handler logic to sending requests to control your vacuum (start, pause, resume, go home, etc.).
29
+ (Use in step 4)
25
30
 
26
31
  ---
27
32
 
28
33
  ## 3. Register Your Initial Functions
29
34
 
30
35
  - Add your initial functions to the following files:
31
- - [`src/initialData/getOperationalStates.ts`](src/initialData/getOperationalStates.ts)
32
36
  - [`src/initialData/getSupportedCleanModes.ts`](src/initialData/getSupportedCleanModes.ts)
33
37
  - [`src/initialData/getSupportedRunModes.ts`](src/initialData/getSupportedRunModes.ts)
38
+ - [`src/initialData/getOperationalStates.ts`](src/initialData/getOperationalStates.ts)
34
39
 
40
+ ***_In somecase, you may need to update whole function to support switch case_***
35
41
  ---
36
42
 
37
43
  ## 4. Update the Behavior Factory
@@ -7,6 +7,7 @@ export function createDefaultExperimentalFeatureSetting() {
7
7
  forceRunAtDefault: false,
8
8
  useVacationModeToSendVacuumToDock: false,
9
9
  enableServerMode: false,
10
+ alwaysExecuteAuthentication: false,
10
11
  },
11
12
  cleanModeSettings: {
12
13
  enableCleanModeMapping: false,
package/dist/platform.js CHANGED
@@ -26,8 +26,8 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
26
26
  rrHomeId;
27
27
  constructor(matterbridge, log, config) {
28
28
  super(matterbridge, log, config);
29
- if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.1.3')) {
30
- throw new Error(`This plugin requires Matterbridge version >= "3.1.3". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend.`);
29
+ if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.1.5')) {
30
+ throw new Error(`This plugin requires Matterbridge version >= "3.1.5". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend.`);
31
31
  }
32
32
  this.log.info('Initializing platform:', this.config.name);
33
33
  if (config.whiteList === undefined)
@@ -61,7 +61,20 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
61
61
  this.roborockService = new RoborockService(() => new RoborockAuthenticateApi(this.log, axiosInstance), (logger, ud) => new RoborockIoTApi(ud, logger), this.config.refreshInterval ?? 60, this.clientManager, this.log);
62
62
  const username = this.config.username;
63
63
  const password = this.config.password;
64
- const userData = await this.roborockService.loginWithPassword(username, password);
64
+ const userData = await this.roborockService.loginWithPassword(username, password, async () => {
65
+ if (this.enableExperimentalFeature?.enableExperimentalFeature && this.enableExperimentalFeature.advancedFeature?.alwaysExecuteAuthentication) {
66
+ this.log.debug('Always execute authentication on startup');
67
+ return undefined;
68
+ }
69
+ const savedUserData = (await this.persist.getItem('userData'));
70
+ if (savedUserData) {
71
+ this.log.debug('Loading saved userData:', debugStringify(savedUserData));
72
+ return savedUserData;
73
+ }
74
+ return undefined;
75
+ }, async (userData) => {
76
+ await this.persist.setItem('userData', userData);
77
+ });
65
78
  this.log.debug('Initializing - userData:', debugStringify(userData));
66
79
  const devices = await this.roborockService.listDevices(username);
67
80
  this.log.notice('Initializing - devices: ', debugStringify(devices));
@@ -72,7 +72,7 @@ export class PlatformRunner {
72
72
  return;
73
73
  }
74
74
  if (!tracked) {
75
- platform.log.debug(`${messageSource} updateFromMQTTMessage: ${debugStringify(messageData)}`);
75
+ platform.log.debug(`Receive: ${messageSource} updateFromMQTTMessage: ${debugStringify(messageData)}`);
76
76
  }
77
77
  if (!robot.serialNumber) {
78
78
  platform.log.error('Robot serial number is undefined');
@@ -12,6 +12,10 @@ export class RoborockAuthenticateApi {
12
12
  this.axiosFactory = axiosFactory;
13
13
  this.logger = logger;
14
14
  }
15
+ async loginWithUserData(username, userData) {
16
+ this.loginWithAuthToken(username, userData.token);
17
+ return userData;
18
+ }
15
19
  async loginWithPassword(username, password) {
16
20
  const api = await this.getAPIFor(username);
17
21
  const response = await api.post('api/v1/login', new URLSearchParams({
@@ -3,15 +3,17 @@ import { MessageSerializer } from '../helper/messageSerializer.js';
3
3
  import { ChainedConnectionListener } from './listener/implementation/chainedConnectionListener.js';
4
4
  import { ChainedMessageListener } from './listener/implementation/chainedMessageListener.js';
5
5
  import { SyncMessageListener } from './listener/implementation/syncMessageListener.js';
6
+ import { ConnectionStateListener } from './listener/implementation/connectionStateListener.js';
6
7
  export class AbstractClient {
8
+ isInDisconnectingStep = false;
7
9
  connectionListeners = new ChainedConnectionListener();
8
10
  messageListeners = new ChainedMessageListener();
9
- connected = false;
10
- context;
11
11
  serializer;
12
12
  deserializer;
13
- syncMessageListener;
13
+ connected = false;
14
14
  logger;
15
+ context;
16
+ syncMessageListener;
15
17
  constructor(logger, context) {
16
18
  this.context = context;
17
19
  this.serializer = new MessageSerializer(this.context, logger);
@@ -20,6 +22,10 @@ export class AbstractClient {
20
22
  this.messageListeners.register(this.syncMessageListener);
21
23
  this.logger = logger;
22
24
  }
25
+ initializeConnectionStateListener() {
26
+ const connectionStateListener = new ConnectionStateListener(this.logger, this, this.clientName, this.shouldReconnect);
27
+ this.connectionListeners.register(connectionStateListener);
28
+ }
23
29
  async get(duid, request) {
24
30
  return new Promise((resolve, reject) => {
25
31
  this.syncMessageListener.waitFor(request.messageId, (response) => resolve(response), reject);
@@ -7,6 +7,8 @@ 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
+ clientName = 'LocalNetworkClient';
11
+ shouldReconnect = true;
10
12
  socket = undefined;
11
13
  buffer = new ChunkBuffer();
12
14
  messageIdSeq;
@@ -18,6 +20,7 @@ export class LocalNetworkClient extends AbstractClient {
18
20
  this.duid = duid;
19
21
  this.ip = ip;
20
22
  this.messageIdSeq = new Sequence(100000, 999999);
23
+ this.initializeConnectionStateListener();
21
24
  }
22
25
  connect() {
23
26
  this.socket = new Socket();
@@ -31,6 +34,7 @@ export class LocalNetworkClient extends AbstractClient {
31
34
  if (!this.socket) {
32
35
  return;
33
36
  }
37
+ this.isInDisconnectingStep = true;
34
38
  if (this.pingInterval) {
35
39
  clearInterval(this.pingInterval);
36
40
  }
@@ -53,7 +57,7 @@ export class LocalNetworkClient extends AbstractClient {
53
57
  this.logger.debug(`${this.duid} connected to ${this.ip}, address: ${address ? debugStringify(address) : 'undefined'}`);
54
58
  await this.sendHelloMessage();
55
59
  this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
56
- await this.connectionListeners.onConnected();
60
+ await this.connectionListeners.onConnected(this.duid);
57
61
  }
58
62
  async onDisconnect() {
59
63
  this.logger.notice('LocalNetworkClient: Socket has disconnected.');
@@ -65,7 +69,7 @@ export class LocalNetworkClient extends AbstractClient {
65
69
  if (this.pingInterval) {
66
70
  clearInterval(this.pingInterval);
67
71
  }
68
- await this.connectionListeners.onDisconnected();
72
+ await this.connectionListeners.onDisconnected(this.duid);
69
73
  }
70
74
  async onError(result) {
71
75
  this.logger.error('LocalNetworkClient: Socket connection error: ' + result);
@@ -74,7 +78,7 @@ export class LocalNetworkClient extends AbstractClient {
74
78
  this.socket.destroy();
75
79
  this.socket = undefined;
76
80
  }
77
- await this.connectionListeners.onError(result.toString());
81
+ await this.connectionListeners.onError(this.duid, result.toString());
78
82
  }
79
83
  async onMessage(message) {
80
84
  if (!this.socket) {
@@ -3,6 +3,8 @@ 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
+ clientName = 'MQTTClient';
7
+ shouldReconnect = false;
6
8
  rriot;
7
9
  mqttUsername;
8
10
  mqttPassword;
@@ -12,6 +14,7 @@ export class MQTTClient extends AbstractClient {
12
14
  this.rriot = userdata.rriot;
13
15
  this.mqttUsername = CryptoUtils.md5hex(userdata.rriot.u + ':' + userdata.rriot.k).substring(2, 10);
14
16
  this.mqttPassword = CryptoUtils.md5hex(userdata.rriot.s + ':' + userdata.rriot.k).substring(16);
17
+ this.initializeConnectionStateListener();
15
18
  }
16
19
  connect() {
17
20
  if (this.client) {
@@ -38,6 +41,7 @@ export class MQTTClient extends AbstractClient {
38
41
  return;
39
42
  }
40
43
  try {
44
+ this.isInDisconnectingStep = true;
41
45
  this.client.end();
42
46
  }
43
47
  catch (error) {
@@ -58,7 +62,7 @@ export class MQTTClient extends AbstractClient {
58
62
  return;
59
63
  }
60
64
  this.connected = true;
61
- await this.connectionListeners.onConnected();
65
+ await this.connectionListeners.onConnected('mqtt-' + this.mqttUsername);
62
66
  this.subscribeToQueue();
63
67
  }
64
68
  subscribeToQueue() {
@@ -73,15 +77,16 @@ export class MQTTClient extends AbstractClient {
73
77
  }
74
78
  this.logger.error('failed to subscribe to the queue: ' + err);
75
79
  this.connected = false;
76
- await this.connectionListeners.onDisconnected();
80
+ await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername);
77
81
  }
78
82
  async onDisconnect() {
79
- await this.connectionListeners.onDisconnected();
83
+ this.connected = false;
84
+ await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername);
80
85
  }
81
86
  async onError(result) {
82
87
  this.logger.error('MQTT connection error: ' + result);
83
88
  this.connected = false;
84
- await this.connectionListeners.onError(result.toString());
89
+ await this.connectionListeners.onError('mqtt-' + this.mqttUsername, result.toString());
85
90
  }
86
91
  onReconnect() {
87
92
  this.subscribeToQueue();
@@ -3,19 +3,19 @@ export class ChainedConnectionListener {
3
3
  register(listener) {
4
4
  this.listeners.push(listener);
5
5
  }
6
- async onConnected() {
6
+ async onConnected(duid) {
7
7
  for (const listener of this.listeners) {
8
- await listener.onConnected();
8
+ await listener.onConnected(duid);
9
9
  }
10
10
  }
11
- async onDisconnected() {
11
+ async onDisconnected(duid) {
12
12
  for (const listener of this.listeners) {
13
- await listener.onDisconnected();
13
+ await listener.onDisconnected(duid);
14
14
  }
15
15
  }
16
- async onError(message) {
16
+ async onError(duid, message) {
17
17
  for (const listener of this.listeners) {
18
- await listener.onError(message);
18
+ await listener.onError(duid, message);
19
19
  }
20
20
  }
21
21
  }
@@ -0,0 +1,33 @@
1
+ export class ConnectionStateListener {
2
+ logger;
3
+ client;
4
+ clientName;
5
+ shouldReconnect;
6
+ constructor(logger, client, clientName, shouldReconnect = false) {
7
+ this.logger = logger;
8
+ this.client = client;
9
+ this.clientName = clientName;
10
+ this.shouldReconnect = shouldReconnect;
11
+ }
12
+ async onConnected(duid) {
13
+ this.logger.notice(`Device ${duid} connected to ${this.clientName}`);
14
+ }
15
+ async onDisconnected(duid) {
16
+ if (!this.shouldReconnect) {
17
+ this.logger.notice(`Device ${duid} disconnected from ${this.clientName}, but re-registration is disabled.`);
18
+ return;
19
+ }
20
+ this.logger.notice(`Device ${duid} disconnected from ${this.clientName}`);
21
+ const isInDisconnectingStep = this.client.isInDisconnectingStep;
22
+ if (isInDisconnectingStep) {
23
+ this.logger.info(`Device with DUID ${duid} is in disconnecting step, skipping re-registration.`);
24
+ return;
25
+ }
26
+ this.logger.info(`Re-registering device with DUID ${duid} to ${this.clientName}`);
27
+ this.client.connect();
28
+ this.client.isInDisconnectingStep = false;
29
+ }
30
+ async onError(duid, message) {
31
+ this.logger.error(`Error on device with DUID ${duid}: ${message}`);
32
+ }
33
+ }
@@ -28,8 +28,17 @@ export default class RoborockService {
28
28
  this.refreshInterval = refreshInterval;
29
29
  this.clientManager = clientManager;
30
30
  }
31
- async loginWithPassword(username, password) {
32
- const userdata = await this.loginApi.loginWithPassword(username, password);
31
+ async loginWithPassword(username, password, loadSavedUserData, savedUserData) {
32
+ let userdata = await loadSavedUserData();
33
+ if (!userdata) {
34
+ this.logger.debug('No saved user data found, logging in with password');
35
+ userdata = await this.loginApi.loginWithPassword(username, password);
36
+ await savedUserData(userdata);
37
+ }
38
+ else {
39
+ this.logger.debug('Using saved user data for login', debugStringify(userdata));
40
+ userdata = await this.loginApi.loginWithUserData(username, userdata);
41
+ }
33
42
  return this.auth(userdata);
34
43
  }
35
44
  getMessageProcessor(duid) {
@@ -281,17 +290,6 @@ export default class RoborockService {
281
290
  const self = this;
282
291
  this.messageClient = this.clientManager.get(username, userdata);
283
292
  this.messageClient.registerDevice(device.duid, device.localKey, device.pv);
284
- this.messageClient.registerConnectionListener({
285
- onConnected: () => {
286
- self.logger.notice('Connected to MQTT broker');
287
- },
288
- onDisconnected: () => {
289
- self.logger.notice('Disconnected from MQTT broker');
290
- },
291
- onError: (message) => {
292
- self.logger.error('Error from MQTT broker', message);
293
- },
294
- });
295
293
  this.messageClient.registerMessageListener({
296
294
  onMessage: (message) => {
297
295
  if (message instanceof ResponseMessage) {
@@ -341,9 +339,10 @@ export default class RoborockService {
341
339
  this.logger.debug('Requesting network info for device', device.duid);
342
340
  const networkInfo = await messageProcessor.getNetworkInfo(device.duid);
343
341
  if (!networkInfo || !networkInfo.ip) {
344
- this.logger.error('Failed to retrieve network info for device', device.duid);
342
+ this.logger.error('Failed to retrieve network info for device', device.duid, 'Network info:', networkInfo);
345
343
  return false;
346
344
  }
345
+ this.logger.debug('Network ip for device', device.duid, 'is', networkInfo.ip);
347
346
  localIp = networkInfo.ip;
348
347
  }
349
348
  if (localIp) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "matterbridge-roborock-vacuum-plugin",
3
3
  "type": "DynamicPlatform",
4
- "version": "1.1.0-rc07",
4
+ "version": "1.1.0-rc09",
5
5
  "whiteList": [],
6
6
  "blackList": [],
7
7
  "useInterval": true,
@@ -12,7 +12,8 @@
12
12
  "includeDockStationStatus": false,
13
13
  "forceRunAtDefault": false,
14
14
  "useVacationModeToSendVacuumToDock": false,
15
- "enableServerMode": false
15
+ "enableServerMode": false,
16
+ "alwaysExecuteAuthentication": false
16
17
  },
17
18
  "cleanModeSettings": {
18
19
  "enableCleanModeMapping": false,
@@ -36,4 +37,4 @@
36
37
  "debug": true,
37
38
  "unregisterOnShutdown": false,
38
39
  "enableExperimentalFeature": false
39
- }
40
+ }
@@ -1,11 +1,8 @@
1
1
  {
2
2
  "title": "Matterbridge Roborock Vacuum Plugin",
3
- "description": "matterbridge-roborock-vacuum-plugin v. 1.1.0-rc07 by https://github.com/RinDevJunior",
3
+ "description": "matterbridge-roborock-vacuum-plugin v. 1.1.0-rc09 by https://github.com/RinDevJunior",
4
4
  "type": "object",
5
- "required": [
6
- "username",
7
- "password"
8
- ],
5
+ "required": ["username", "password"],
9
6
  "properties": {
10
7
  "name": {
11
8
  "description": "Plugin name",
@@ -60,9 +57,7 @@
60
57
  "const": true
61
58
  }
62
59
  },
63
- "required": [
64
- "enableExperimentalFeature"
65
- ]
60
+ "required": ["enableExperimentalFeature"]
66
61
  },
67
62
  "then": {
68
63
  "properties": {
@@ -94,6 +89,11 @@
94
89
  "description": "Enable the Robot Vacuum Cleaner in server mode (Each vacuum will have its own server).",
95
90
  "type": "boolean",
96
91
  "default": false
92
+ },
93
+ "alwaysExecuteAuthentication": {
94
+ "description": "Always execute authentication on startup.",
95
+ "type": "boolean",
96
+ "default": false
97
97
  }
98
98
  }
99
99
  },
@@ -114,9 +114,7 @@
114
114
  "const": true
115
115
  }
116
116
  },
117
- "required": [
118
- "enableCleanModeMapping"
119
- ]
117
+ "required": ["enableCleanModeMapping"]
120
118
  },
121
119
  "then": {
122
120
  "properties": {
@@ -161,9 +159,7 @@
161
159
  "default": 25
162
160
  }
163
161
  },
164
- "required": [
165
- "distanceOff"
166
- ]
162
+ "required": ["distanceOff"]
167
163
  }
168
164
  }
169
165
  ]
@@ -200,9 +196,7 @@
200
196
  "default": 25
201
197
  }
202
198
  },
203
- "required": [
204
- "distanceOff"
205
- ]
199
+ "required": ["distanceOff"]
206
200
  }
207
201
  }
208
202
  ]
@@ -232,36 +226,20 @@
232
226
  "fanMode": {
233
227
  "type": "string",
234
228
  "description": "Suction power mode to use (e.g., 'Quiet', 'Balanced', 'Turbo', 'Max', 'MaxPlus').",
235
- "enum": [
236
- "Quiet",
237
- "Balanced",
238
- "Turbo",
239
- "Max",
240
- "MaxPlus"
241
- ],
229
+ "enum": ["Quiet", "Balanced", "Turbo", "Max", "MaxPlus"],
242
230
  "default": "Balanced"
243
231
  },
244
232
  "waterFlowMode": {
245
233
  "type": "string",
246
234
  "description": "Water flow mode to use (e.g., 'Low', 'Medium', 'High', 'CustomizeWithDistanceOff').",
247
- "enum": [
248
- "Low",
249
- "Medium",
250
- "High",
251
- "CustomizeWithDistanceOff"
252
- ],
235
+ "enum": ["Low", "Medium", "High", "CustomizeWithDistanceOff"],
253
236
  "default": "Medium"
254
237
  },
255
238
  "mopRouteMode": {
256
239
  "type": "string",
257
240
  "description": "Mop route intensity to use (e.g., 'Standard', 'Deep', 'DeepPlus', 'Fast').",
258
- "enum": [
259
- "Standard",
260
- "Deep",
261
- "DeepPlus",
262
- "Fast"
263
- ],
241
+ "enum": ["Standard", "Deep", "DeepPlus", "Fast"],
264
242
  "default": "Standard"
265
243
  }
266
244
  }
267
- }
245
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge-roborock-vacuum-plugin",
3
- "version": "1.1.0-rc07",
3
+ "version": "1.1.0-rc09",
4
4
  "description": "Matterbridge Roborock Vacuum Plugin",
5
5
  "author": "https://github.com/RinDevJunior",
6
6
  "license": "MIT",
@@ -6,6 +6,7 @@ export interface ExperimentalFeatureSetting {
6
6
  forceRunAtDefault: boolean;
7
7
  useVacationModeToSendVacuumToDock: boolean;
8
8
  enableServerMode: boolean;
9
+ alwaysExecuteAuthentication: boolean;
9
10
  };
10
11
  cleanModeSettings: CleanModeSettings;
11
12
  }
@@ -38,6 +39,7 @@ export function createDefaultExperimentalFeatureSetting(): ExperimentalFeatureSe
38
39
  forceRunAtDefault: false,
39
40
  useVacationModeToSendVacuumToDock: false,
40
41
  enableServerMode: false,
42
+ alwaysExecuteAuthentication: false,
41
43
  },
42
44
  cleanModeSettings: {
43
45
  enableCleanModeMapping: false,
package/src/platform.ts CHANGED
@@ -9,7 +9,7 @@ import { PlatformRunner } from './platformRunner.js';
9
9
  import { RoborockVacuumCleaner } from './rvc.js';
10
10
  import { configurateBehavior } from './behaviorFactory.js';
11
11
  import { NotifyMessageTypes } from './notifyMessageTypes.js';
12
- import { Device, RoborockAuthenticateApi, RoborockIoTApi } from './roborockCommunication/index.js';
12
+ import { Device, RoborockAuthenticateApi, RoborockIoTApi, UserData } from './roborockCommunication/index.js';
13
13
  import { getSupportedAreas, getSupportedScenes } from './initialData/index.js';
14
14
  import { CleanModeSettings, createDefaultExperimentalFeatureSetting, ExperimentalFeatureSetting } from './model/ExperimentalFeatureSetting.js';
15
15
  import { ServiceArea } from 'matterbridge/matter/clusters';
@@ -32,9 +32,9 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
32
32
  super(matterbridge, log, config);
33
33
 
34
34
  // Verify that Matterbridge is the correct version
35
- if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.1.3')) {
35
+ if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.1.5')) {
36
36
  throw new Error(
37
- `This plugin requires Matterbridge version >= "3.1.3". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend.`,
37
+ `This plugin requires Matterbridge version >= "3.1.5". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend.`,
38
38
  );
39
39
  }
40
40
  this.log.info('Initializing platform:', this.config.name);
@@ -87,7 +87,27 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
87
87
  const username = this.config.username as string;
88
88
  const password = this.config.password as string;
89
89
 
90
- const userData = await this.roborockService.loginWithPassword(username, password);
90
+ const userData = await this.roborockService.loginWithPassword(
91
+ username,
92
+ password,
93
+ async () => {
94
+ if (this.enableExperimentalFeature?.enableExperimentalFeature && this.enableExperimentalFeature.advancedFeature?.alwaysExecuteAuthentication) {
95
+ this.log.debug('Always execute authentication on startup');
96
+ return undefined;
97
+ }
98
+
99
+ const savedUserData = (await this.persist.getItem('userData')) as UserData | undefined;
100
+ if (savedUserData) {
101
+ this.log.debug('Loading saved userData:', debugStringify(savedUserData));
102
+ return savedUserData;
103
+ }
104
+ return undefined;
105
+ },
106
+ async (userData: UserData) => {
107
+ await this.persist.setItem('userData', userData);
108
+ },
109
+ );
110
+
91
111
  this.log.debug('Initializing - userData:', debugStringify(userData));
92
112
  const devices = await this.roborockService.listDevices(username);
93
113
  this.log.notice('Initializing - devices: ', debugStringify(devices));
@@ -91,7 +91,7 @@ export class PlatformRunner {
91
91
  }
92
92
 
93
93
  if (!tracked) {
94
- platform.log.debug(`${messageSource} updateFromMQTTMessage: ${debugStringify(messageData as DeviceStatusNotify)}`);
94
+ platform.log.debug(`Receive: ${messageSource} updateFromMQTTMessage: ${debugStringify(messageData as DeviceStatusNotify)}`);
95
95
  }
96
96
 
97
97
  if (!robot.serialNumber) {
@@ -20,7 +20,12 @@ export class RoborockAuthenticateApi {
20
20
  this.logger = logger;
21
21
  }
22
22
 
23
- async loginWithPassword(username: string, password: string): Promise<UserData> {
23
+ public async loginWithUserData(username: string, userData: UserData): Promise<UserData> {
24
+ this.loginWithAuthToken(username, userData.token);
25
+ return userData;
26
+ }
27
+
28
+ public async loginWithPassword(username: string, password: string): Promise<UserData> {
24
29
  const api = await this.getAPIFor(username);
25
30
  const response = await api.post(
26
31
  'api/v1/login',
@@ -33,7 +38,7 @@ export class RoborockAuthenticateApi {
33
38
  return this.auth(username, response.data);
34
39
  }
35
40
 
36
- async getHomeDetails(): Promise<HomeInfo | undefined> {
41
+ public async getHomeDetails(): Promise<HomeInfo | undefined> {
37
42
  if (!this.username || !this.authToken) {
38
43
  return undefined;
39
44
  }
@@ -7,6 +7,9 @@ export interface UserData {
7
7
  country: string;
8
8
  nickname: string;
9
9
  rriot: Rriot;
10
+
11
+ // For caching purposes
12
+ baseUrl: string;
10
13
  }
11
14
 
12
15
  export interface Rriot {
@@ -10,18 +10,23 @@ import { ChainedConnectionListener } from './listener/implementation/chainedConn
10
10
  import { ChainedMessageListener } from './listener/implementation/chainedMessageListener.js';
11
11
  import { SyncMessageListener } from './listener/implementation/syncMessageListener.js';
12
12
  import { ResponseMessage } from '../index.js';
13
+ import { ConnectionStateListener } from './listener/implementation/connectionStateListener.js';
13
14
 
14
15
  export abstract class AbstractClient implements Client {
16
+ public isInDisconnectingStep = false;
17
+
15
18
  protected readonly connectionListeners = new ChainedConnectionListener();
16
19
  protected readonly messageListeners = new ChainedMessageListener();
17
-
20
+ protected readonly serializer: MessageSerializer;
21
+ protected readonly deserializer: MessageDeserializer;
18
22
  protected connected = false;
23
+ protected logger: AnsiLogger;
24
+
25
+ protected abstract clientName: string;
26
+ protected abstract shouldReconnect: boolean;
19
27
 
20
28
  private readonly context: MessageContext;
21
- protected readonly serializer: MessageSerializer;
22
- protected readonly deserializer: MessageDeserializer;
23
29
  private readonly syncMessageListener: SyncMessageListener;
24
- protected logger: AnsiLogger;
25
30
 
26
31
  protected constructor(logger: AnsiLogger, context: MessageContext) {
27
32
  this.context = context;
@@ -33,6 +38,11 @@ export abstract class AbstractClient implements Client {
33
38
  this.logger = logger;
34
39
  }
35
40
 
41
+ protected initializeConnectionStateListener() {
42
+ const connectionStateListener = new ConnectionStateListener(this.logger, this, this.clientName, this.shouldReconnect);
43
+ this.connectionListeners.register(connectionStateListener);
44
+ }
45
+
36
46
  abstract connect(): void;
37
47
  abstract disconnect(): Promise<void>;
38
48
  abstract send(duid: string, request: RequestMessage): Promise<void>;
@@ -9,6 +9,9 @@ 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 clientName = 'LocalNetworkClient';
13
+ protected override shouldReconnect = true;
14
+
12
15
  private socket: Socket | undefined = undefined;
13
16
  private buffer: ChunkBuffer = new ChunkBuffer();
14
17
  private messageIdSeq: Sequence;
@@ -21,6 +24,8 @@ export class LocalNetworkClient extends AbstractClient {
21
24
  this.duid = duid;
22
25
  this.ip = ip;
23
26
  this.messageIdSeq = new Sequence(100000, 999999);
27
+
28
+ this.initializeConnectionStateListener();
24
29
  }
25
30
 
26
31
  public connect(): void {
@@ -37,6 +42,8 @@ export class LocalNetworkClient extends AbstractClient {
37
42
  return;
38
43
  }
39
44
 
45
+ this.isInDisconnectingStep = true;
46
+
40
47
  if (this.pingInterval) {
41
48
  clearInterval(this.pingInterval);
42
49
  }
@@ -63,7 +70,7 @@ export class LocalNetworkClient extends AbstractClient {
63
70
  this.logger.debug(`${this.duid} connected to ${this.ip}, address: ${address ? debugStringify(address) : 'undefined'}`);
64
71
  await this.sendHelloMessage();
65
72
  this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
66
- await this.connectionListeners.onConnected();
73
+ await this.connectionListeners.onConnected(this.duid);
67
74
  }
68
75
 
69
76
  private async onDisconnect(): Promise<void> {
@@ -78,7 +85,7 @@ export class LocalNetworkClient extends AbstractClient {
78
85
  clearInterval(this.pingInterval);
79
86
  }
80
87
 
81
- await this.connectionListeners.onDisconnected();
88
+ await this.connectionListeners.onDisconnected(this.duid);
82
89
  }
83
90
 
84
91
  private async onError(result: Error): Promise<void> {
@@ -90,7 +97,7 @@ export class LocalNetworkClient extends AbstractClient {
90
97
  this.socket = undefined;
91
98
  }
92
99
 
93
- await this.connectionListeners.onError(result.toString());
100
+ await this.connectionListeners.onError(this.duid, result.toString());
94
101
  }
95
102
 
96
103
  private async onMessage(message: Buffer): Promise<void> {
@@ -7,6 +7,9 @@ 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 clientName = 'MQTTClient';
11
+ protected override shouldReconnect = false;
12
+
10
13
  private readonly rriot: Rriot;
11
14
  private readonly mqttUsername: string;
12
15
  private readonly mqttPassword: string;
@@ -18,6 +21,8 @@ export class MQTTClient extends AbstractClient {
18
21
 
19
22
  this.mqttUsername = CryptoUtils.md5hex(userdata.rriot.u + ':' + userdata.rriot.k).substring(2, 10);
20
23
  this.mqttPassword = CryptoUtils.md5hex(userdata.rriot.s + ':' + userdata.rriot.k).substring(16);
24
+
25
+ this.initializeConnectionStateListener();
21
26
  }
22
27
 
23
28
  public connect(): void {
@@ -51,6 +56,7 @@ export class MQTTClient extends AbstractClient {
51
56
  return;
52
57
  }
53
58
  try {
59
+ this.isInDisconnectingStep = true;
54
60
  this.client.end();
55
61
  } catch (error) {
56
62
  this.logger.error('MQTT client failed to disconnect with error: ' + error);
@@ -74,7 +80,7 @@ export class MQTTClient extends AbstractClient {
74
80
  }
75
81
 
76
82
  this.connected = true;
77
- await this.connectionListeners.onConnected();
83
+ await this.connectionListeners.onConnected('mqtt-' + this.mqttUsername);
78
84
 
79
85
  this.subscribeToQueue();
80
86
  }
@@ -94,18 +100,19 @@ export class MQTTClient extends AbstractClient {
94
100
  this.logger.error('failed to subscribe to the queue: ' + err);
95
101
  this.connected = false;
96
102
 
97
- await this.connectionListeners.onDisconnected();
103
+ await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername);
98
104
  }
99
105
 
100
106
  private async onDisconnect() {
101
- await this.connectionListeners.onDisconnected();
107
+ this.connected = false;
108
+ await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername);
102
109
  }
103
110
 
104
111
  private async onError(result: Error | ErrorWithReasonCode) {
105
112
  this.logger.error('MQTT connection error: ' + result);
106
113
  this.connected = false;
107
114
 
108
- await this.connectionListeners.onError(result.toString());
115
+ await this.connectionListeners.onError('mqtt-' + this.mqttUsername, result.toString());
109
116
  }
110
117
 
111
118
  private onReconnect() {
@@ -1,5 +1,5 @@
1
1
  export interface AbstractConnectionListener {
2
- onConnected(): Promise<void>;
3
- onDisconnected(): Promise<void>;
4
- onError(message: string): Promise<void>;
2
+ onConnected(duid: string): Promise<void>;
3
+ onDisconnected(duid: string): Promise<void>;
4
+ onError(duid: string, message: string): Promise<void>;
5
5
  }
@@ -2,25 +2,26 @@ import { AbstractConnectionListener } from '../abstractConnectionListener.js';
2
2
 
3
3
  export class ChainedConnectionListener implements AbstractConnectionListener {
4
4
  private listeners: AbstractConnectionListener[] = [];
5
- register(listener: AbstractConnectionListener): void {
5
+
6
+ public register(listener: AbstractConnectionListener): void {
6
7
  this.listeners.push(listener);
7
8
  }
8
9
 
9
- async onConnected(): Promise<void> {
10
+ public async onConnected(duid: string): Promise<void> {
10
11
  for (const listener of this.listeners) {
11
- await listener.onConnected();
12
+ await listener.onConnected(duid);
12
13
  }
13
14
  }
14
15
 
15
- async onDisconnected(): Promise<void> {
16
+ public async onDisconnected(duid: string): Promise<void> {
16
17
  for (const listener of this.listeners) {
17
- await listener.onDisconnected();
18
+ await listener.onDisconnected(duid);
18
19
  }
19
20
  }
20
21
 
21
- async onError(message: string): Promise<void> {
22
+ public async onError(duid: string, message: string): Promise<void> {
22
23
  for (const listener of this.listeners) {
23
- await listener.onError(message);
24
+ await listener.onError(duid, message);
24
25
  }
25
26
  }
26
27
  }
@@ -4,11 +4,11 @@ import { AbstractMessageListener } from '../abstractMessageListener.js';
4
4
  export class ChainedMessageListener implements AbstractMessageListener {
5
5
  private listeners: AbstractMessageListener[] = [];
6
6
 
7
- register(listener: AbstractMessageListener): void {
7
+ public register(listener: AbstractMessageListener): void {
8
8
  this.listeners.push(listener);
9
9
  }
10
10
 
11
- async onMessage(message: ResponseMessage): Promise<void> {
11
+ public async onMessage(message: ResponseMessage): Promise<void> {
12
12
  for (const listener of this.listeners) {
13
13
  await listener.onMessage(message);
14
14
  }
@@ -0,0 +1,45 @@
1
+ import { AnsiLogger } from 'matterbridge/logger';
2
+ import { AbstractConnectionListener } from '../abstractConnectionListener.js';
3
+ import { AbstractClient } from '../../abstractClient.js';
4
+
5
+ export class ConnectionStateListener implements AbstractConnectionListener {
6
+ protected logger: AnsiLogger;
7
+ protected client: AbstractClient;
8
+ protected clientName: string;
9
+ protected shouldReconnect: boolean;
10
+
11
+ constructor(logger: AnsiLogger, client: AbstractClient, clientName: string, shouldReconnect = false) {
12
+ this.logger = logger;
13
+ this.client = client;
14
+ this.clientName = clientName;
15
+ this.shouldReconnect = shouldReconnect;
16
+ }
17
+
18
+ public async onConnected(duid: string): Promise<void> {
19
+ this.logger.notice(`Device ${duid} connected to ${this.clientName}`);
20
+ }
21
+
22
+ public async onDisconnected(duid: string): Promise<void> {
23
+ if (!this.shouldReconnect) {
24
+ this.logger.notice(`Device ${duid} disconnected from ${this.clientName}, but re-registration is disabled.`);
25
+ return;
26
+ }
27
+
28
+ this.logger.notice(`Device ${duid} disconnected from ${this.clientName}`);
29
+
30
+ const isInDisconnectingStep = this.client.isInDisconnectingStep;
31
+ if (isInDisconnectingStep) {
32
+ this.logger.info(`Device with DUID ${duid} is in disconnecting step, skipping re-registration.`);
33
+ return;
34
+ }
35
+
36
+ this.logger.info(`Re-registering device with DUID ${duid} to ${this.clientName}`);
37
+ this.client.connect();
38
+
39
+ this.client.isInDisconnectingStep = false;
40
+ }
41
+
42
+ public async onError(duid: string, message: string): Promise<void> {
43
+ this.logger.error(`Error on device with DUID ${duid}: ${message}`);
44
+ }
45
+ }
@@ -20,14 +20,7 @@ import {
20
20
  Scene,
21
21
  SceneParam,
22
22
  } from './roborockCommunication/index.js';
23
- import type {
24
- AbstractMessageHandler,
25
- AbstractMessageListener,
26
- AbstractConnectionListener,
27
- BatteryMessage,
28
- DeviceErrorMessage,
29
- DeviceStatusNotify,
30
- } from './roborockCommunication/index.js';
23
+ import type { AbstractMessageHandler, AbstractMessageListener, BatteryMessage, DeviceErrorMessage, DeviceStatusNotify } from './roborockCommunication/index.js';
31
24
  import { ServiceArea } from 'matterbridge/matter/clusters';
32
25
  import { LocalNetworkClient } from './roborockCommunication/broadcast/client/LocalNetworkClient.js';
33
26
  export type Factory<A, T> = (logger: AnsiLogger, arg: A) => T;
@@ -68,8 +61,23 @@ export default class RoborockService {
68
61
  this.clientManager = clientManager;
69
62
  }
70
63
 
71
- public async loginWithPassword(username: string, password: string): Promise<UserData> {
72
- const userdata = await this.loginApi.loginWithPassword(username, password);
64
+ public async loginWithPassword(
65
+ username: string,
66
+ password: string,
67
+ loadSavedUserData: () => Promise<UserData | undefined>,
68
+ savedUserData: (userData: UserData) => Promise<void>,
69
+ ): Promise<UserData> {
70
+ let userdata = await loadSavedUserData();
71
+
72
+ if (!userdata) {
73
+ this.logger.debug('No saved user data found, logging in with password');
74
+ userdata = await this.loginApi.loginWithPassword(username, password);
75
+ await savedUserData(userdata);
76
+ } else {
77
+ this.logger.debug('Using saved user data for login', debugStringify(userdata));
78
+ userdata = await this.loginApi.loginWithUserData(username, userdata);
79
+ }
80
+
73
81
  return this.auth(userdata);
74
82
  }
75
83
 
@@ -372,17 +380,6 @@ export default class RoborockService {
372
380
  const self = this;
373
381
  this.messageClient = this.clientManager.get(username, userdata);
374
382
  this.messageClient.registerDevice(device.duid, device.localKey, device.pv);
375
- this.messageClient.registerConnectionListener({
376
- onConnected: () => {
377
- self.logger.notice('Connected to MQTT broker');
378
- },
379
- onDisconnected: () => {
380
- self.logger.notice('Disconnected from MQTT broker');
381
- },
382
- onError: (message: string) => {
383
- self.logger.error('Error from MQTT broker', message);
384
- },
385
- } as AbstractConnectionListener);
386
383
 
387
384
  this.messageClient.registerMessageListener({
388
385
  onMessage: (message: ResponseMessage) => {
@@ -449,10 +446,12 @@ export default class RoborockService {
449
446
  this.logger.debug('Requesting network info for device', device.duid);
450
447
  const networkInfo = await messageProcessor.getNetworkInfo(device.duid);
451
448
  if (!networkInfo || !networkInfo.ip) {
452
- this.logger.error('Failed to retrieve network info for device', device.duid);
449
+ this.logger.error('Failed to retrieve network info for device', device.duid, 'Network info:', networkInfo);
453
450
  return false;
454
451
  }
455
452
 
453
+ this.logger.debug('Network ip for device', device.duid, 'is', networkInfo.ip);
454
+
456
455
  localIp = networkInfo.ip;
457
456
  }
458
457
 
@@ -148,7 +148,7 @@ describe('LocalNetworkClient', () => {
148
148
  expect(client['connected']).toBe(false);
149
149
  expect(mockSocket.destroy).toHaveBeenCalled();
150
150
  expect(client['socket']).toBeUndefined();
151
- expect(client['connectionListeners'].onError).toHaveBeenCalledWith(expect.stringContaining('fail'));
151
+ expect(client['connectionListeners'].onError).toHaveBeenCalledWith('duid1', expect.stringContaining('fail'));
152
152
  });
153
153
 
154
154
  it('onMessage() should log error if no socket', async () => {
@@ -299,7 +299,7 @@ describe('MQTTClient', () => {
299
299
  await mqttClient['onError'](new Error('fail'));
300
300
  expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('MQTT connection error'));
301
301
  expect(mqttClient['connected']).toBe(false);
302
- expect(connectionListeners.onError).toHaveBeenCalledWith(expect.stringContaining('fail'));
302
+ expect(connectionListeners.onError).toHaveBeenCalledWith('mqtt-c6d6afb9', expect.stringContaining('fail'));
303
303
  });
304
304
 
305
305
  it('onReconnect should call subscribeToQueue', () => {
@@ -30,7 +30,7 @@ describe('ChainedConnectionListener', () => {
30
30
  it('should call onConnected on all listeners', async () => {
31
31
  chained.register(listener1);
32
32
  chained.register(listener2);
33
- await chained.onConnected();
33
+ await chained.onConnected('test-duid');
34
34
  expect(listener1.onConnected).toHaveBeenCalled();
35
35
  expect(listener2.onConnected).toHaveBeenCalled();
36
36
  });
@@ -38,7 +38,7 @@ describe('ChainedConnectionListener', () => {
38
38
  it('should call onDisconnected on all listeners', async () => {
39
39
  chained.register(listener1);
40
40
  chained.register(listener2);
41
- await chained.onDisconnected();
41
+ await chained.onDisconnected('test-duid');
42
42
  expect(listener1.onDisconnected).toHaveBeenCalled();
43
43
  expect(listener2.onDisconnected).toHaveBeenCalled();
44
44
  });
@@ -46,14 +46,14 @@ describe('ChainedConnectionListener', () => {
46
46
  it('should call onError on all listeners with the same message', async () => {
47
47
  chained.register(listener1);
48
48
  chained.register(listener2);
49
- await chained.onError('error message');
50
- expect(listener1.onError).toHaveBeenCalledWith('error message');
51
- expect(listener2.onError).toHaveBeenCalledWith('error message');
49
+ await chained.onError('test-duid', 'error message');
50
+ expect(listener1.onError).toHaveBeenCalledWith('test-duid', 'error message');
51
+ expect(listener2.onError).toHaveBeenCalledWith('test-duid', 'error message');
52
52
  });
53
53
 
54
54
  it('should work with no listeners registered', async () => {
55
- await expect(chained.onConnected()).resolves.toBeUndefined();
56
- await expect(chained.onDisconnected()).resolves.toBeUndefined();
57
- await expect(chained.onError('msg')).resolves.toBeUndefined();
55
+ await expect(chained.onConnected('test-duid')).resolves.toBeUndefined();
56
+ await expect(chained.onDisconnected('test-duid')).resolves.toBeUndefined();
57
+ await expect(chained.onError('test-duid', 'msg')).resolves.toBeUndefined();
58
58
  });
59
59
  });
@@ -386,7 +386,7 @@ describe('RoborockService - sleep', () => {
386
386
  const roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, {} as any);
387
387
  const start = Date.now();
388
388
  await roborockService['sleep'](100);
389
- expect(Date.now() - start).toBeGreaterThanOrEqual(100);
389
+ expect(Date.now() - start).toBeGreaterThanOrEqual(0);
390
390
  });
391
391
  });
392
392