matterbridge-roborock-vacuum-plugin 1.1.0-rc03 → 1.1.0-rc04

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.
@@ -13,6 +13,11 @@ jobs:
13
13
  os: [ubuntu-latest, windows-latest, macos-latest]
14
14
 
15
15
  steps:
16
+ - name: Cancel Previous Runs
17
+ uses: styfle/cancel-workflow-action@0.12.0
18
+ with:
19
+ access_token: ${{ github.token }}
20
+
16
21
  - name: Checkout code
17
22
  uses: actions/checkout@v4
18
23
 
@@ -7,6 +7,11 @@ jobs:
7
7
  runs-on: ubuntu-latest
8
8
 
9
9
  steps:
10
+ - name: Cancel Previous Runs
11
+ uses: styfle/cancel-workflow-action@0.12.0
12
+ with:
13
+ access_token: ${{ github.token }}
14
+
10
15
  - name: Checkout code
11
16
  uses: actions/checkout@v4
12
17
 
@@ -12,9 +12,9 @@ export default class RoborockService {
12
12
  deviceNotify;
13
13
  messageClient;
14
14
  remoteDevices = new Set();
15
- messageProcessor;
16
- ip;
17
- localClient;
15
+ messageProcessorMap = new Map();
16
+ ipMap = new Map();
17
+ localClientMap = new Map();
18
18
  clientManager;
19
19
  refreshInterval;
20
20
  requestDeviceStatusInterval;
@@ -32,11 +32,12 @@ export default class RoborockService {
32
32
  const userdata = await this.loginApi.loginWithPassword(username, password);
33
33
  return this.auth(userdata);
34
34
  }
35
- getMessageProcessor() {
36
- if (!this.messageProcessor) {
35
+ getMessageProcessor(duid) {
36
+ const messageProcessor = this.messageProcessorMap.get(duid);
37
+ if (!messageProcessor) {
37
38
  this.logger.error('MessageApi is not initialized.');
38
39
  }
39
- return this.messageProcessor;
40
+ return messageProcessor;
40
41
  }
41
42
  setSelectedAreas(duid, selectedAreas) {
42
43
  this.logger.debug('RoborockService - setSelectedAreas', selectedAreas);
@@ -53,7 +54,7 @@ export default class RoborockService {
53
54
  }
54
55
  async getCleanModeData(duid) {
55
56
  this.logger.notice('RoborockService - getCleanModeData');
56
- const data = await this.messageProcessor?.getCleanModeData(duid);
57
+ const data = await this.getMessageProcessor(duid)?.getCleanModeData(duid);
57
58
  if (!data) {
58
59
  throw new Error('Failed to retrieve clean mode data');
59
60
  }
@@ -61,7 +62,7 @@ export default class RoborockService {
61
62
  }
62
63
  async changeCleanMode(duid, { suctionPower, waterFlow, distance_off, mopRoute }) {
63
64
  this.logger.notice('RoborockService - changeCleanMode');
64
- return this.messageProcessor?.changeCleanMode(duid, suctionPower, waterFlow, mopRoute, distance_off);
65
+ return this.getMessageProcessor(duid)?.changeCleanMode(duid, suctionPower, waterFlow, mopRoute, distance_off);
65
66
  }
66
67
  async startClean(duid) {
67
68
  const supportedRooms = this.supportedAreas.get(duid) ?? [];
@@ -71,11 +72,11 @@ export default class RoborockService {
71
72
  if (supportedRoutines.length === 0) {
72
73
  if (selected.length == supportedRooms.length || selected.length === 0 || supportedRooms.length === 0) {
73
74
  this.logger.debug('RoborockService - startGlobalClean');
74
- this.getMessageProcessor()?.startClean(duid);
75
+ this.getMessageProcessor(duid)?.startClean(duid);
75
76
  }
76
77
  else {
77
78
  this.logger.debug('RoborockService - startRoomClean', debugStringify({ duid, selected }));
78
- return this.messageProcessor?.startRoomClean(duid, selected, 1);
79
+ return this.getMessageProcessor(duid)?.startRoomClean(duid, selected, 1);
79
80
  }
80
81
  }
81
82
  else {
@@ -91,11 +92,11 @@ export default class RoborockService {
91
92
  }
92
93
  else if (rooms.length == supportedRooms.length || rooms.length === 0 || supportedRooms.length === 0) {
93
94
  this.logger.debug('RoborockService - startGlobalClean');
94
- this.getMessageProcessor()?.startClean(duid);
95
+ this.getMessageProcessor(duid)?.startClean(duid);
95
96
  }
96
97
  else if (rooms.length > 0) {
97
98
  this.logger.debug('RoborockService - startRoomClean', debugStringify({ duid, rooms }));
98
- return this.messageProcessor?.startRoomClean(duid, rooms, 1);
99
+ return this.getMessageProcessor(duid)?.startRoomClean(duid, rooms, 1);
99
100
  }
100
101
  else {
101
102
  this.logger.warn('RoborockService - something goes wrong.', debugStringify({ duid, rooms, rt, selected, supportedRooms, supportedRoutines }));
@@ -105,42 +106,50 @@ export default class RoborockService {
105
106
  }
106
107
  async pauseClean(duid) {
107
108
  this.logger.debug('RoborockService - pauseClean');
108
- await this.getMessageProcessor()?.pauseClean(duid);
109
+ await this.getMessageProcessor(duid)?.pauseClean(duid);
109
110
  }
110
111
  async stopAndGoHome(duid) {
111
112
  this.logger.debug('RoborockService - stopAndGoHome');
112
- await this.getMessageProcessor()?.gotoDock(duid);
113
+ await this.getMessageProcessor(duid)?.gotoDock(duid);
113
114
  }
114
115
  async resumeClean(duid) {
115
116
  this.logger.debug('RoborockService - resumeClean');
116
- await this.getMessageProcessor()?.resumeClean(duid);
117
+ await this.getMessageProcessor(duid)?.resumeClean(duid);
117
118
  }
118
119
  async playSoundToLocate(duid) {
119
120
  this.logger.debug('RoborockService - findMe');
120
- await this.getMessageProcessor()?.findMyRobot(duid);
121
+ await this.getMessageProcessor(duid)?.findMyRobot(duid);
121
122
  }
122
123
  async customGet(duid, method) {
123
124
  this.logger.debug('RoborockService - customSend-message', method);
124
- return this.getMessageProcessor()?.getCustomMessage(duid, new RequestMessage({ method }));
125
+ return this.getMessageProcessor(duid)?.getCustomMessage(duid, new RequestMessage({ method }));
125
126
  }
126
127
  async customGetInSecure(duid, method) {
127
128
  this.logger.debug('RoborockService - customGetInSecure-message', method);
128
- return this.getMessageProcessor()?.getCustomMessage(duid, new RequestMessage({ method, secure: true }));
129
+ return this.getMessageProcessor(duid)?.getCustomMessage(duid, new RequestMessage({ method, secure: true }));
129
130
  }
130
131
  async customSend(duid, request) {
131
- return this.getMessageProcessor()?.sendCustomMessage(duid, request);
132
+ return this.getMessageProcessor(duid)?.sendCustomMessage(duid, request);
132
133
  }
133
134
  stopService() {
134
135
  if (this.messageClient) {
135
136
  this.messageClient.disconnect();
136
137
  this.messageClient = undefined;
137
138
  }
138
- if (this.localClient) {
139
- this.localClient.disconnect();
140
- this.localClient = undefined;
139
+ if (this.localClientMap.size > 0) {
140
+ for (const [duid, client] of this.localClientMap.entries()) {
141
+ this.logger.debug('Disconnecting local client for device', duid);
142
+ client.disconnect();
143
+ this.localClientMap.delete(duid);
144
+ this.logger.debug('Local client disconnected for device', duid);
145
+ }
141
146
  }
142
- if (this.messageProcessor) {
143
- this.messageProcessor = undefined;
147
+ if (this.messageProcessorMap.size > 0) {
148
+ for (const [duid] of this.messageProcessorMap.entries()) {
149
+ this.logger.debug('Disconnecting message processor for device', duid);
150
+ this.messageProcessorMap.delete(duid);
151
+ this.logger.debug('Message processor disconnected for device', duid);
152
+ }
144
153
  }
145
154
  if (this.requestDeviceStatusInterval) {
146
155
  clearInterval(this.requestDeviceStatusInterval);
@@ -153,9 +162,10 @@ export default class RoborockService {
153
162
  async activateDeviceNotify(device) {
154
163
  const self = this;
155
164
  this.logger.debug('Requesting device info for device', device.duid);
165
+ const messageProcessor = this.getMessageProcessor(device.duid);
156
166
  this.requestDeviceStatusInterval = setInterval(async () => {
157
- if (this.messageProcessor) {
158
- await this.messageProcessor.getDeviceStatus(device.duid).then((response) => {
167
+ if (messageProcessor) {
168
+ await messageProcessor.getDeviceStatus(device.duid).then((response) => {
159
169
  if (self.deviceNotify) {
160
170
  const message = { duid: device.duid, ...response.errorStatus, ...response.message };
161
171
  self.logger.debug('Device status update', debugStringify(message));
@@ -307,9 +317,9 @@ export default class RoborockService {
307
317
  return;
308
318
  }
309
319
  const self = this;
310
- this.messageProcessor = new MessageProcessor(this.messageClient);
311
- this.messageProcessor.injectLogger(this.logger);
312
- this.messageProcessor.registerListener({
320
+ const messageProcessor = new MessageProcessor(this.messageClient);
321
+ messageProcessor.injectLogger(this.logger);
322
+ messageProcessor.registerListener({
313
323
  onError: (message) => {
314
324
  if (self.deviceNotify) {
315
325
  self.deviceNotify(NotifyMessageTypes.ErrorOccurred, { duid: device.duid, errorCode: message });
@@ -323,26 +333,30 @@ export default class RoborockService {
323
333
  onStatusChanged: () => {
324
334
  },
325
335
  });
336
+ this.messageProcessorMap.set(device.duid, messageProcessor);
326
337
  this.logger.debug('Local device', device.duid);
338
+ let localIp = this.ipMap.get(device.duid);
327
339
  try {
328
- if (!this.ip) {
340
+ if (!localIp) {
329
341
  this.logger.debug('Requesting network info for device', device.duid);
330
- const networkInfo = await this.messageProcessor.getNetworkInfo(device.duid);
331
- this.ip = networkInfo.ip;
342
+ const networkInfo = await messageProcessor.getNetworkInfo(device.duid);
343
+ localIp = networkInfo.ip;
332
344
  }
333
- if (this.ip) {
334
- this.logger.debug('initializing the local connection for this client towards ' + this.ip);
335
- this.localClient = this.messageClient.registerClient(device.duid, this.ip);
336
- this.localClient.connect();
345
+ if (localIp) {
346
+ this.logger.debug('initializing the local connection for this client towards ' + localIp);
347
+ const localClient = this.messageClient.registerClient(device.duid, localIp);
348
+ localClient.connect();
337
349
  let count = 0;
338
- while (!this.localClient.isConnected() && count < 20) {
350
+ while (!localClient.isConnected() && count < 20) {
339
351
  this.logger.debug('Keep waiting for local client to connect');
340
352
  count++;
341
- await this.sleep(500);
353
+ await this.sleep(200);
342
354
  }
343
- if (!this.localClient.isConnected()) {
355
+ if (!localClient.isConnected()) {
344
356
  throw new Error('Local client did not connect after 10 attempts, something is wrong');
345
357
  }
358
+ this.ipMap.set(device.duid, localIp);
359
+ this.localClientMap.set(device.duid, localClient);
346
360
  this.logger.debug('LocalClient connected');
347
361
  }
348
362
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "matterbridge-roborock-vacuum-plugin",
3
3
  "type": "DynamicPlatform",
4
- "version": "1.1.0-rc03",
4
+ "version": "1.1.0-rc04",
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-rc03 by https://github.com/RinDevJunior",
3
+ "description": "matterbridge-roborock-vacuum-plugin v. 1.1.0-rc04 by https://github.com/RinDevJunior",
4
4
  "type": "object",
5
5
  "required": [
6
6
  "username",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge-roborock-vacuum-plugin",
3
- "version": "1.1.0-rc03",
3
+ "version": "1.1.0-rc04",
4
4
  "description": "Matterbridge Roborock Vacuum Plugin",
5
5
  "author": "https://github.com/RinDevJunior",
6
6
  "license": "MIT",
package/src/platform.ts CHANGED
@@ -179,6 +179,7 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
179
179
  this.log.info('onConfigurateDevice finished');
180
180
  }
181
181
 
182
+ // Running in loop to configurate devices
182
183
  private async configurateDevice(vacuum: Device) {
183
184
  const username = this.config.username as string;
184
185
 
@@ -42,9 +42,9 @@ export default class RoborockService {
42
42
  deviceNotify?: (messageSource: NotifyMessageTypes, homeData: unknown) => void;
43
43
  messageClient: ClientRouter | undefined;
44
44
  remoteDevices = new Set<string>();
45
- messageProcessor: MessageProcessor | undefined;
46
- ip: string | undefined;
47
- localClient: Client | undefined;
45
+ messageProcessorMap = new Map<string, MessageProcessor>();
46
+ ipMap = new Map<string, string>();
47
+ localClientMap = new Map<string, Client>();
48
48
  clientManager: ClientManager;
49
49
  refreshInterval: number;
50
50
  requestDeviceStatusInterval: NodeJS.Timeout | undefined;
@@ -73,11 +73,12 @@ export default class RoborockService {
73
73
  return this.auth(userdata);
74
74
  }
75
75
 
76
- public getMessageProcessor(): MessageProcessor | undefined {
77
- if (!this.messageProcessor) {
76
+ public getMessageProcessor(duid: string): MessageProcessor | undefined {
77
+ const messageProcessor = this.messageProcessorMap.get(duid);
78
+ if (!messageProcessor) {
78
79
  this.logger.error('MessageApi is not initialized.');
79
80
  }
80
- return this.messageProcessor;
81
+ return messageProcessor;
81
82
  }
82
83
 
83
84
  public setSelectedAreas(duid: string, selectedAreas: number[]): void {
@@ -99,7 +100,7 @@ export default class RoborockService {
99
100
 
100
101
  public async getCleanModeData(duid: string): Promise<{ suctionPower: number; waterFlow: number; distance_off: number; mopRoute: number }> {
101
102
  this.logger.notice('RoborockService - getCleanModeData');
102
- const data = await this.messageProcessor?.getCleanModeData(duid);
103
+ const data = await this.getMessageProcessor(duid)?.getCleanModeData(duid);
103
104
  if (!data) {
104
105
  throw new Error('Failed to retrieve clean mode data');
105
106
  }
@@ -111,7 +112,7 @@ export default class RoborockService {
111
112
  { suctionPower, waterFlow, distance_off, mopRoute }: { suctionPower: number; waterFlow: number; distance_off: number; mopRoute: number },
112
113
  ): Promise<void> {
113
114
  this.logger.notice('RoborockService - changeCleanMode');
114
- return this.messageProcessor?.changeCleanMode(duid, suctionPower, waterFlow, mopRoute, distance_off);
115
+ return this.getMessageProcessor(duid)?.changeCleanMode(duid, suctionPower, waterFlow, mopRoute, distance_off);
115
116
  }
116
117
 
117
118
  public async startClean(duid: string): Promise<void> {
@@ -123,10 +124,10 @@ export default class RoborockService {
123
124
  if (supportedRoutines.length === 0) {
124
125
  if (selected.length == supportedRooms.length || selected.length === 0 || supportedRooms.length === 0) {
125
126
  this.logger.debug('RoborockService - startGlobalClean');
126
- this.getMessageProcessor()?.startClean(duid);
127
+ this.getMessageProcessor(duid)?.startClean(duid);
127
128
  } else {
128
129
  this.logger.debug('RoborockService - startRoomClean', debugStringify({ duid, selected }));
129
- return this.messageProcessor?.startRoomClean(duid, selected, 1);
130
+ return this.getMessageProcessor(duid)?.startRoomClean(duid, selected, 1);
130
131
  }
131
132
  } else {
132
133
  const rooms = selected.filter((slt) => supportedRooms.some((a: ServiceArea.Area) => a.areaId == slt));
@@ -146,13 +147,13 @@ export default class RoborockService {
146
147
  * If no rooms are selected, or all selected rooms match the supported rooms,
147
148
  */
148
149
  this.logger.debug('RoborockService - startGlobalClean');
149
- this.getMessageProcessor()?.startClean(duid);
150
+ this.getMessageProcessor(duid)?.startClean(duid);
150
151
  } else if (rooms.length > 0) {
151
152
  /**
152
153
  * If there are rooms selected
153
154
  */
154
155
  this.logger.debug('RoborockService - startRoomClean', debugStringify({ duid, rooms }));
155
- return this.messageProcessor?.startRoomClean(duid, rooms, 1);
156
+ return this.getMessageProcessor(duid)?.startRoomClean(duid, rooms, 1);
156
157
  } else {
157
158
  this.logger.warn('RoborockService - something goes wrong.', debugStringify({ duid, rooms, rt, selected, supportedRooms, supportedRoutines }));
158
159
  return;
@@ -162,36 +163,36 @@ export default class RoborockService {
162
163
 
163
164
  public async pauseClean(duid: string): Promise<void> {
164
165
  this.logger.debug('RoborockService - pauseClean');
165
- await this.getMessageProcessor()?.pauseClean(duid);
166
+ await this.getMessageProcessor(duid)?.pauseClean(duid);
166
167
  }
167
168
 
168
169
  public async stopAndGoHome(duid: string): Promise<void> {
169
170
  this.logger.debug('RoborockService - stopAndGoHome');
170
- await this.getMessageProcessor()?.gotoDock(duid);
171
+ await this.getMessageProcessor(duid)?.gotoDock(duid);
171
172
  }
172
173
 
173
174
  public async resumeClean(duid: string): Promise<void> {
174
175
  this.logger.debug('RoborockService - resumeClean');
175
- await this.getMessageProcessor()?.resumeClean(duid);
176
+ await this.getMessageProcessor(duid)?.resumeClean(duid);
176
177
  }
177
178
 
178
179
  public async playSoundToLocate(duid: string): Promise<void> {
179
180
  this.logger.debug('RoborockService - findMe');
180
- await this.getMessageProcessor()?.findMyRobot(duid);
181
+ await this.getMessageProcessor(duid)?.findMyRobot(duid);
181
182
  }
182
183
 
183
184
  public async customGet(duid: string, method: string): Promise<unknown> {
184
185
  this.logger.debug('RoborockService - customSend-message', method);
185
- return this.getMessageProcessor()?.getCustomMessage(duid, new RequestMessage({ method }));
186
+ return this.getMessageProcessor(duid)?.getCustomMessage(duid, new RequestMessage({ method }));
186
187
  }
187
188
 
188
189
  public async customGetInSecure(duid: string, method: string): Promise<unknown> {
189
190
  this.logger.debug('RoborockService - customGetInSecure-message', method);
190
- return this.getMessageProcessor()?.getCustomMessage(duid, new RequestMessage({ method, secure: true }));
191
+ return this.getMessageProcessor(duid)?.getCustomMessage(duid, new RequestMessage({ method, secure: true }));
191
192
  }
192
193
 
193
194
  public async customSend(duid: string, request: RequestMessage): Promise<void> {
194
- return this.getMessageProcessor()?.sendCustomMessage(duid, request);
195
+ return this.getMessageProcessor(duid)?.sendCustomMessage(duid, request);
195
196
  }
196
197
 
197
198
  public stopService(): void {
@@ -200,13 +201,21 @@ export default class RoborockService {
200
201
  this.messageClient = undefined;
201
202
  }
202
203
 
203
- if (this.localClient) {
204
- this.localClient.disconnect();
205
- this.localClient = undefined;
204
+ if (this.localClientMap.size > 0) {
205
+ for (const [duid, client] of this.localClientMap.entries()) {
206
+ this.logger.debug('Disconnecting local client for device', duid);
207
+ client.disconnect();
208
+ this.localClientMap.delete(duid);
209
+ this.logger.debug('Local client disconnected for device', duid);
210
+ }
206
211
  }
207
212
 
208
- if (this.messageProcessor) {
209
- this.messageProcessor = undefined;
213
+ if (this.messageProcessorMap.size > 0) {
214
+ for (const [duid] of this.messageProcessorMap.entries()) {
215
+ this.logger.debug('Disconnecting message processor for device', duid);
216
+ this.messageProcessorMap.delete(duid);
217
+ this.logger.debug('Message processor disconnected for device', duid);
218
+ }
210
219
  }
211
220
 
212
221
  if (this.requestDeviceStatusInterval) {
@@ -223,9 +232,10 @@ export default class RoborockService {
223
232
  // eslint-disable-next-line @typescript-eslint/no-this-alias
224
233
  const self = this;
225
234
  this.logger.debug('Requesting device info for device', device.duid);
235
+ const messageProcessor = this.getMessageProcessor(device.duid);
226
236
  this.requestDeviceStatusInterval = setInterval(async () => {
227
- if (this.messageProcessor) {
228
- await this.messageProcessor.getDeviceStatus(device.duid).then((response: DeviceStatus) => {
237
+ if (messageProcessor) {
238
+ await messageProcessor.getDeviceStatus(device.duid).then((response: DeviceStatus) => {
229
239
  if (self.deviceNotify) {
230
240
  const message = { duid: device.duid, ...response.errorStatus, ...response.message } as DeviceStatusNotify;
231
241
  self.logger.debug('Device status update', debugStringify(message));
@@ -407,9 +417,9 @@ export default class RoborockService {
407
417
  // eslint-disable-next-line @typescript-eslint/no-this-alias
408
418
  const self = this;
409
419
 
410
- this.messageProcessor = new MessageProcessor(this.messageClient);
411
- this.messageProcessor.injectLogger(this.logger);
412
- this.messageProcessor.registerListener({
420
+ const messageProcessor = new MessageProcessor(this.messageClient);
421
+ messageProcessor.injectLogger(this.logger);
422
+ messageProcessor.registerListener({
413
423
  onError: (message: VacuumErrorCode) => {
414
424
  if (self.deviceNotify) {
415
425
  self.deviceNotify(NotifyMessageTypes.ErrorOccurred, { duid: device.duid, errorCode: message } as DeviceErrorMessage);
@@ -430,30 +440,35 @@ export default class RoborockService {
430
440
  },
431
441
  } as AbstractMessageHandler);
432
442
 
443
+ this.messageProcessorMap.set(device.duid, messageProcessor);
444
+
433
445
  this.logger.debug('Local device', device.duid);
446
+ let localIp = this.ipMap.get(device.duid);
434
447
  try {
435
- if (!this.ip) {
448
+ if (!localIp) {
436
449
  this.logger.debug('Requesting network info for device', device.duid);
437
- const networkInfo = await this.messageProcessor.getNetworkInfo(device.duid);
438
- this.ip = networkInfo.ip;
450
+ const networkInfo = await messageProcessor.getNetworkInfo(device.duid);
451
+ localIp = networkInfo.ip;
439
452
  }
440
453
 
441
- if (this.ip) {
442
- this.logger.debug('initializing the local connection for this client towards ' + this.ip);
443
- this.localClient = this.messageClient.registerClient(device.duid, this.ip) as LocalNetworkClient;
444
- this.localClient.connect();
454
+ if (localIp) {
455
+ this.logger.debug('initializing the local connection for this client towards ' + localIp);
456
+ const localClient = this.messageClient.registerClient(device.duid, localIp) as LocalNetworkClient;
457
+ localClient.connect();
445
458
 
446
459
  let count = 0;
447
- while (!this.localClient.isConnected() && count < 20) {
460
+ while (!localClient.isConnected() && count < 20) {
448
461
  this.logger.debug('Keep waiting for local client to connect');
449
462
  count++;
450
- await this.sleep(500);
463
+ await this.sleep(200);
451
464
  }
452
465
 
453
- if (!this.localClient.isConnected()) {
466
+ if (!localClient.isConnected()) {
454
467
  throw new Error('Local client did not connect after 10 attempts, something is wrong');
455
468
  }
456
469
 
470
+ this.ipMap.set(device.duid, localIp);
471
+ this.localClientMap.set(device.duid, localClient);
457
472
  this.logger.debug('LocalClient connected');
458
473
  }
459
474
  } catch (error) {
@@ -22,7 +22,7 @@ describe('RoborockService - startClean', () => {
22
22
  } as any;
23
23
 
24
24
  roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
25
- roborockService['messageProcessor'] = mockMessageProcessor;
25
+ roborockService['messageProcessorMap'] = new Map<string, MessageProcessor>([['test-duid', mockMessageProcessor]]);
26
26
  });
27
27
 
28
28
  it('should start global clean when no areas or selected areas are provided', async () => {
@@ -153,3 +153,243 @@ describe('RoborockService - startClean', () => {
153
153
  expect(mockMessageProcessor.startRoomClean).toHaveBeenCalledWith(duid, [1, 3], 1);
154
154
  });
155
155
  });
156
+ describe('RoborockService - basic setters/getters', () => {
157
+ let roborockService: RoborockService;
158
+ let mockLogger: AnsiLogger;
159
+
160
+ beforeEach(() => {
161
+ mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
162
+ roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
163
+ });
164
+
165
+ it('setSelectedAreas should set selected areas', () => {
166
+ roborockService.setSelectedAreas('duid', [1, 2]);
167
+ expect(roborockService['selectedAreas'].get('duid')).toEqual([1, 2]);
168
+ expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - setSelectedAreas', [1, 2]);
169
+ });
170
+
171
+ it('setSupportedAreas should set supported areas', () => {
172
+ const areas = [{ areaId: 1, mapId: null, areaInfo: {} as any }];
173
+ roborockService.setSupportedAreas('duid', areas);
174
+ expect(roborockService['supportedAreas'].get('duid')).toEqual(areas);
175
+ });
176
+
177
+ it('setSupportedScenes should set supported routines', () => {
178
+ const routines = [{ areaId: 99, mapId: null, areaInfo: {} as any }];
179
+ roborockService.setSupportedScenes('duid', routines);
180
+ expect(roborockService['supportedRoutines'].get('duid')).toEqual(routines);
181
+ });
182
+
183
+ it('getSupportedAreas should return supported areas', () => {
184
+ const areas = [{ areaId: 1, mapId: null, areaInfo: {} as any }];
185
+ roborockService['supportedAreas'].set('duid', areas);
186
+ expect(roborockService.getSupportedAreas('duid')).toEqual(areas);
187
+ });
188
+ });
189
+
190
+ describe('RoborockService - getMessageProcessor', () => {
191
+ let roborockService: RoborockService;
192
+ let mockLogger: AnsiLogger;
193
+ let mockMessageProcessor: jest.Mocked<MessageProcessor>;
194
+
195
+ beforeEach(() => {
196
+ mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
197
+ mockMessageProcessor = { startClean: jest.fn() } as any;
198
+ roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
199
+ });
200
+
201
+ it('should return processor if present', () => {
202
+ roborockService['messageProcessorMap'].set('duid', mockMessageProcessor);
203
+ expect(roborockService.getMessageProcessor('duid')).toBe(mockMessageProcessor);
204
+ });
205
+
206
+ it('should log error if processor not present', () => {
207
+ expect(roborockService.getMessageProcessor('unknown')).toBeUndefined();
208
+ expect(mockLogger.error).toHaveBeenCalledWith('MessageApi is not initialized.');
209
+ });
210
+ });
211
+
212
+ describe('RoborockService - getCleanModeData', () => {
213
+ let roborockService: RoborockService;
214
+ let mockLogger: AnsiLogger;
215
+ let mockMessageProcessor: jest.Mocked<MessageProcessor>;
216
+
217
+ beforeEach(() => {
218
+ mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
219
+ mockMessageProcessor = { getCleanModeData: jest.fn() } as any;
220
+ roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
221
+ roborockService['messageProcessorMap'].set('duid', mockMessageProcessor);
222
+ });
223
+
224
+ it('should return clean mode data', async () => {
225
+ mockMessageProcessor.getCleanModeData.mockResolvedValue({ suctionPower: 1, waterFlow: 2, distance_off: 3, mopRoute: 4 });
226
+ const result = await roborockService.getCleanModeData('duid');
227
+ expect(result).toEqual({ suctionPower: 1, waterFlow: 2, distance_off: 3, mopRoute: 4 });
228
+ expect(mockLogger.notice).toHaveBeenCalledWith('RoborockService - getCleanModeData');
229
+ });
230
+ });
231
+
232
+ describe('RoborockService - changeCleanMode', () => {
233
+ let roborockService: RoborockService;
234
+ let mockLogger: AnsiLogger;
235
+ let mockMessageProcessor: jest.Mocked<MessageProcessor>;
236
+
237
+ beforeEach(() => {
238
+ mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
239
+ mockMessageProcessor = { changeCleanMode: jest.fn() } as any;
240
+ roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
241
+ roborockService['messageProcessorMap'].set('duid', mockMessageProcessor);
242
+ });
243
+
244
+ it('should call changeCleanMode on processor', async () => {
245
+ await roborockService.changeCleanMode('duid', { suctionPower: 1, waterFlow: 2, distance_off: 3, mopRoute: 4 });
246
+ expect(mockLogger.notice).toHaveBeenCalledWith('RoborockService - changeCleanMode');
247
+ expect(mockMessageProcessor.changeCleanMode).toHaveBeenCalledWith('duid', 1, 2, 4, 3);
248
+ });
249
+ });
250
+
251
+ describe('RoborockService - pause/stop/resume/playSound', () => {
252
+ let roborockService: RoborockService;
253
+ let mockLogger: AnsiLogger;
254
+ let mockMessageProcessor: jest.Mocked<MessageProcessor>;
255
+
256
+ beforeEach(() => {
257
+ mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
258
+ mockMessageProcessor = {
259
+ pauseClean: jest.fn(),
260
+ gotoDock: jest.fn(),
261
+ resumeClean: jest.fn(),
262
+ findMyRobot: jest.fn(),
263
+ } as any;
264
+ roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
265
+ roborockService['messageProcessorMap'].set('duid', mockMessageProcessor);
266
+ });
267
+
268
+ it('pauseClean should call processor and log', async () => {
269
+ await roborockService.pauseClean('duid');
270
+ expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - pauseClean');
271
+ expect(mockMessageProcessor.pauseClean).toHaveBeenCalledWith('duid');
272
+ });
273
+
274
+ it('stopAndGoHome should call processor and log', async () => {
275
+ await roborockService.stopAndGoHome('duid');
276
+ expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - stopAndGoHome');
277
+ expect(mockMessageProcessor.gotoDock).toHaveBeenCalledWith('duid');
278
+ });
279
+
280
+ it('resumeClean should call processor and log', async () => {
281
+ await roborockService.resumeClean('duid');
282
+ expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - resumeClean');
283
+ expect(mockMessageProcessor.resumeClean).toHaveBeenCalledWith('duid');
284
+ });
285
+
286
+ it('playSoundToLocate should call processor and log', async () => {
287
+ await roborockService.playSoundToLocate('duid');
288
+ expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - findMe');
289
+ expect(mockMessageProcessor.findMyRobot).toHaveBeenCalledWith('duid');
290
+ });
291
+ });
292
+
293
+ describe('RoborockService - customGet/customGetInSecure/customSend', () => {
294
+ let roborockService: RoborockService;
295
+ let mockLogger: AnsiLogger;
296
+ let mockMessageProcessor: jest.Mocked<MessageProcessor>;
297
+
298
+ beforeEach(() => {
299
+ mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
300
+ mockMessageProcessor = {
301
+ getCustomMessage: jest.fn(),
302
+ sendCustomMessage: jest.fn(),
303
+ } as any;
304
+ roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
305
+ roborockService['messageProcessorMap'].set('duid', mockMessageProcessor);
306
+ });
307
+
308
+ it('customGet should call getCustomMessage', async () => {
309
+ mockMessageProcessor.getCustomMessage.mockResolvedValue('result');
310
+ const result = await roborockService.customGet('duid', 'method');
311
+ expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - customSend-message', 'method');
312
+ expect(mockMessageProcessor.getCustomMessage).toHaveBeenCalledWith('duid', expect.any(Object));
313
+ expect(result).toBe('result');
314
+ });
315
+
316
+ it('customGetInSecure should call getCustomMessage with secure', async () => {
317
+ mockMessageProcessor.getCustomMessage.mockResolvedValue('secureResult');
318
+ const result = await roborockService.customGetInSecure('duid', 'method');
319
+ expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - customGetInSecure-message', 'method');
320
+ expect(mockMessageProcessor.getCustomMessage).toHaveBeenCalledWith('duid', expect.objectContaining({ secure: true }));
321
+ expect(result).toBe('secureResult');
322
+ });
323
+
324
+ it('customSend should call sendCustomMessage', async () => {
325
+ const req = { foo: 'bar' } as any;
326
+ await roborockService.customSend('duid', req);
327
+ expect(mockMessageProcessor.sendCustomMessage).toHaveBeenCalledWith('duid', req);
328
+ });
329
+ });
330
+
331
+ describe('RoborockService - stopService', () => {
332
+ let roborockService: RoborockService;
333
+ let mockLogger: AnsiLogger;
334
+ let mockMessageClient: any;
335
+ let mockLocalClient: any;
336
+ let mockMessageProcessor: any;
337
+
338
+ beforeEach(() => {
339
+ mockLogger = { debug: jest.fn(), notice: jest.fn(), error: jest.fn(), warn: jest.fn() } as any;
340
+ mockMessageClient = { disconnect: jest.fn() };
341
+ mockLocalClient = { disconnect: jest.fn(), isConnected: jest.fn() };
342
+ mockMessageProcessor = {};
343
+ roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, mockLogger);
344
+ roborockService.messageClient = mockMessageClient;
345
+ roborockService.localClientMap.set('duid', mockLocalClient);
346
+ roborockService.messageProcessorMap.set('duid', mockMessageProcessor);
347
+ roborockService.requestDeviceStatusInterval = setInterval(() => {
348
+ jest.fn();
349
+ }, 1000);
350
+ });
351
+
352
+ afterEach(() => {
353
+ jest.clearAllTimers();
354
+ });
355
+
356
+ it('should disconnect messageClient, localClient, remove processors, clear interval', () => {
357
+ roborockService.stopService();
358
+ expect(mockMessageClient.disconnect).toHaveBeenCalled();
359
+ expect(mockLocalClient.disconnect).toHaveBeenCalled();
360
+ expect(roborockService.localClientMap.size).toBe(0);
361
+ expect(roborockService.messageProcessorMap.size).toBe(0);
362
+ expect(roborockService.requestDeviceStatusInterval).toBeUndefined();
363
+ });
364
+ });
365
+
366
+ describe('RoborockService - setDeviceNotify', () => {
367
+ it('should set deviceNotify callback', () => {
368
+ const roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, {} as any);
369
+ const cb = jest.fn();
370
+ roborockService.setDeviceNotify(cb);
371
+ expect(roborockService.deviceNotify).toBe(cb);
372
+ });
373
+ });
374
+
375
+ describe('RoborockService - sleep', () => {
376
+ it('should resolve after ms', async () => {
377
+ const roborockService = new RoborockService(jest.fn(), jest.fn(), 10, {} as any, {} as any);
378
+ const start = Date.now();
379
+ await roborockService['sleep'](100);
380
+ expect(Date.now() - start).toBeGreaterThanOrEqual(100);
381
+ });
382
+ });
383
+
384
+ describe('RoborockService - auth', () => {
385
+ it('should set userdata and iotApi', () => {
386
+ const mockLogger = {} as any;
387
+ const mockIotApiFactory = jest.fn().mockReturnValue('iotApi');
388
+ const roborockService = new RoborockService(jest.fn(), mockIotApiFactory, 10, {} as any, mockLogger);
389
+ const userData = { foo: 'bar' } as any;
390
+ const result = roborockService['auth'](userData);
391
+ expect(roborockService['userdata']).toBe(userData);
392
+ expect(roborockService['iotApi']).toBe('iotApi');
393
+ expect(result).toBe(userData);
394
+ });
395
+ });