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.
Files changed (30) hide show
  1. package/README.md +1 -1
  2. package/dist/platform.js +2 -2
  3. package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +27 -27
  4. package/dist/roborockCommunication/broadcast/client/MQTTClient.js +1 -2
  5. package/dist/roborockCommunication/broadcast/clientRouter.js +1 -1
  6. package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js +2 -2
  7. package/dist/roborockCommunication/broadcast/messageProcessor.js +9 -0
  8. package/dist/roborockCommunication/broadcast/model/protocol.js +9 -0
  9. package/dist/roborockService.js +20 -1
  10. package/matterbridge-roborock-vacuum-plugin.config.json +1 -1
  11. package/matterbridge-roborock-vacuum-plugin.schema.json +1 -1
  12. package/misc/status.md +119 -0
  13. package/package.json +1 -1
  14. package/src/platform.ts +2 -2
  15. package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +35 -33
  16. package/src/roborockCommunication/broadcast/client/MQTTClient.ts +2 -2
  17. package/src/roborockCommunication/broadcast/clientRouter.ts +1 -1
  18. package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts +2 -3
  19. package/src/roborockCommunication/broadcast/messageProcessor.ts +12 -5
  20. package/src/roborockCommunication/broadcast/model/protocol.ts +9 -0
  21. package/src/roborockService.ts +21 -1
  22. package/src/tests/roborockCommunication/RESTAPI/roborockAuthenticateApi.test.ts +136 -0
  23. package/src/tests/roborockCommunication/RESTAPI/roborockIoTApi.test.ts +106 -0
  24. package/src/tests/roborockCommunication/broadcast/client/LocalNetworkClient.test.ts +3 -5
  25. package/src/tests/roborockCommunication/broadcast/clientRouter.test.ts +168 -0
  26. package/src/tests/roborockCommunication/broadcast/messageProcessor.test.ts +131 -0
  27. package/src/tests/roborockService.test.ts +102 -2
  28. package/src/tests/roborockService3.test.ts +133 -0
  29. package/src/tests/roborockService4.test.ts +76 -0
  30. 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.3`**
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.6')) {
30
- throw new Error(`This plugin requires Matterbridge version >= "3.1.6". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend.`);
29
+ if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.1.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
- await this.destroySocket(`Socket disconnected. Had error: ${hadError}`);
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('LocalNetworkClient: Socket connection error: ' + error.message);
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: (...args) => {
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;
@@ -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
- }, 3000);
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 = {}));
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "matterbridge-roborock-vacuum-plugin",
3
3
  "type": "DynamicPlatform",
4
- "version": "1.1.0-rc13",
4
+ "version": "1.1.0-rc14",
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-rc13 by https://github.com/RinDevJunior",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge-roborock-vacuum-plugin",
3
- "version": "1.1.0-rc13",
3
+ "version": "1.1.0-rc14",
4
4
  "description": "Matterbridge Roborock Vacuum Plugin",
5
5
  "author": "https://github.com/RinDevJunior",
6
6
  "license": "MIT",
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.6')) {
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.6". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend.`,
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
- private async onEnd(): Promise<void> {
89
- await this.destroySocket('Socket has ended.');
90
- await this.connectionListeners.onDisconnected(this.duid, 'Socket has ended.');
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
- await this.destroySocket(`Socket disconnected. Had error: ${hadError}`);
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('LocalNetworkClient: Socket connection error: ' + error.message);
120
- this.connected = false;
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
- if (this.pingInterval) {
128
- clearInterval(this.pingInterval);
129
- }
111
+ private async onTimeout(): Promise<void> {
112
+ this.logger.error(` [LocalNetworkClient]: Socket for ${this.duid} timed out.`);
113
+ }
130
114
 
131
- await this.connectionListeners.onError(this.duid, error.message);
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: (...args: unknown[]) => {
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);
@@ -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
- }, 3000);
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}
@@ -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);