matterbridge-roborock-vacuum-plugin 1.1.0-rc07 → 1.1.0-rc08
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.
- package/README.md +4 -1
- package/README_DEV.md +13 -7
- package/dist/model/ExperimentalFeatureSetting.js +1 -0
- package/dist/platform.js +16 -3
- package/dist/platformRunner.js +1 -1
- package/dist/roborockCommunication/RESTAPI/roborockAuthenticateApi.js +4 -0
- package/dist/roborockCommunication/broadcast/abstractClient.js +8 -3
- package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +4 -3
- package/dist/roborockCommunication/broadcast/client/MQTTClient.js +6 -4
- package/dist/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.js +6 -6
- package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js +25 -0
- package/dist/roborockService.js +13 -14
- package/matterbridge-roborock-vacuum-plugin.config.json +4 -3
- package/matterbridge-roborock-vacuum-plugin.schema.json +15 -37
- package/package.json +1 -1
- package/src/model/ExperimentalFeatureSetting.ts +2 -0
- package/src/platform.ts +24 -4
- package/src/platformRunner.ts +1 -1
- package/src/roborockCommunication/RESTAPI/roborockAuthenticateApi.ts +7 -2
- package/src/roborockCommunication/Zmodel/userData.ts +3 -0
- package/src/roborockCommunication/broadcast/abstractClient.ts +11 -4
- package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +5 -3
- package/src/roborockCommunication/broadcast/client/MQTTClient.ts +6 -4
- package/src/roborockCommunication/broadcast/listener/abstractConnectionListener.ts +3 -3
- package/src/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.ts +8 -7
- package/src/roborockCommunication/broadcast/listener/implementation/chainedMessageListener.ts +2 -2
- package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts +35 -0
- package/src/roborockService.ts +21 -22
- package/src/tests/roborockCommunication/broadcast/client/LocalNetworkClient.test.ts +1 -1
- package/src/tests/roborockCommunication/broadcast/client/MQTTClient.test.ts +1 -1
- package/src/tests/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.test.ts +8 -8
- 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:**
|
|
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/
|
|
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 `
|
|
21
|
-
_Example:_ [`src/behaviors/roborock.vacuum/
|
|
22
|
-
This file
|
|
23
|
-
- **Create an `
|
|
24
|
-
|
|
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
|
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.
|
|
30
|
-
throw new Error(`This plugin requires Matterbridge version >= "3.1.
|
|
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));
|
package/dist/platformRunner.js
CHANGED
|
@@ -72,7 +72,7 @@ export class PlatformRunner {
|
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
if (!tracked) {
|
|
75
|
-
platform.log.debug(
|
|
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,21 +3,26 @@ 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
|
-
|
|
13
|
+
connected = false;
|
|
14
14
|
logger;
|
|
15
|
+
context;
|
|
16
|
+
syncMessageListener;
|
|
17
|
+
connectionStateListener;
|
|
15
18
|
constructor(logger, context) {
|
|
16
19
|
this.context = context;
|
|
17
20
|
this.serializer = new MessageSerializer(this.context, logger);
|
|
18
21
|
this.deserializer = new MessageDeserializer(this.context, logger);
|
|
19
22
|
this.syncMessageListener = new SyncMessageListener(logger);
|
|
20
23
|
this.messageListeners.register(this.syncMessageListener);
|
|
24
|
+
this.connectionStateListener = new ConnectionStateListener(logger, this);
|
|
25
|
+
this.connectionListeners.register(this.connectionStateListener);
|
|
21
26
|
this.logger = logger;
|
|
22
27
|
}
|
|
23
28
|
async get(duid, request) {
|
|
@@ -31,6 +31,7 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
31
31
|
if (!this.socket) {
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
+
this.isInDisconnectingStep = true;
|
|
34
35
|
if (this.pingInterval) {
|
|
35
36
|
clearInterval(this.pingInterval);
|
|
36
37
|
}
|
|
@@ -53,7 +54,7 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
53
54
|
this.logger.debug(`${this.duid} connected to ${this.ip}, address: ${address ? debugStringify(address) : 'undefined'}`);
|
|
54
55
|
await this.sendHelloMessage();
|
|
55
56
|
this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
|
|
56
|
-
await this.connectionListeners.onConnected();
|
|
57
|
+
await this.connectionListeners.onConnected(this.duid);
|
|
57
58
|
}
|
|
58
59
|
async onDisconnect() {
|
|
59
60
|
this.logger.notice('LocalNetworkClient: Socket has disconnected.');
|
|
@@ -65,7 +66,7 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
65
66
|
if (this.pingInterval) {
|
|
66
67
|
clearInterval(this.pingInterval);
|
|
67
68
|
}
|
|
68
|
-
await this.connectionListeners.onDisconnected();
|
|
69
|
+
await this.connectionListeners.onDisconnected(this.duid);
|
|
69
70
|
}
|
|
70
71
|
async onError(result) {
|
|
71
72
|
this.logger.error('LocalNetworkClient: Socket connection error: ' + result);
|
|
@@ -74,7 +75,7 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
74
75
|
this.socket.destroy();
|
|
75
76
|
this.socket = undefined;
|
|
76
77
|
}
|
|
77
|
-
await this.connectionListeners.onError(result.toString());
|
|
78
|
+
await this.connectionListeners.onError(this.duid, result.toString());
|
|
78
79
|
}
|
|
79
80
|
async onMessage(message) {
|
|
80
81
|
if (!this.socket) {
|
|
@@ -38,6 +38,7 @@ export class MQTTClient extends AbstractClient {
|
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
try {
|
|
41
|
+
this.isInDisconnectingStep = true;
|
|
41
42
|
this.client.end();
|
|
42
43
|
}
|
|
43
44
|
catch (error) {
|
|
@@ -58,7 +59,7 @@ export class MQTTClient extends AbstractClient {
|
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
61
|
this.connected = true;
|
|
61
|
-
await this.connectionListeners.onConnected();
|
|
62
|
+
await this.connectionListeners.onConnected('mqtt-' + this.mqttUsername);
|
|
62
63
|
this.subscribeToQueue();
|
|
63
64
|
}
|
|
64
65
|
subscribeToQueue() {
|
|
@@ -73,15 +74,16 @@ export class MQTTClient extends AbstractClient {
|
|
|
73
74
|
}
|
|
74
75
|
this.logger.error('failed to subscribe to the queue: ' + err);
|
|
75
76
|
this.connected = false;
|
|
76
|
-
await this.connectionListeners.onDisconnected();
|
|
77
|
+
await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername);
|
|
77
78
|
}
|
|
78
79
|
async onDisconnect() {
|
|
79
|
-
|
|
80
|
+
this.connected = false;
|
|
81
|
+
await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername);
|
|
80
82
|
}
|
|
81
83
|
async onError(result) {
|
|
82
84
|
this.logger.error('MQTT connection error: ' + result);
|
|
83
85
|
this.connected = false;
|
|
84
|
-
await this.connectionListeners.onError(result.toString());
|
|
86
|
+
await this.connectionListeners.onError('mqtt-' + this.mqttUsername, result.toString());
|
|
85
87
|
}
|
|
86
88
|
onReconnect() {
|
|
87
89
|
this.subscribeToQueue();
|
package/dist/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.js
CHANGED
|
@@ -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
|
}
|
package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class ConnectionStateListener {
|
|
2
|
+
logger;
|
|
3
|
+
client;
|
|
4
|
+
constructor(logger, client) {
|
|
5
|
+
this.logger = logger;
|
|
6
|
+
this.client = client;
|
|
7
|
+
}
|
|
8
|
+
async onConnected(duid) {
|
|
9
|
+
this.logger.notice(`Device ${duid} connected to MQTT broker`);
|
|
10
|
+
}
|
|
11
|
+
async onDisconnected(duid) {
|
|
12
|
+
this.logger.notice(`Device ${duid} disconnected from MQTT broker`);
|
|
13
|
+
const isInDisconnectingStep = this.client.isInDisconnectingStep;
|
|
14
|
+
if (isInDisconnectingStep) {
|
|
15
|
+
this.logger.info(`Device with DUID ${duid} is in disconnecting step, skipping re-registration.`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
this.logger.info(`Re-registering device with DUID ${duid} to MQTT broker`);
|
|
19
|
+
this.client.connect();
|
|
20
|
+
this.client.isInDisconnectingStep = false;
|
|
21
|
+
}
|
|
22
|
+
async onError(duid, message) {
|
|
23
|
+
this.logger.error(`Error on device with DUID ${duid}: ${message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/roborockService.js
CHANGED
|
@@ -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
|
-
|
|
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-
|
|
4
|
+
"version": "1.1.0-rc08",
|
|
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-
|
|
3
|
+
"description": "matterbridge-roborock-vacuum-plugin v. 1.1.0-rc08 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
|
@@ -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.
|
|
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.
|
|
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(
|
|
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));
|
package/src/platformRunner.ts
CHANGED
|
@@ -91,7 +91,7 @@ export class PlatformRunner {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
if (!tracked) {
|
|
94
|
-
platform.log.debug(
|
|
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
|
|
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
|
}
|
|
@@ -10,18 +10,21 @@ 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;
|
|
19
24
|
|
|
20
25
|
private readonly context: MessageContext;
|
|
21
|
-
protected readonly serializer: MessageSerializer;
|
|
22
|
-
protected readonly deserializer: MessageDeserializer;
|
|
23
26
|
private readonly syncMessageListener: SyncMessageListener;
|
|
24
|
-
|
|
27
|
+
private readonly connectionStateListener: ConnectionStateListener;
|
|
25
28
|
|
|
26
29
|
protected constructor(logger: AnsiLogger, context: MessageContext) {
|
|
27
30
|
this.context = context;
|
|
@@ -30,6 +33,10 @@ export abstract class AbstractClient implements Client {
|
|
|
30
33
|
|
|
31
34
|
this.syncMessageListener = new SyncMessageListener(logger);
|
|
32
35
|
this.messageListeners.register(this.syncMessageListener);
|
|
36
|
+
|
|
37
|
+
this.connectionStateListener = new ConnectionStateListener(logger, this);
|
|
38
|
+
this.connectionListeners.register(this.connectionStateListener);
|
|
39
|
+
|
|
33
40
|
this.logger = logger;
|
|
34
41
|
}
|
|
35
42
|
|
|
@@ -37,6 +37,8 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
this.isInDisconnectingStep = true;
|
|
41
|
+
|
|
40
42
|
if (this.pingInterval) {
|
|
41
43
|
clearInterval(this.pingInterval);
|
|
42
44
|
}
|
|
@@ -63,7 +65,7 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
63
65
|
this.logger.debug(`${this.duid} connected to ${this.ip}, address: ${address ? debugStringify(address) : 'undefined'}`);
|
|
64
66
|
await this.sendHelloMessage();
|
|
65
67
|
this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
|
|
66
|
-
await this.connectionListeners.onConnected();
|
|
68
|
+
await this.connectionListeners.onConnected(this.duid);
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
private async onDisconnect(): Promise<void> {
|
|
@@ -78,7 +80,7 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
78
80
|
clearInterval(this.pingInterval);
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
await this.connectionListeners.onDisconnected();
|
|
83
|
+
await this.connectionListeners.onDisconnected(this.duid);
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
private async onError(result: Error): Promise<void> {
|
|
@@ -90,7 +92,7 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
90
92
|
this.socket = undefined;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
await this.connectionListeners.onError(result.toString());
|
|
95
|
+
await this.connectionListeners.onError(this.duid, result.toString());
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
private async onMessage(message: Buffer): Promise<void> {
|
|
@@ -51,6 +51,7 @@ export class MQTTClient extends AbstractClient {
|
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
try {
|
|
54
|
+
this.isInDisconnectingStep = true;
|
|
54
55
|
this.client.end();
|
|
55
56
|
} catch (error) {
|
|
56
57
|
this.logger.error('MQTT client failed to disconnect with error: ' + error);
|
|
@@ -74,7 +75,7 @@ export class MQTTClient extends AbstractClient {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
this.connected = true;
|
|
77
|
-
await this.connectionListeners.onConnected();
|
|
78
|
+
await this.connectionListeners.onConnected('mqtt-' + this.mqttUsername);
|
|
78
79
|
|
|
79
80
|
this.subscribeToQueue();
|
|
80
81
|
}
|
|
@@ -94,18 +95,19 @@ export class MQTTClient extends AbstractClient {
|
|
|
94
95
|
this.logger.error('failed to subscribe to the queue: ' + err);
|
|
95
96
|
this.connected = false;
|
|
96
97
|
|
|
97
|
-
await this.connectionListeners.onDisconnected();
|
|
98
|
+
await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
private async onDisconnect() {
|
|
101
|
-
|
|
102
|
+
this.connected = false;
|
|
103
|
+
await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername);
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
private async onError(result: Error | ErrorWithReasonCode) {
|
|
105
107
|
this.logger.error('MQTT connection error: ' + result);
|
|
106
108
|
this.connected = false;
|
|
107
109
|
|
|
108
|
-
await this.connectionListeners.onError(result.toString());
|
|
110
|
+
await this.connectionListeners.onError('mqtt-' + this.mqttUsername, result.toString());
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
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
|
}
|
package/src/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.ts
CHANGED
|
@@ -2,25 +2,26 @@ import { AbstractConnectionListener } from '../abstractConnectionListener.js';
|
|
|
2
2
|
|
|
3
3
|
export class ChainedConnectionListener implements AbstractConnectionListener {
|
|
4
4
|
private listeners: AbstractConnectionListener[] = [];
|
|
5
|
-
|
|
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
|
}
|
package/src/roborockCommunication/broadcast/listener/implementation/chainedMessageListener.ts
CHANGED
|
@@ -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
|
}
|
package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
constructor(logger: AnsiLogger, client: AbstractClient) {
|
|
9
|
+
this.logger = logger;
|
|
10
|
+
this.client = client;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public async onConnected(duid: string): Promise<void> {
|
|
14
|
+
this.logger.notice(`Device ${duid} connected to MQTT broker`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public async onDisconnected(duid: string): Promise<void> {
|
|
18
|
+
this.logger.notice(`Device ${duid} disconnected from MQTT broker`);
|
|
19
|
+
|
|
20
|
+
const isInDisconnectingStep = this.client.isInDisconnectingStep;
|
|
21
|
+
if (isInDisconnectingStep) {
|
|
22
|
+
this.logger.info(`Device with DUID ${duid} is in disconnecting step, skipping re-registration.`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.logger.info(`Re-registering device with DUID ${duid} to MQTT broker`);
|
|
27
|
+
this.client.connect();
|
|
28
|
+
|
|
29
|
+
this.client.isInDisconnectingStep = false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async onError(duid: string, message: string): Promise<void> {
|
|
33
|
+
this.logger.error(`Error on device with DUID ${duid}: ${message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/roborockService.ts
CHANGED
|
@@ -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(
|
|
72
|
-
|
|
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(
|
|
389
|
+
expect(Date.now() - start).toBeGreaterThanOrEqual(0);
|
|
390
390
|
});
|
|
391
391
|
});
|
|
392
392
|
|