matterbridge-roborock-vacuum-plugin 1.1.0-rc13 → 1.1.0-rc14
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 +1 -1
- package/dist/platform.js +2 -2
- package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +27 -27
- package/dist/roborockCommunication/broadcast/client/MQTTClient.js +1 -2
- package/dist/roborockCommunication/broadcast/clientRouter.js +1 -1
- package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js +2 -2
- package/dist/roborockCommunication/broadcast/messageProcessor.js +9 -0
- package/dist/roborockCommunication/broadcast/model/protocol.js +9 -0
- package/dist/roborockService.js +20 -1
- package/matterbridge-roborock-vacuum-plugin.config.json +1 -1
- package/matterbridge-roborock-vacuum-plugin.schema.json +1 -1
- package/misc/status.md +119 -0
- package/package.json +1 -1
- package/src/platform.ts +2 -2
- package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +35 -33
- package/src/roborockCommunication/broadcast/client/MQTTClient.ts +2 -2
- package/src/roborockCommunication/broadcast/clientRouter.ts +1 -1
- package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts +2 -3
- package/src/roborockCommunication/broadcast/messageProcessor.ts +12 -5
- package/src/roborockCommunication/broadcast/model/protocol.ts +9 -0
- package/src/roborockService.ts +21 -1
- package/src/tests/roborockCommunication/RESTAPI/roborockAuthenticateApi.test.ts +136 -0
- package/src/tests/roborockCommunication/RESTAPI/roborockIoTApi.test.ts +106 -0
- package/src/tests/roborockCommunication/broadcast/client/LocalNetworkClient.test.ts +3 -5
- package/src/tests/roborockCommunication/broadcast/clientRouter.test.ts +168 -0
- package/src/tests/roborockCommunication/broadcast/messageProcessor.test.ts +131 -0
- package/src/tests/roborockService.test.ts +102 -2
- package/src/tests/roborockService3.test.ts +133 -0
- package/src/tests/roborockService4.test.ts +76 -0
- package/src/tests/roborockService5.test.ts +79 -0
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ To get the **DUID** for your devices, you have two options:
|
|
|
69
69
|
### 🚧 Project Status
|
|
70
70
|
|
|
71
71
|
- **Under active development**
|
|
72
|
-
- Requires **`matterbridge@3.1.
|
|
72
|
+
- Requires **`matterbridge@3.1.7`**
|
|
73
73
|
- ⚠️ **Known Issue:**
|
|
74
74
|
+ Vacuum may appear as **two devices** in Apple Home
|
|
75
75
|
+ Error: Wrong CRC32 2241274590, expected 0 -> this is normal error and not impact to plugin functionality
|
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.7')) {
|
|
30
|
+
throw new Error(`This plugin requires Matterbridge version >= "3.1.7". 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)
|
|
@@ -13,6 +13,7 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
13
13
|
buffer = new ChunkBuffer();
|
|
14
14
|
messageIdSeq;
|
|
15
15
|
pingInterval;
|
|
16
|
+
keepConnectionAliveInterval = undefined;
|
|
16
17
|
duid;
|
|
17
18
|
ip;
|
|
18
19
|
constructor(logger, context, duid, ip) {
|
|
@@ -34,6 +35,7 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
34
35
|
this.socket.on('timeout', this.onTimeout.bind(this));
|
|
35
36
|
this.socket.on('data', this.onMessage.bind(this));
|
|
36
37
|
this.socket.connect(58867, this.ip);
|
|
38
|
+
this.keepConnectionAlive();
|
|
37
39
|
}
|
|
38
40
|
async disconnect() {
|
|
39
41
|
if (!this.socket) {
|
|
@@ -57,29 +59,16 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
57
59
|
this.socket.write(this.wrapWithLengthData(message.buffer));
|
|
58
60
|
}
|
|
59
61
|
async onConnect() {
|
|
60
|
-
this.logger.debug(`LocalNetworkClient: ${this.duid} connected to ${this.ip}`);
|
|
61
|
-
this.logger.debug(`LocalNetworkClient: ${this.duid} socket writable: ${this.socket?.writable}, readable: ${this.socket?.readable}`);
|
|
62
|
-
this.connected = true;
|
|
63
|
-
this.retryCount = 0;
|
|
62
|
+
this.logger.debug(` [LocalNetworkClient]: ${this.duid} connected to ${this.ip}`);
|
|
63
|
+
this.logger.debug(` [LocalNetworkClient]: ${this.duid} socket writable: ${this.socket?.writable}, readable: ${this.socket?.readable}`);
|
|
64
64
|
await this.sendHelloMessage();
|
|
65
65
|
this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
|
|
66
|
+
this.connected = true;
|
|
67
|
+
this.retryCount = 0;
|
|
66
68
|
await this.connectionListeners.onConnected(this.duid);
|
|
67
69
|
}
|
|
68
|
-
async onEnd() {
|
|
69
|
-
await this.destroySocket('Socket has ended.');
|
|
70
|
-
await this.connectionListeners.onDisconnected(this.duid, 'Socket has ended.');
|
|
71
|
-
}
|
|
72
70
|
async onDisconnect(hadError) {
|
|
73
|
-
|
|
74
|
-
if (!hadError) {
|
|
75
|
-
await this.connectionListeners.onDisconnected(this.duid, 'Socket disconnected. Had no error.');
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
async onTimeout() {
|
|
79
|
-
this.logger.error(`LocalNetworkClient: Socket for ${this.duid} timed out.`);
|
|
80
|
-
}
|
|
81
|
-
async destroySocket(message) {
|
|
82
|
-
this.logger.error(`LocalNetworkClient: Destroying socket for ${this.duid} due to: ${message}`);
|
|
71
|
+
this.logger.info(` [LocalNetworkClient]: ${this.duid} socket disconnected. Had error: ${hadError}`);
|
|
83
72
|
this.connected = false;
|
|
84
73
|
if (this.socket) {
|
|
85
74
|
this.socket.destroy();
|
|
@@ -88,19 +77,18 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
88
77
|
if (this.pingInterval) {
|
|
89
78
|
clearInterval(this.pingInterval);
|
|
90
79
|
}
|
|
80
|
+
await this.connectionListeners.onDisconnected(this.duid, 'Socket disconnected. Had no error.');
|
|
91
81
|
}
|
|
92
82
|
async onError(error) {
|
|
93
|
-
this.logger.error(
|
|
94
|
-
this.connected = false;
|
|
95
|
-
if (this.socket) {
|
|
96
|
-
this.socket.destroy();
|
|
97
|
-
this.socket = undefined;
|
|
98
|
-
}
|
|
99
|
-
if (this.pingInterval) {
|
|
100
|
-
clearInterval(this.pingInterval);
|
|
101
|
-
}
|
|
83
|
+
this.logger.error(` [LocalNetworkClient]: Socket error for ${this.duid}: ${error.message}`);
|
|
102
84
|
await this.connectionListeners.onError(this.duid, error.message);
|
|
103
85
|
}
|
|
86
|
+
async onTimeout() {
|
|
87
|
+
this.logger.error(` [LocalNetworkClient]: Socket for ${this.duid} timed out.`);
|
|
88
|
+
}
|
|
89
|
+
async onEnd() {
|
|
90
|
+
this.logger.debug(` [LocalNetworkClient]: ${this.duid} socket ended.`);
|
|
91
|
+
}
|
|
104
92
|
async onMessage(message) {
|
|
105
93
|
if (!this.socket) {
|
|
106
94
|
return;
|
|
@@ -170,4 +158,16 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
170
158
|
});
|
|
171
159
|
await this.send(this.duid, request);
|
|
172
160
|
}
|
|
161
|
+
keepConnectionAlive() {
|
|
162
|
+
if (this.keepConnectionAliveInterval) {
|
|
163
|
+
clearTimeout(this.keepConnectionAliveInterval);
|
|
164
|
+
this.keepConnectionAliveInterval.unref();
|
|
165
|
+
}
|
|
166
|
+
this.keepConnectionAliveInterval = setInterval(() => {
|
|
167
|
+
if (this.socket === undefined || !this.connected || !this.socket.writable || this.socket.readable) {
|
|
168
|
+
this.logger.debug(` [LocalNetworkClient]: ${this.duid} socket is not writable or readable, reconnecting...`);
|
|
169
|
+
this.connect();
|
|
170
|
+
}
|
|
171
|
+
}, 60 * 60 * 1000);
|
|
172
|
+
}
|
|
173
173
|
}
|
|
@@ -26,8 +26,7 @@ export class MQTTClient extends AbstractClient {
|
|
|
26
26
|
username: this.mqttUsername,
|
|
27
27
|
password: this.mqttPassword,
|
|
28
28
|
keepalive: 30,
|
|
29
|
-
log: (
|
|
30
|
-
this.logger.debug(`MQTTClient args: ${debugStringify(args)}`);
|
|
29
|
+
log: () => {
|
|
31
30
|
},
|
|
32
31
|
});
|
|
33
32
|
this.mqttClient.on('connect', this.onConnect.bind(this));
|
|
@@ -7,9 +7,9 @@ export class ClientRouter {
|
|
|
7
7
|
connectionListeners = new ChainedConnectionListener();
|
|
8
8
|
messageListeners = new ChainedMessageListener();
|
|
9
9
|
context;
|
|
10
|
-
mqttClient;
|
|
11
10
|
localClients = new Map();
|
|
12
11
|
logger;
|
|
12
|
+
mqttClient;
|
|
13
13
|
constructor(logger, userdata) {
|
|
14
14
|
this.context = new MessageContext(userdata);
|
|
15
15
|
this.logger = logger;
|
package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js
CHANGED
|
@@ -31,10 +31,10 @@ export class ConnectionStateListener {
|
|
|
31
31
|
this.logger.info(`Device with DUID ${duid} is in disconnecting step, skipping re-registration.`);
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
-
this.logger.info(`Re-registering device with DUID ${duid} to ${this.clientName}`);
|
|
35
34
|
setTimeout(() => {
|
|
35
|
+
this.logger.info(`Re-registering device with DUID ${duid} to ${this.clientName}`);
|
|
36
36
|
this.client.connect();
|
|
37
|
-
},
|
|
37
|
+
}, 10000);
|
|
38
38
|
this.client.isInDisconnectingStep = false;
|
|
39
39
|
}
|
|
40
40
|
async onError(duid, message) {
|
|
@@ -31,6 +31,15 @@ export class MessageProcessor {
|
|
|
31
31
|
}
|
|
32
32
|
return undefined;
|
|
33
33
|
}
|
|
34
|
+
async getDeviceStatusOverMQTT(duid) {
|
|
35
|
+
const request = new RequestMessage({ method: 'get_status', secure: true });
|
|
36
|
+
const response = await this.client.get(duid, request);
|
|
37
|
+
if (response) {
|
|
38
|
+
this.logger?.debug('MQTT - Device status: ', debugStringify(response));
|
|
39
|
+
return new DeviceStatus(response);
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
34
43
|
async getRooms(duid, rooms) {
|
|
35
44
|
const request = new RequestMessage({ method: 'get_room_mapping' });
|
|
36
45
|
return this.client.get(duid, request).then((response) => new RoomInfo(rooms, response ?? []));
|
|
@@ -13,7 +13,16 @@ export var Protocol;
|
|
|
13
13
|
Protocol[Protocol["battery"] = 122] = "battery";
|
|
14
14
|
Protocol[Protocol["suction_power"] = 123] = "suction_power";
|
|
15
15
|
Protocol[Protocol["water_box_mode"] = 124] = "water_box_mode";
|
|
16
|
+
Protocol[Protocol["main_brush_work_time"] = 125] = "main_brush_work_time";
|
|
17
|
+
Protocol[Protocol["side_brush_work_time"] = 126] = "side_brush_work_time";
|
|
18
|
+
Protocol[Protocol["filter_work_time"] = 127] = "filter_work_time";
|
|
16
19
|
Protocol[Protocol["additional_props"] = 128] = "additional_props";
|
|
20
|
+
Protocol[Protocol["task_complete"] = 130] = "task_complete";
|
|
21
|
+
Protocol[Protocol["task_cancel_low_power"] = 131] = "task_cancel_low_power";
|
|
22
|
+
Protocol[Protocol["task_cancel_in_motion"] = 132] = "task_cancel_in_motion";
|
|
23
|
+
Protocol[Protocol["charge_status"] = 133] = "charge_status";
|
|
24
|
+
Protocol[Protocol["drying_status"] = 134] = "drying_status";
|
|
17
25
|
Protocol[Protocol["back_type"] = 139] = "back_type";
|
|
18
26
|
Protocol[Protocol["map_response"] = 301] = "map_response";
|
|
27
|
+
Protocol[Protocol["some_thing_happened_when_socket_closed"] = 500] = "some_thing_happened_when_socket_closed";
|
|
19
28
|
})(Protocol || (Protocol = {}));
|
package/dist/roborockService.js
CHANGED
|
@@ -201,7 +201,7 @@ export default class RoborockService {
|
|
|
201
201
|
await messageProcessor.getDeviceStatus(device.duid).then((response) => {
|
|
202
202
|
if (self.deviceNotify && response) {
|
|
203
203
|
const message = { duid: device.duid, ...response.errorStatus, ...response.message };
|
|
204
|
-
self.logger.debug('Device status update', debugStringify(message));
|
|
204
|
+
self.logger.debug('Socket - Device status update', debugStringify(message));
|
|
205
205
|
self.deviceNotify(NotifyMessageTypes.LocalMessage, message);
|
|
206
206
|
}
|
|
207
207
|
});
|
|
@@ -211,6 +211,25 @@ export default class RoborockService {
|
|
|
211
211
|
}
|
|
212
212
|
}, this.refreshInterval * 1000);
|
|
213
213
|
}
|
|
214
|
+
activateDeviceNotifyOverMQTT(device) {
|
|
215
|
+
const self = this;
|
|
216
|
+
this.logger.notice('Requesting device info for device over MQTT', device.duid);
|
|
217
|
+
const messageProcessor = this.getMessageProcessor(device.duid);
|
|
218
|
+
this.requestDeviceStatusInterval = setInterval(async () => {
|
|
219
|
+
if (messageProcessor) {
|
|
220
|
+
await messageProcessor.getDeviceStatusOverMQTT(device.duid).then((response) => {
|
|
221
|
+
if (self.deviceNotify && response) {
|
|
222
|
+
const message = { duid: device.duid, ...response.errorStatus, ...response.message };
|
|
223
|
+
self.logger.debug('MQTT - Device status update', debugStringify(message));
|
|
224
|
+
self.deviceNotify(NotifyMessageTypes.LocalMessage, message);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
self.logger.error('Local client not initialized');
|
|
230
|
+
}
|
|
231
|
+
}, this.refreshInterval * 500);
|
|
232
|
+
}
|
|
214
233
|
async listDevices(username) {
|
|
215
234
|
assert(this.iotApi !== undefined);
|
|
216
235
|
assert(this.userdata !== undefined);
|
|
@@ -1,6 +1,6 @@
|
|
|
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-rc14 by https://github.com/RinDevJunior",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"required": ["username", "password"],
|
|
6
6
|
"properties": {
|
package/misc/status.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Status Message
|
|
2
|
+
|
|
3
|
+
### Response
|
|
4
|
+
|
|
5
|
+
| Key | Example | Description | Only available for |
|
|
6
|
+
| --------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
|
|
7
|
+
| `battery` | _100_ | Battery level (in %) | |
|
|
8
|
+
| `clean_area` | _140000_ | Total area (in cm²) | |
|
|
9
|
+
| `clean_time` | _15_ | Total cleaning time (in s) | |
|
|
10
|
+
| `dnd_enabled` | _0_ | Is 'Do Not Disturb' enabled (0=disabled, 1=enabled) | |
|
|
11
|
+
| `error_code` | _0_ | Error code (see [list](#error-codes) below) | |
|
|
12
|
+
| `fan_power` | _102_ | Fan power, corresponds to the values in [Custom Mode](../README_CLEANMODE.md) | |
|
|
13
|
+
| `in_cleaning` | _0_ | Is device cleaning | |
|
|
14
|
+
| `in_fresh_state` | _1_ | ? | |
|
|
15
|
+
| `in_returning` | _0_ | Is returning to dock (0=no, 1=yes) | |
|
|
16
|
+
| `is_locating` | _0_ | ? | |
|
|
17
|
+
| `lab_status` | _1_ | ? | |
|
|
18
|
+
| `lock_status` | _0_ | ? | |
|
|
19
|
+
| `map_present` | _1_ | Is map present | |
|
|
20
|
+
| `map_status` | _3_ | ? | |
|
|
21
|
+
| `mop_forbidden_enable` | _0_ | ? | |
|
|
22
|
+
| `msg_seq` | _52_ | Message sequence increments with each request | |
|
|
23
|
+
| `msg_ver` | _2_ | Message version (seems always 4 and 2 for s6) see below for other examples | |
|
|
24
|
+
| `state` | _8_ | Status code (see [list](#status-codes) below) | |
|
|
25
|
+
| `water_box_carriage_status` | _0_ | Is carriage mounted (0=no, 1=yes) | |
|
|
26
|
+
| `water_box_mode` | _204_ | Water quantity control, | |
|
|
27
|
+
| `water_box_status` | _1_ | Is water tank mounted (0=no, 1=yes) | |
|
|
28
|
+
|
|
29
|
+
#### Example
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"result": [{
|
|
34
|
+
"msg_ver": 2,
|
|
35
|
+
"msg_seq": 52,
|
|
36
|
+
"state": 8,
|
|
37
|
+
"battery": 100,
|
|
38
|
+
"clean_time": 15,
|
|
39
|
+
"clean_area": 140000,
|
|
40
|
+
"error_code": 0,
|
|
41
|
+
"map_present": 1,
|
|
42
|
+
"in_cleaning": 0,
|
|
43
|
+
"in_returning": 0,
|
|
44
|
+
"in_fresh_state": 1,
|
|
45
|
+
"lab_status": 1,
|
|
46
|
+
"water_box_status": 1,
|
|
47
|
+
"fan_power": 102,
|
|
48
|
+
"dnd_enabled": 0,
|
|
49
|
+
"map_status": 3,
|
|
50
|
+
"is_locating": 0,
|
|
51
|
+
"lock_status": 0,
|
|
52
|
+
"water_box_mode": 204,
|
|
53
|
+
"water_box_carriage_status": 0,
|
|
54
|
+
"mop_forbidden_enable": 0
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"id": 96
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Codes
|
|
62
|
+
|
|
63
|
+
### Status Codes
|
|
64
|
+
|
|
65
|
+
| Code | Description |
|
|
66
|
+
| ---- | -------------- |
|
|
67
|
+
| 0 | Unknown |
|
|
68
|
+
| 1 | Initiating |
|
|
69
|
+
| 2 | Sleeping |
|
|
70
|
+
| 3 | Idle |
|
|
71
|
+
| 4 | Remote Control |
|
|
72
|
+
| 5 | Cleaning |
|
|
73
|
+
| 6 | Returning Dock |
|
|
74
|
+
| 7 | Manual Mode |
|
|
75
|
+
| 8 | Charging |
|
|
76
|
+
| 9 | Charging Error |
|
|
77
|
+
| 10 | Paused |
|
|
78
|
+
| 11 | Spot Cleaning |
|
|
79
|
+
| 12 | In Error |
|
|
80
|
+
| 13 | Shutting Down |
|
|
81
|
+
| 14 | Updating |
|
|
82
|
+
| 15 | Docking |
|
|
83
|
+
| 16 | Go To |
|
|
84
|
+
| 17 | Zone Clean |
|
|
85
|
+
| 18 | Room Clean |
|
|
86
|
+
| 100 | Fully Charged |
|
|
87
|
+
|
|
88
|
+
### Error Codes
|
|
89
|
+
|
|
90
|
+
| Code | Description |
|
|
91
|
+
| ---- | ------------------------------------- |
|
|
92
|
+
| 0 | No error |
|
|
93
|
+
| 1 | Laser sensor fault |
|
|
94
|
+
| 2 | Collision sensor fault |
|
|
95
|
+
| 3 | Wheel floating |
|
|
96
|
+
| 4 | Cliff sensor fault |
|
|
97
|
+
| 5 | Main brush blocked |
|
|
98
|
+
| 6 | Side brush blocked |
|
|
99
|
+
| 7 | Wheel blocked |
|
|
100
|
+
| 8 | Device stuck |
|
|
101
|
+
| 9 | Dust bin missing |
|
|
102
|
+
| 10 | Filter blocked |
|
|
103
|
+
| 11 | Magnetic field detected |
|
|
104
|
+
| 12 | Low battery |
|
|
105
|
+
| 13 | Charging problem |
|
|
106
|
+
| 14 | Battery failure |
|
|
107
|
+
| 15 | Wall sensor fault |
|
|
108
|
+
| 16 | Uneven surface |
|
|
109
|
+
| 17 | Side brush failure |
|
|
110
|
+
| 18 | Suction fan failure |
|
|
111
|
+
| 19 | Unpowered charging station |
|
|
112
|
+
| 20 | Unknown Error |
|
|
113
|
+
| 21 | Laser pressure sensor problem |
|
|
114
|
+
| 22 | Charge sensor problem |
|
|
115
|
+
| 23 | Dock problem |
|
|
116
|
+
| 24 | No-go zone or invisible wall detected |
|
|
117
|
+
| 254 | Bin full |
|
|
118
|
+
| 255 | Internal error |
|
|
119
|
+
| -1 | Unknown Error |
|
package/package.json
CHANGED
package/src/platform.ts
CHANGED
|
@@ -33,9 +33,9 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
33
33
|
super(matterbridge, log, config);
|
|
34
34
|
|
|
35
35
|
// Verify that Matterbridge is the correct version
|
|
36
|
-
if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.1.
|
|
36
|
+
if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.1.7')) {
|
|
37
37
|
throw new Error(
|
|
38
|
-
`This plugin requires Matterbridge version >= "3.1.
|
|
38
|
+
`This plugin requires Matterbridge version >= "3.1.7". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend.`,
|
|
39
39
|
);
|
|
40
40
|
}
|
|
41
41
|
this.log.info('Initializing platform:', this.config.name);
|
|
@@ -16,6 +16,7 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
16
16
|
private buffer: ChunkBuffer = new ChunkBuffer();
|
|
17
17
|
private messageIdSeq: Sequence;
|
|
18
18
|
private pingInterval?: NodeJS.Timeout;
|
|
19
|
+
private keepConnectionAliveInterval: NodeJS.Timeout | undefined = undefined;
|
|
19
20
|
duid: string;
|
|
20
21
|
ip: string;
|
|
21
22
|
|
|
@@ -45,6 +46,8 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
45
46
|
// Data event listener
|
|
46
47
|
this.socket.on('data', this.onMessage.bind(this));
|
|
47
48
|
this.socket.connect(58867, this.ip);
|
|
49
|
+
|
|
50
|
+
this.keepConnectionAlive();
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
public async disconnect(): Promise<void> {
|
|
@@ -75,35 +78,19 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
private async onConnect(): Promise<void> {
|
|
78
|
-
this.logger.debug(`LocalNetworkClient: ${this.duid} connected to ${this.ip}`);
|
|
79
|
-
this.logger.debug(`LocalNetworkClient: ${this.duid} socket writable: ${this.socket?.writable}, readable: ${this.socket?.readable}`);
|
|
80
|
-
this.connected = true;
|
|
81
|
-
this.retryCount = 0;
|
|
81
|
+
this.logger.debug(` [LocalNetworkClient]: ${this.duid} connected to ${this.ip}`);
|
|
82
|
+
this.logger.debug(` [LocalNetworkClient]: ${this.duid} socket writable: ${this.socket?.writable}, readable: ${this.socket?.readable}`);
|
|
82
83
|
|
|
83
84
|
await this.sendHelloMessage();
|
|
84
85
|
this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
|
|
85
|
-
await this.connectionListeners.onConnected(this.duid);
|
|
86
|
-
}
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
await this.connectionListeners.
|
|
87
|
+
this.connected = true;
|
|
88
|
+
this.retryCount = 0;
|
|
89
|
+
await this.connectionListeners.onConnected(this.duid);
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
private async onDisconnect(hadError: boolean): Promise<void> {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (!hadError) {
|
|
97
|
-
await this.connectionListeners.onDisconnected(this.duid, 'Socket disconnected. Had no error.');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private async onTimeout(): Promise<void> {
|
|
102
|
-
this.logger.error(`LocalNetworkClient: Socket for ${this.duid} timed out.`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private async destroySocket(message: string): Promise<void> {
|
|
106
|
-
this.logger.error(`LocalNetworkClient: Destroying socket for ${this.duid} due to: ${message}`);
|
|
93
|
+
this.logger.info(` [LocalNetworkClient]: ${this.duid} socket disconnected. Had error: ${hadError}`);
|
|
107
94
|
this.connected = false;
|
|
108
95
|
|
|
109
96
|
if (this.socket) {
|
|
@@ -113,22 +100,20 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
113
100
|
if (this.pingInterval) {
|
|
114
101
|
clearInterval(this.pingInterval);
|
|
115
102
|
}
|
|
103
|
+
await this.connectionListeners.onDisconnected(this.duid, 'Socket disconnected. Had no error.');
|
|
116
104
|
}
|
|
117
105
|
|
|
118
106
|
private async onError(error: Error): Promise<void> {
|
|
119
|
-
this.logger.error(
|
|
120
|
-
this.
|
|
121
|
-
|
|
122
|
-
if (this.socket) {
|
|
123
|
-
this.socket.destroy();
|
|
124
|
-
this.socket = undefined;
|
|
125
|
-
}
|
|
107
|
+
this.logger.error(` [LocalNetworkClient]: Socket error for ${this.duid}: ${error.message}`);
|
|
108
|
+
await this.connectionListeners.onError(this.duid, error.message);
|
|
109
|
+
}
|
|
126
110
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
111
|
+
private async onTimeout(): Promise<void> {
|
|
112
|
+
this.logger.error(` [LocalNetworkClient]: Socket for ${this.duid} timed out.`);
|
|
113
|
+
}
|
|
130
114
|
|
|
131
|
-
|
|
115
|
+
private async onEnd(): Promise<void> {
|
|
116
|
+
this.logger.debug(` [LocalNetworkClient]: ${this.duid} socket ended.`);
|
|
132
117
|
}
|
|
133
118
|
|
|
134
119
|
private async onMessage(message: Buffer): Promise<void> {
|
|
@@ -212,4 +197,21 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
212
197
|
});
|
|
213
198
|
await this.send(this.duid, request);
|
|
214
199
|
}
|
|
200
|
+
|
|
201
|
+
private keepConnectionAlive(): void {
|
|
202
|
+
if (this.keepConnectionAliveInterval) {
|
|
203
|
+
clearTimeout(this.keepConnectionAliveInterval);
|
|
204
|
+
this.keepConnectionAliveInterval.unref();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.keepConnectionAliveInterval = setInterval(
|
|
208
|
+
() => {
|
|
209
|
+
if (this.socket === undefined || !this.connected || !this.socket.writable || this.socket.readable) {
|
|
210
|
+
this.logger.debug(` [LocalNetworkClient]: ${this.duid} socket is not writable or readable, reconnecting...`);
|
|
211
|
+
this.connect();
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
60 * 60 * 1000,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
215
217
|
}
|
|
@@ -36,8 +36,8 @@ export class MQTTClient extends AbstractClient {
|
|
|
36
36
|
username: this.mqttUsername,
|
|
37
37
|
password: this.mqttPassword,
|
|
38
38
|
keepalive: 30,
|
|
39
|
-
log: (
|
|
40
|
-
this.logger.debug(`MQTTClient args: ${debugStringify(args)}`);
|
|
39
|
+
log: () => {
|
|
40
|
+
// ...args: unknown[] this.logger.debug(`MQTTClient args: ${debugStringify(args)}`);
|
|
41
41
|
},
|
|
42
42
|
});
|
|
43
43
|
|
|
@@ -15,9 +15,9 @@ export class ClientRouter implements Client {
|
|
|
15
15
|
protected readonly messageListeners = new ChainedMessageListener();
|
|
16
16
|
|
|
17
17
|
private readonly context: MessageContext;
|
|
18
|
-
private readonly mqttClient: MQTTClient;
|
|
19
18
|
private readonly localClients = new Map<string, LocalNetworkClient>();
|
|
20
19
|
private readonly logger: AnsiLogger;
|
|
20
|
+
private mqttClient: MQTTClient;
|
|
21
21
|
|
|
22
22
|
public constructor(logger: AnsiLogger, userdata: UserData) {
|
|
23
23
|
this.context = new MessageContext(userdata);
|
package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts
CHANGED
|
@@ -43,11 +43,10 @@ export class ConnectionStateListener implements AbstractConnectionListener {
|
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
this.logger.info(`Re-registering device with DUID ${duid} to ${this.clientName}`);
|
|
47
|
-
|
|
48
46
|
setTimeout(() => {
|
|
47
|
+
this.logger.info(`Re-registering device with DUID ${duid} to ${this.clientName}`);
|
|
49
48
|
this.client.connect();
|
|
50
|
-
},
|
|
49
|
+
}, 10000);
|
|
51
50
|
|
|
52
51
|
this.client.isInDisconnectingStep = false;
|
|
53
52
|
}
|
|
@@ -34,11 +34,6 @@ export class MessageProcessor {
|
|
|
34
34
|
return await this.client.get(duid, request);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// public async getDeviceStatus(duid: string): Promise<DeviceStatus> {
|
|
38
|
-
// const request = new RequestMessage({ method: 'get_status' });
|
|
39
|
-
// return this.client.get<CloudMessageResult[]>(duid, request).then((response) => new DeviceStatus(response[0]));
|
|
40
|
-
// }
|
|
41
|
-
|
|
42
37
|
public async getDeviceStatus(duid: string): Promise<DeviceStatus | undefined> {
|
|
43
38
|
const request = new RequestMessage({ method: 'get_status' });
|
|
44
39
|
const response = await this.client.get<CloudMessageResult[]>(duid, request);
|
|
@@ -51,6 +46,18 @@ export class MessageProcessor {
|
|
|
51
46
|
return undefined;
|
|
52
47
|
}
|
|
53
48
|
|
|
49
|
+
public async getDeviceStatusOverMQTT(duid: string): Promise<DeviceStatus | undefined> {
|
|
50
|
+
const request = new RequestMessage({ method: 'get_status', secure: true });
|
|
51
|
+
const response = await this.client.get<CloudMessageResult[]>(duid, request);
|
|
52
|
+
|
|
53
|
+
if (response) {
|
|
54
|
+
this.logger?.debug('MQTT - Device status: ', debugStringify(response));
|
|
55
|
+
return new DeviceStatus(response);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
public async getRooms(duid: string, rooms: Room[]): Promise<RoomInfo> {
|
|
55
62
|
const request = new RequestMessage({ method: 'get_room_mapping' });
|
|
56
63
|
return this.client.get<number[][] | undefined>(duid, request).then((response) => new RoomInfo(rooms, response ?? []));
|
|
@@ -12,8 +12,17 @@ export enum Protocol {
|
|
|
12
12
|
battery = 122,
|
|
13
13
|
suction_power = 123,
|
|
14
14
|
water_box_mode = 124,
|
|
15
|
+
main_brush_work_time = 125,
|
|
16
|
+
side_brush_work_time = 126,
|
|
17
|
+
filter_work_time = 127,
|
|
15
18
|
additional_props = 128,
|
|
19
|
+
task_complete = 130,
|
|
20
|
+
task_cancel_low_power = 131,
|
|
21
|
+
task_cancel_in_motion = 132,
|
|
22
|
+
charge_status = 133,
|
|
23
|
+
drying_status = 134,
|
|
16
24
|
back_type = 139, // WTF is this
|
|
17
25
|
map_response = 301,
|
|
26
|
+
some_thing_happened_when_socket_closed = 500,
|
|
18
27
|
}
|
|
19
28
|
// "deviceStatus":{"120":0,"121":8,"122":100,"123":110,"124":209,"125":99,"126":96,"127":97,"128":0,"133":1,"134":1,"135":0,"139":0}
|
package/src/roborockService.ts
CHANGED
|
@@ -274,7 +274,7 @@ export default class RoborockService {
|
|
|
274
274
|
await messageProcessor.getDeviceStatus(device.duid).then((response: DeviceStatus | undefined) => {
|
|
275
275
|
if (self.deviceNotify && response) {
|
|
276
276
|
const message = { duid: device.duid, ...response.errorStatus, ...response.message } as DeviceStatusNotify;
|
|
277
|
-
self.logger.debug('Device status update', debugStringify(message));
|
|
277
|
+
self.logger.debug('Socket - Device status update', debugStringify(message));
|
|
278
278
|
self.deviceNotify(NotifyMessageTypes.LocalMessage, message);
|
|
279
279
|
}
|
|
280
280
|
});
|
|
@@ -284,6 +284,26 @@ export default class RoborockService {
|
|
|
284
284
|
}, this.refreshInterval * 1000);
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
public activateDeviceNotifyOverMQTT(device: Device): void {
|
|
288
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
289
|
+
const self = this;
|
|
290
|
+
this.logger.notice('Requesting device info for device over MQTT', device.duid);
|
|
291
|
+
const messageProcessor = this.getMessageProcessor(device.duid);
|
|
292
|
+
this.requestDeviceStatusInterval = setInterval(async () => {
|
|
293
|
+
if (messageProcessor) {
|
|
294
|
+
await messageProcessor.getDeviceStatusOverMQTT(device.duid).then((response: DeviceStatus | undefined) => {
|
|
295
|
+
if (self.deviceNotify && response) {
|
|
296
|
+
const message = { duid: device.duid, ...response.errorStatus, ...response.message } as DeviceStatusNotify;
|
|
297
|
+
self.logger.debug('MQTT - Device status update', debugStringify(message));
|
|
298
|
+
self.deviceNotify(NotifyMessageTypes.LocalMessage, message);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
} else {
|
|
302
|
+
self.logger.error('Local client not initialized');
|
|
303
|
+
}
|
|
304
|
+
}, this.refreshInterval * 500);
|
|
305
|
+
}
|
|
306
|
+
|
|
287
307
|
public async listDevices(username: string): Promise<Device[]> {
|
|
288
308
|
assert(this.iotApi !== undefined);
|
|
289
309
|
assert(this.userdata !== undefined);
|