matterbridge-roborock-vacuum-plugin 1.1.0-rc09 → 1.1.0-rc10
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/dist/platform.js +5 -0
- package/dist/roborockCommunication/RESTAPI/roborockIoTApi.js +11 -0
- package/dist/roborockCommunication/Zmodel/map.js +1 -0
- package/dist/roborockCommunication/Zmodel/mapInfo.js +24 -0
- package/dist/roborockCommunication/Zmodel/multipleMap.js +1 -0
- package/dist/roborockCommunication/broadcast/abstractClient.js +2 -1
- package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +22 -2
- package/dist/roborockCommunication/broadcast/client/MQTTClient.js +4 -0
- package/dist/roborockCommunication/broadcast/clientRouter.js +2 -2
- package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js +9 -2
- package/dist/roborockCommunication/helper/messageDeserializer.js +2 -2
- package/dist/roborockCommunication/index.js +1 -0
- package/dist/roborockService.js +31 -9
- package/matterbridge-roborock-vacuum-plugin.config.json +1 -1
- package/matterbridge-roborock-vacuum-plugin.schema.json +1 -1
- package/package.json +1 -1
- package/src/platform.ts +6 -0
- package/src/roborockCommunication/RESTAPI/roborockIoTApi.ts +12 -0
- package/src/roborockCommunication/Zmodel/map.ts +14 -0
- package/src/roborockCommunication/Zmodel/mapInfo.ts +31 -0
- package/src/roborockCommunication/Zmodel/multipleMap.ts +8 -0
- package/src/roborockCommunication/broadcast/abstractClient.ts +3 -1
- package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +26 -2
- package/src/roborockCommunication/broadcast/client/MQTTClient.ts +4 -0
- package/src/roborockCommunication/broadcast/clientRouter.ts +2 -2
- package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts +10 -2
- package/src/roborockCommunication/helper/messageDeserializer.ts +2 -2
- package/src/roborockCommunication/index.ts +2 -0
- package/src/roborockService.ts +38 -10
- package/src/tests/roborockService.test.ts +3 -11
package/dist/platform.js
CHANGED
|
@@ -156,6 +156,11 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
156
156
|
this.log.error(`Failed to connect to local network for device: ${vacuum.name} (${vacuum.duid})`);
|
|
157
157
|
return false;
|
|
158
158
|
}
|
|
159
|
+
if (vacuum.rooms === undefined || vacuum.rooms.length === 0) {
|
|
160
|
+
const map_info = await this.roborockService.getMapInformation(vacuum.duid);
|
|
161
|
+
const rooms = map_info?.maps?.[0]?.rooms ?? [];
|
|
162
|
+
vacuum.rooms = rooms;
|
|
163
|
+
}
|
|
159
164
|
const roomMap = await this.platformRunner.getRoomMapFromDevice(vacuum);
|
|
160
165
|
this.log.debug('Initializing - roomMap: ', debugStringify(roomMap));
|
|
161
166
|
const behaviorHandler = configurateBehavior(vacuum.data.model, vacuum.duid, this.roborockService, this.cleanModeSettings, this.enableExperimentalFeature?.advancedFeature?.forceRunAtDefault ?? false, this.log);
|
|
@@ -48,6 +48,17 @@ export class RoborockIoTApi {
|
|
|
48
48
|
return undefined;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
async getHomev3(homeId) {
|
|
52
|
+
const result = await this.api.get('v3/user/homes/' + homeId);
|
|
53
|
+
const apiResponse = result.data;
|
|
54
|
+
if (apiResponse.result) {
|
|
55
|
+
return apiResponse.result;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.logger.error('Failed to retrieve the home data');
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
51
62
|
async getScenes(homeId) {
|
|
52
63
|
const result = await this.api.get('user/scene/home/' + homeId);
|
|
53
64
|
const apiResponse = result.data;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import decodeComponent from '../helper/nameDecoder.js';
|
|
2
|
+
export class MapInfo {
|
|
3
|
+
maps = [];
|
|
4
|
+
constructor(multimap) {
|
|
5
|
+
multimap.map_info.forEach((map) => {
|
|
6
|
+
this.maps.push({
|
|
7
|
+
id: map.mapFlag,
|
|
8
|
+
name: decodeComponent(map.name)?.toLowerCase(),
|
|
9
|
+
rooms: map.rooms.map((room) => {
|
|
10
|
+
return {
|
|
11
|
+
id: room.iot_name_id,
|
|
12
|
+
name: room.iot_name,
|
|
13
|
+
};
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
getById(id) {
|
|
19
|
+
return this.maps.find((m) => m.id === id)?.name;
|
|
20
|
+
}
|
|
21
|
+
getByName(name) {
|
|
22
|
+
return this.maps.find((m) => m.name === name.toLowerCase())?.id;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -6,6 +6,7 @@ import { SyncMessageListener } from './listener/implementation/syncMessageListen
|
|
|
6
6
|
import { ConnectionStateListener } from './listener/implementation/connectionStateListener.js';
|
|
7
7
|
export class AbstractClient {
|
|
8
8
|
isInDisconnectingStep = false;
|
|
9
|
+
retryCount = 0;
|
|
9
10
|
connectionListeners = new ChainedConnectionListener();
|
|
10
11
|
messageListeners = new ChainedMessageListener();
|
|
11
12
|
serializer;
|
|
@@ -23,7 +24,7 @@ export class AbstractClient {
|
|
|
23
24
|
this.logger = logger;
|
|
24
25
|
}
|
|
25
26
|
initializeConnectionStateListener() {
|
|
26
|
-
const connectionStateListener = new ConnectionStateListener(this.logger, this, this.clientName, this.shouldReconnect);
|
|
27
|
+
const connectionStateListener = new ConnectionStateListener(this.logger, this, this.clientName, this.changeToSecureConnection, this.shouldReconnect);
|
|
27
28
|
this.connectionListeners.register(connectionStateListener);
|
|
28
29
|
}
|
|
29
30
|
async get(duid, request) {
|
|
@@ -7,6 +7,7 @@ import { AbstractClient } from '../abstractClient.js';
|
|
|
7
7
|
import { Sequence } from '../../helper/sequence.js';
|
|
8
8
|
import { ChunkBuffer } from '../../helper/chunkBuffer.js';
|
|
9
9
|
export class LocalNetworkClient extends AbstractClient {
|
|
10
|
+
changeToSecureConnection;
|
|
10
11
|
clientName = 'LocalNetworkClient';
|
|
11
12
|
shouldReconnect = true;
|
|
12
13
|
socket = undefined;
|
|
@@ -15,17 +16,23 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
15
16
|
pingInterval;
|
|
16
17
|
duid;
|
|
17
18
|
ip;
|
|
18
|
-
constructor(logger, context, duid, ip) {
|
|
19
|
+
constructor(logger, context, duid, ip, inject) {
|
|
19
20
|
super(logger, context);
|
|
20
21
|
this.duid = duid;
|
|
21
22
|
this.ip = ip;
|
|
22
23
|
this.messageIdSeq = new Sequence(100000, 999999);
|
|
23
24
|
this.initializeConnectionStateListener();
|
|
25
|
+
this.changeToSecureConnection = inject;
|
|
24
26
|
}
|
|
25
27
|
connect() {
|
|
28
|
+
if (this.socket) {
|
|
29
|
+
this.socket.destroy();
|
|
30
|
+
this.socket = undefined;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
26
33
|
this.socket = new Socket();
|
|
27
34
|
this.socket.on('close', this.onDisconnect.bind(this));
|
|
28
|
-
this.socket.on('end', this.
|
|
35
|
+
this.socket.on('end', this.onEnd.bind(this));
|
|
29
36
|
this.socket.on('error', this.onError.bind(this));
|
|
30
37
|
this.socket.on('data', this.onMessage.bind(this));
|
|
31
38
|
this.socket.connect(58867, this.ip, this.onConnect.bind(this));
|
|
@@ -58,6 +65,19 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
58
65
|
await this.sendHelloMessage();
|
|
59
66
|
this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
|
|
60
67
|
await this.connectionListeners.onConnected(this.duid);
|
|
68
|
+
this.retryCount = 0;
|
|
69
|
+
}
|
|
70
|
+
async onEnd() {
|
|
71
|
+
this.logger.notice('LocalNetworkClient: Socket has ended.');
|
|
72
|
+
this.connected = false;
|
|
73
|
+
if (this.socket) {
|
|
74
|
+
this.socket.destroy();
|
|
75
|
+
this.socket = undefined;
|
|
76
|
+
}
|
|
77
|
+
if (this.pingInterval) {
|
|
78
|
+
clearInterval(this.pingInterval);
|
|
79
|
+
}
|
|
80
|
+
await this.connectionListeners.onDisconnected(this.duid);
|
|
61
81
|
}
|
|
62
82
|
async onDisconnect() {
|
|
63
83
|
this.logger.notice('LocalNetworkClient: Socket has disconnected.');
|
|
@@ -3,6 +3,7 @@ import * as CryptoUtils from '../../helper/cryptoHelper.js';
|
|
|
3
3
|
import { AbstractClient } from '../abstractClient.js';
|
|
4
4
|
import { debugStringify } from 'matterbridge/logger';
|
|
5
5
|
export class MQTTClient extends AbstractClient {
|
|
6
|
+
changeToSecureConnection;
|
|
6
7
|
clientName = 'MQTTClient';
|
|
7
8
|
shouldReconnect = false;
|
|
8
9
|
rriot;
|
|
@@ -15,6 +16,9 @@ export class MQTTClient extends AbstractClient {
|
|
|
15
16
|
this.mqttUsername = CryptoUtils.md5hex(userdata.rriot.u + ':' + userdata.rriot.k).substring(2, 10);
|
|
16
17
|
this.mqttPassword = CryptoUtils.md5hex(userdata.rriot.s + ':' + userdata.rriot.k).substring(16);
|
|
17
18
|
this.initializeConnectionStateListener();
|
|
19
|
+
this.changeToSecureConnection = (duid) => {
|
|
20
|
+
this.logger.info(`MqttClient for ${duid} has been disconnected`);
|
|
21
|
+
};
|
|
18
22
|
}
|
|
19
23
|
connect() {
|
|
20
24
|
if (this.client) {
|
|
@@ -20,8 +20,8 @@ export class ClientRouter {
|
|
|
20
20
|
registerDevice(duid, localKey, pv) {
|
|
21
21
|
this.context.registerDevice(duid, localKey, pv);
|
|
22
22
|
}
|
|
23
|
-
registerClient(duid, ip) {
|
|
24
|
-
const localClient = new LocalNetworkClient(this.logger, this.context, duid, ip);
|
|
23
|
+
registerClient(duid, ip, onDisconnect) {
|
|
24
|
+
const localClient = new LocalNetworkClient(this.logger, this.context, duid, ip, onDisconnect);
|
|
25
25
|
localClient.registerConnectionListener(this.connectionListeners);
|
|
26
26
|
localClient.registerMessageListener(this.messageListeners);
|
|
27
27
|
this.localClients.set(duid, localClient);
|
package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js
CHANGED
|
@@ -3,11 +3,13 @@ export class ConnectionStateListener {
|
|
|
3
3
|
client;
|
|
4
4
|
clientName;
|
|
5
5
|
shouldReconnect;
|
|
6
|
-
|
|
6
|
+
changeToSecureConnection;
|
|
7
|
+
constructor(logger, client, clientName, changeToSecureConnection, shouldReconnect = false) {
|
|
7
8
|
this.logger = logger;
|
|
8
9
|
this.client = client;
|
|
9
10
|
this.clientName = clientName;
|
|
10
11
|
this.shouldReconnect = shouldReconnect;
|
|
12
|
+
this.changeToSecureConnection = changeToSecureConnection;
|
|
11
13
|
}
|
|
12
14
|
async onConnected(duid) {
|
|
13
15
|
this.logger.notice(`Device ${duid} connected to ${this.clientName}`);
|
|
@@ -17,7 +19,12 @@ export class ConnectionStateListener {
|
|
|
17
19
|
this.logger.notice(`Device ${duid} disconnected from ${this.clientName}, but re-registration is disabled.`);
|
|
18
20
|
return;
|
|
19
21
|
}
|
|
20
|
-
this.
|
|
22
|
+
if (this.client.retryCount > 10) {
|
|
23
|
+
this.logger.error(`Device with DUID ${duid} has exceeded retry limit, not re-registering.`);
|
|
24
|
+
this.changeToSecureConnection(duid);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
this.client.retryCount++;
|
|
21
28
|
const isInDisconnectingStep = this.client.isInDisconnectingStep;
|
|
22
29
|
if (isInDisconnectingStep) {
|
|
23
30
|
this.logger.info(`Device with DUID ${duid} is in disconnecting step, skipping re-registration.`);
|
|
@@ -56,14 +56,14 @@ export class MessageDeserializer {
|
|
|
56
56
|
return new ResponseMessage(duid, { dps: { id: 0, result: null } });
|
|
57
57
|
}
|
|
58
58
|
if (data.protocol == Protocol.rpc_response || data.protocol == Protocol.general_request) {
|
|
59
|
-
return this.
|
|
59
|
+
return this.deserializeRpcResponse(duid, data);
|
|
60
60
|
}
|
|
61
61
|
else {
|
|
62
62
|
this.logger.error('unknown protocol: ' + data.protocol);
|
|
63
63
|
return new ResponseMessage(duid, { dps: { id: 0, result: null } });
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
deserializeRpcResponse(duid, data) {
|
|
67
67
|
const payload = JSON.parse(data.payload.toString());
|
|
68
68
|
const dps = payload.dps;
|
|
69
69
|
this.parseJsonInDps(dps, Protocol.general_request);
|
|
@@ -7,4 +7,5 @@ export { Protocol } from './broadcast/model/protocol.js';
|
|
|
7
7
|
export { ClientRouter } from './broadcast/clientRouter.js';
|
|
8
8
|
export { DeviceStatus } from './Zmodel/deviceStatus.js';
|
|
9
9
|
export { ResponseMessage } from './broadcast/model/responseMessage.js';
|
|
10
|
+
export { MapInfo } from './Zmodel/mapInfo.js';
|
|
10
11
|
export { Scene } from './Zmodel/scene.js';
|
package/dist/roborockService.js
CHANGED
|
@@ -2,7 +2,7 @@ import assert from 'node:assert';
|
|
|
2
2
|
import { debugStringify } from 'matterbridge/logger';
|
|
3
3
|
import { NotifyMessageTypes } from './notifyMessageTypes.js';
|
|
4
4
|
import { clearInterval } from 'node:timers';
|
|
5
|
-
import { RoborockAuthenticateApi, RoborockIoTApi, MessageProcessor, Protocol, RequestMessage, ResponseMessage, } from './roborockCommunication/index.js';
|
|
5
|
+
import { RoborockAuthenticateApi, RoborockIoTApi, MessageProcessor, Protocol, RequestMessage, ResponseMessage, MapInfo, } from './roborockCommunication/index.js';
|
|
6
6
|
export default class RoborockService {
|
|
7
7
|
loginApi;
|
|
8
8
|
logger;
|
|
@@ -69,6 +69,17 @@ export default class RoborockService {
|
|
|
69
69
|
}
|
|
70
70
|
return data;
|
|
71
71
|
}
|
|
72
|
+
async getRoomIdFromMap(duid) {
|
|
73
|
+
const data = (await this.customGet(duid, new RequestMessage({ method: 'get_map_v1' })));
|
|
74
|
+
return data?.vacuumRoom;
|
|
75
|
+
}
|
|
76
|
+
async getMapInformation(duid) {
|
|
77
|
+
this.logger.debug('RoborockService - getMapInformation', duid);
|
|
78
|
+
assert(this.messageClient !== undefined);
|
|
79
|
+
return this.messageClient.get(duid, new RequestMessage({ method: 'get_multi_maps_list' })).then((response) => {
|
|
80
|
+
return new MapInfo(response[0]);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
72
83
|
async changeCleanMode(duid, { suctionPower, waterFlow, distance_off, mopRoute }) {
|
|
73
84
|
this.logger.notice('RoborockService - changeCleanMode');
|
|
74
85
|
return this.getMessageProcessor(duid)?.changeCleanMode(duid, suctionPower, waterFlow, mopRoute, distance_off);
|
|
@@ -129,13 +140,9 @@ export default class RoborockService {
|
|
|
129
140
|
this.logger.debug('RoborockService - findMe');
|
|
130
141
|
await this.getMessageProcessor(duid)?.findMyRobot(duid);
|
|
131
142
|
}
|
|
132
|
-
async customGet(duid,
|
|
133
|
-
this.logger.debug('RoborockService - customSend-message', method);
|
|
134
|
-
return this.getMessageProcessor(duid)?.getCustomMessage(duid,
|
|
135
|
-
}
|
|
136
|
-
async customGetInSecure(duid, method) {
|
|
137
|
-
this.logger.debug('RoborockService - customGetInSecure-message', method);
|
|
138
|
-
return this.getMessageProcessor(duid)?.getCustomMessage(duid, new RequestMessage({ method, secure: true }));
|
|
143
|
+
async customGet(duid, request) {
|
|
144
|
+
this.logger.debug('RoborockService - customSend-message', request.method, request.params, request.secure);
|
|
145
|
+
return this.getMessageProcessor(duid)?.getCustomMessage(duid, request);
|
|
139
146
|
}
|
|
140
147
|
async customSend(duid, request) {
|
|
141
148
|
return this.getMessageProcessor(duid)?.sendCustomMessage(duid, request);
|
|
@@ -201,6 +208,18 @@ export default class RoborockService {
|
|
|
201
208
|
const scenes = (await this.iotApi.getScenes(homeDetails.rrHomeId)) ?? [];
|
|
202
209
|
const products = new Map();
|
|
203
210
|
homeData.products.forEach((p) => products.set(p.id, p.model));
|
|
211
|
+
if (homeData.rooms.length === 0) {
|
|
212
|
+
const homeDataV2 = await this.iotApi.getHomev2(homeDetails.rrHomeId);
|
|
213
|
+
if (homeDataV2 && homeDataV2.rooms && homeDataV2.rooms.length > 0) {
|
|
214
|
+
homeData.rooms = homeDataV2.rooms;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
const homeDataV3 = await this.iotApi.getHomev3(homeDetails.rrHomeId);
|
|
218
|
+
if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
|
|
219
|
+
homeData.rooms = homeDataV3.rooms;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
204
223
|
const devices = [...homeData.devices, ...homeData.receivedDevices];
|
|
205
224
|
const result = devices.map((device) => {
|
|
206
225
|
return {
|
|
@@ -347,7 +366,7 @@ export default class RoborockService {
|
|
|
347
366
|
}
|
|
348
367
|
if (localIp) {
|
|
349
368
|
this.logger.debug('initializing the local connection for this client towards ' + localIp);
|
|
350
|
-
const localClient = this.messageClient.registerClient(device.duid, localIp);
|
|
369
|
+
const localClient = this.messageClient.registerClient(device.duid, localIp, this.onLocalClientDisconnect);
|
|
351
370
|
localClient.connect();
|
|
352
371
|
let count = 0;
|
|
353
372
|
while (!localClient.isConnected() && count < 20) {
|
|
@@ -369,6 +388,9 @@ export default class RoborockService {
|
|
|
369
388
|
}
|
|
370
389
|
return true;
|
|
371
390
|
}
|
|
391
|
+
onLocalClientDisconnect(duid) {
|
|
392
|
+
this.logger.debug('Local client disconnected for device', duid);
|
|
393
|
+
}
|
|
372
394
|
sleep(ms) {
|
|
373
395
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
374
396
|
}
|
|
@@ -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-rc10 by https://github.com/RinDevJunior",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"required": ["username", "password"],
|
|
6
6
|
"properties": {
|
package/package.json
CHANGED
package/src/platform.ts
CHANGED
|
@@ -223,6 +223,12 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
223
223
|
return false;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
if (vacuum.rooms === undefined || vacuum.rooms.length === 0) {
|
|
227
|
+
const map_info = await this.roborockService.getMapInformation(vacuum.duid);
|
|
228
|
+
const rooms = map_info?.maps?.[0]?.rooms ?? [];
|
|
229
|
+
vacuum.rooms = rooms;
|
|
230
|
+
}
|
|
231
|
+
|
|
226
232
|
const roomMap = await this.platformRunner.getRoomMapFromDevice(vacuum);
|
|
227
233
|
|
|
228
234
|
this.log.debug('Initializing - roomMap: ', debugStringify(roomMap));
|
|
@@ -57,6 +57,18 @@ export class RoborockIoTApi {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
public async getHomev3(homeId: number): Promise<Home | undefined> {
|
|
61
|
+
const result = await this.api.get('v3/user/homes/' + homeId); // can be v3 also
|
|
62
|
+
|
|
63
|
+
const apiResponse: ApiResponse<Home> = result.data;
|
|
64
|
+
if (apiResponse.result) {
|
|
65
|
+
return apiResponse.result;
|
|
66
|
+
} else {
|
|
67
|
+
this.logger.error('Failed to retrieve the home data');
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
public async getScenes(homeId: number): Promise<Scene[] | undefined> {
|
|
61
73
|
const result = await this.api.get('user/scene/home/' + homeId);
|
|
62
74
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import decodeComponent from '../helper/nameDecoder.js';
|
|
2
|
+
import { RoomInformation } from './map.js';
|
|
3
|
+
import type { MultipleMap } from './multipleMap.js';
|
|
4
|
+
import { Room } from './room.js';
|
|
5
|
+
|
|
6
|
+
export class MapInfo {
|
|
7
|
+
readonly maps: { id: number; name: string | undefined; rooms: Room[] }[] = [];
|
|
8
|
+
|
|
9
|
+
constructor(multimap: MultipleMap) {
|
|
10
|
+
multimap.map_info.forEach((map) => {
|
|
11
|
+
this.maps.push({
|
|
12
|
+
id: map.mapFlag,
|
|
13
|
+
name: decodeComponent(map.name)?.toLowerCase(),
|
|
14
|
+
rooms: map.rooms.map((room: RoomInformation) => {
|
|
15
|
+
return {
|
|
16
|
+
id: room.iot_name_id,
|
|
17
|
+
name: room.iot_name,
|
|
18
|
+
} as unknown as Room;
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getById(id: number): string | undefined {
|
|
25
|
+
return this.maps.find((m) => m.id === id)?.name;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getByName(name: string): number | undefined {
|
|
29
|
+
return this.maps.find((m) => m.name === name.toLowerCase())?.id;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -14,6 +14,7 @@ import { ConnectionStateListener } from './listener/implementation/connectionSta
|
|
|
14
14
|
|
|
15
15
|
export abstract class AbstractClient implements Client {
|
|
16
16
|
public isInDisconnectingStep = false;
|
|
17
|
+
public retryCount = 0;
|
|
17
18
|
|
|
18
19
|
protected readonly connectionListeners = new ChainedConnectionListener();
|
|
19
20
|
protected readonly messageListeners = new ChainedMessageListener();
|
|
@@ -24,6 +25,7 @@ export abstract class AbstractClient implements Client {
|
|
|
24
25
|
|
|
25
26
|
protected abstract clientName: string;
|
|
26
27
|
protected abstract shouldReconnect: boolean;
|
|
28
|
+
protected abstract changeToSecureConnection: (duid: string) => void;
|
|
27
29
|
|
|
28
30
|
private readonly context: MessageContext;
|
|
29
31
|
private readonly syncMessageListener: SyncMessageListener;
|
|
@@ -39,7 +41,7 @@ export abstract class AbstractClient implements Client {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
protected initializeConnectionStateListener() {
|
|
42
|
-
const connectionStateListener = new ConnectionStateListener(this.logger, this, this.clientName, this.shouldReconnect);
|
|
44
|
+
const connectionStateListener = new ConnectionStateListener(this.logger, this, this.clientName, this.changeToSecureConnection, this.shouldReconnect);
|
|
43
45
|
this.connectionListeners.register(connectionStateListener);
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -9,6 +9,7 @@ import { Sequence } from '../../helper/sequence.js';
|
|
|
9
9
|
import { ChunkBuffer } from '../../helper/chunkBuffer.js';
|
|
10
10
|
|
|
11
11
|
export class LocalNetworkClient extends AbstractClient {
|
|
12
|
+
protected override changeToSecureConnection: (duid: string) => void;
|
|
12
13
|
protected override clientName = 'LocalNetworkClient';
|
|
13
14
|
protected override shouldReconnect = true;
|
|
14
15
|
|
|
@@ -19,19 +20,26 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
19
20
|
duid: string;
|
|
20
21
|
ip: string;
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
constructor(logger: AnsiLogger, context: MessageContext, duid: string, ip: string, inject: (duid: string) => void) {
|
|
23
24
|
super(logger, context);
|
|
24
25
|
this.duid = duid;
|
|
25
26
|
this.ip = ip;
|
|
26
27
|
this.messageIdSeq = new Sequence(100000, 999999);
|
|
27
28
|
|
|
28
29
|
this.initializeConnectionStateListener();
|
|
30
|
+
this.changeToSecureConnection = inject;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
public connect(): void {
|
|
34
|
+
if (this.socket) {
|
|
35
|
+
this.socket.destroy();
|
|
36
|
+
this.socket = undefined;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
this.socket = new Socket();
|
|
33
41
|
this.socket.on('close', this.onDisconnect.bind(this));
|
|
34
|
-
this.socket.on('end', this.
|
|
42
|
+
this.socket.on('end', this.onEnd.bind(this));
|
|
35
43
|
this.socket.on('error', this.onError.bind(this));
|
|
36
44
|
this.socket.on('data', this.onMessage.bind(this));
|
|
37
45
|
this.socket.connect(58867, this.ip, this.onConnect.bind(this));
|
|
@@ -71,6 +79,22 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
71
79
|
await this.sendHelloMessage();
|
|
72
80
|
this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
|
|
73
81
|
await this.connectionListeners.onConnected(this.duid);
|
|
82
|
+
this.retryCount = 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async onEnd(): Promise<void> {
|
|
86
|
+
this.logger.notice('LocalNetworkClient: Socket has ended.');
|
|
87
|
+
this.connected = false;
|
|
88
|
+
|
|
89
|
+
if (this.socket) {
|
|
90
|
+
this.socket.destroy();
|
|
91
|
+
this.socket = undefined;
|
|
92
|
+
}
|
|
93
|
+
if (this.pingInterval) {
|
|
94
|
+
clearInterval(this.pingInterval);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await this.connectionListeners.onDisconnected(this.duid);
|
|
74
98
|
}
|
|
75
99
|
|
|
76
100
|
private async onDisconnect(): Promise<void> {
|
|
@@ -7,6 +7,7 @@ import { Rriot, UserData } from '../../Zmodel/userData.js';
|
|
|
7
7
|
import { AnsiLogger, debugStringify } from 'matterbridge/logger';
|
|
8
8
|
|
|
9
9
|
export class MQTTClient extends AbstractClient {
|
|
10
|
+
protected override changeToSecureConnection: (duid: string) => void;
|
|
10
11
|
protected override clientName = 'MQTTClient';
|
|
11
12
|
protected override shouldReconnect = false;
|
|
12
13
|
|
|
@@ -23,6 +24,9 @@ export class MQTTClient extends AbstractClient {
|
|
|
23
24
|
this.mqttPassword = CryptoUtils.md5hex(userdata.rriot.s + ':' + userdata.rriot.k).substring(16);
|
|
24
25
|
|
|
25
26
|
this.initializeConnectionStateListener();
|
|
27
|
+
this.changeToSecureConnection = (duid: string) => {
|
|
28
|
+
this.logger.info(`MqttClient for ${duid} has been disconnected`);
|
|
29
|
+
};
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
public connect(): void {
|
|
@@ -32,8 +32,8 @@ export class ClientRouter implements Client {
|
|
|
32
32
|
this.context.registerDevice(duid, localKey, pv);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
public registerClient(duid: string, ip: string): Client {
|
|
36
|
-
const localClient = new LocalNetworkClient(this.logger, this.context, duid, ip);
|
|
35
|
+
public registerClient(duid: string, ip: string, onDisconnect: (duid: string) => void): Client {
|
|
36
|
+
const localClient = new LocalNetworkClient(this.logger, this.context, duid, ip, onDisconnect);
|
|
37
37
|
localClient.registerConnectionListener(this.connectionListeners);
|
|
38
38
|
localClient.registerMessageListener(this.messageListeners);
|
|
39
39
|
|
package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts
CHANGED
|
@@ -7,12 +7,14 @@ export class ConnectionStateListener implements AbstractConnectionListener {
|
|
|
7
7
|
protected client: AbstractClient;
|
|
8
8
|
protected clientName: string;
|
|
9
9
|
protected shouldReconnect: boolean;
|
|
10
|
+
protected changeToSecureConnection: (duid: string) => void;
|
|
10
11
|
|
|
11
|
-
constructor(logger: AnsiLogger, client: AbstractClient, clientName: string, shouldReconnect = false) {
|
|
12
|
+
constructor(logger: AnsiLogger, client: AbstractClient, clientName: string, changeToSecureConnection: (duid: string) => void, shouldReconnect = false) {
|
|
12
13
|
this.logger = logger;
|
|
13
14
|
this.client = client;
|
|
14
15
|
this.clientName = clientName;
|
|
15
16
|
this.shouldReconnect = shouldReconnect;
|
|
17
|
+
this.changeToSecureConnection = changeToSecureConnection;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
public async onConnected(duid: string): Promise<void> {
|
|
@@ -25,7 +27,13 @@ export class ConnectionStateListener implements AbstractConnectionListener {
|
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
this.
|
|
30
|
+
if (this.client.retryCount > 10) {
|
|
31
|
+
this.logger.error(`Device with DUID ${duid} has exceeded retry limit, not re-registering.`);
|
|
32
|
+
this.changeToSecureConnection(duid);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.client.retryCount++;
|
|
29
37
|
|
|
30
38
|
const isInDisconnectingStep = this.client.isInDisconnectingStep;
|
|
31
39
|
if (isInDisconnectingStep) {
|
|
@@ -79,14 +79,14 @@ export class MessageDeserializer {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
if (data.protocol == Protocol.rpc_response || data.protocol == Protocol.general_request) {
|
|
82
|
-
return this.
|
|
82
|
+
return this.deserializeRpcResponse(duid, data);
|
|
83
83
|
} else {
|
|
84
84
|
this.logger.error('unknown protocol: ' + data.protocol);
|
|
85
85
|
return new ResponseMessage(duid, { dps: { id: 0, result: null } });
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
private
|
|
89
|
+
private deserializeRpcResponse(duid: string, data: Message): ResponseMessage {
|
|
90
90
|
const payload = JSON.parse(data.payload.toString());
|
|
91
91
|
const dps = payload.dps;
|
|
92
92
|
this.parseJsonInDps(dps, Protocol.general_request);
|
|
@@ -7,6 +7,7 @@ export { Protocol } from './broadcast/model/protocol.js';
|
|
|
7
7
|
export { ClientRouter } from './broadcast/clientRouter.js';
|
|
8
8
|
export { DeviceStatus } from './Zmodel/deviceStatus.js';
|
|
9
9
|
export { ResponseMessage } from './broadcast/model/responseMessage.js';
|
|
10
|
+
export { MapInfo } from './Zmodel/mapInfo.js';
|
|
10
11
|
|
|
11
12
|
export { Scene } from './Zmodel/scene.js';
|
|
12
13
|
|
|
@@ -18,3 +19,4 @@ export type { Home } from './Zmodel/home.js';
|
|
|
18
19
|
export type { Client } from './broadcast/client.js';
|
|
19
20
|
export type { SceneParam } from './Zmodel/scene.js';
|
|
20
21
|
export type { BatteryMessage, DeviceErrorMessage, DeviceStatusNotify } from './Zmodel/batteryMessage.js';
|
|
22
|
+
export type { MultipleMap } from './Zmodel/multipleMap.js';
|
package/src/roborockService.ts
CHANGED
|
@@ -19,8 +19,9 @@ import {
|
|
|
19
19
|
ResponseMessage,
|
|
20
20
|
Scene,
|
|
21
21
|
SceneParam,
|
|
22
|
+
MapInfo,
|
|
22
23
|
} from './roborockCommunication/index.js';
|
|
23
|
-
import type { AbstractMessageHandler, AbstractMessageListener, BatteryMessage, DeviceErrorMessage, DeviceStatusNotify } from './roborockCommunication/index.js';
|
|
24
|
+
import type { AbstractMessageHandler, AbstractMessageListener, BatteryMessage, DeviceErrorMessage, DeviceStatusNotify, MultipleMap } from './roborockCommunication/index.js';
|
|
24
25
|
import { ServiceArea } from 'matterbridge/matter/clusters';
|
|
25
26
|
import { LocalNetworkClient } from './roborockCommunication/broadcast/client/LocalNetworkClient.js';
|
|
26
27
|
export type Factory<A, T> = (logger: AnsiLogger, arg: A) => T;
|
|
@@ -115,6 +116,19 @@ export default class RoborockService {
|
|
|
115
116
|
return data;
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
public async getRoomIdFromMap(duid: string): Promise<number | undefined> {
|
|
120
|
+
const data = (await this.customGet(duid, new RequestMessage({ method: 'get_map_v1' }))) as { vacuumRoom: number | undefined };
|
|
121
|
+
return data?.vacuumRoom;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public async getMapInformation(duid: string): Promise<MapInfo> {
|
|
125
|
+
this.logger.debug('RoborockService - getMapInformation', duid);
|
|
126
|
+
assert(this.messageClient !== undefined);
|
|
127
|
+
return this.messageClient.get<MultipleMap[]>(duid, new RequestMessage({ method: 'get_multi_maps_list' })).then((response) => {
|
|
128
|
+
return new MapInfo(response[0]);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
public async changeCleanMode(
|
|
119
133
|
duid: string,
|
|
120
134
|
{ suctionPower, waterFlow, distance_off, mopRoute }: { suctionPower: number; waterFlow: number; distance_off: number; mopRoute: number },
|
|
@@ -189,14 +203,9 @@ export default class RoborockService {
|
|
|
189
203
|
await this.getMessageProcessor(duid)?.findMyRobot(duid);
|
|
190
204
|
}
|
|
191
205
|
|
|
192
|
-
public async customGet(duid: string,
|
|
193
|
-
this.logger.debug('RoborockService - customSend-message', method);
|
|
194
|
-
return this.getMessageProcessor(duid)?.getCustomMessage(duid,
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
public async customGetInSecure(duid: string, method: string): Promise<unknown> {
|
|
198
|
-
this.logger.debug('RoborockService - customGetInSecure-message', method);
|
|
199
|
-
return this.getMessageProcessor(duid)?.getCustomMessage(duid, new RequestMessage({ method, secure: true }));
|
|
206
|
+
public async customGet(duid: string, request: RequestMessage): Promise<unknown> {
|
|
207
|
+
this.logger.debug('RoborockService - customSend-message', request.method, request.params, request.secure);
|
|
208
|
+
return this.getMessageProcessor(duid)?.getCustomMessage(duid, request);
|
|
200
209
|
}
|
|
201
210
|
|
|
202
211
|
public async customSend(duid: string, request: RequestMessage): Promise<void> {
|
|
@@ -274,6 +283,20 @@ export default class RoborockService {
|
|
|
274
283
|
|
|
275
284
|
const products = new Map<string, string>();
|
|
276
285
|
homeData.products.forEach((p) => products.set(p.id, p.model));
|
|
286
|
+
|
|
287
|
+
// Try to get rooms from v2 API if rooms are empty
|
|
288
|
+
if (homeData.rooms.length === 0) {
|
|
289
|
+
const homeDataV2 = await this.iotApi.getHomev2(homeDetails.rrHomeId);
|
|
290
|
+
if (homeDataV2 && homeDataV2.rooms && homeDataV2.rooms.length > 0) {
|
|
291
|
+
homeData.rooms = homeDataV2.rooms;
|
|
292
|
+
} else {
|
|
293
|
+
const homeDataV3 = await this.iotApi.getHomev3(homeDetails.rrHomeId);
|
|
294
|
+
if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
|
|
295
|
+
homeData.rooms = homeDataV3.rooms;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
277
300
|
const devices: Device[] = [...homeData.devices, ...homeData.receivedDevices];
|
|
278
301
|
// homeData.devices.length > 0 ? homeData.devices : homeData.receivedDevices;
|
|
279
302
|
|
|
@@ -457,7 +480,7 @@ export default class RoborockService {
|
|
|
457
480
|
|
|
458
481
|
if (localIp) {
|
|
459
482
|
this.logger.debug('initializing the local connection for this client towards ' + localIp);
|
|
460
|
-
const localClient = this.messageClient.registerClient(device.duid, localIp) as LocalNetworkClient;
|
|
483
|
+
const localClient = this.messageClient.registerClient(device.duid, localIp, this.onLocalClientDisconnect) as LocalNetworkClient;
|
|
461
484
|
localClient.connect();
|
|
462
485
|
|
|
463
486
|
let count = 0;
|
|
@@ -483,6 +506,11 @@ export default class RoborockService {
|
|
|
483
506
|
return true;
|
|
484
507
|
}
|
|
485
508
|
|
|
509
|
+
private onLocalClientDisconnect(duid: string): void {
|
|
510
|
+
// this.mqttAlwaysOnDevices.set(duid, true);
|
|
511
|
+
this.logger.debug('Local client disconnected for device', duid);
|
|
512
|
+
}
|
|
513
|
+
|
|
486
514
|
private sleep(ms: number): Promise<void> {
|
|
487
515
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
488
516
|
}
|
|
@@ -2,7 +2,7 @@ import { AnsiLogger } from 'matterbridge/logger';
|
|
|
2
2
|
import { ServiceArea } from 'matterbridge/matter/clusters';
|
|
3
3
|
import RoborockService from '../roborockService';
|
|
4
4
|
import { MessageProcessor } from '../roborockCommunication/broadcast/messageProcessor';
|
|
5
|
-
import { Device } from '../roborockCommunication';
|
|
5
|
+
import { Device, RequestMessage } from '../roborockCommunication';
|
|
6
6
|
|
|
7
7
|
describe('RoborockService - startClean', () => {
|
|
8
8
|
let roborockService: RoborockService;
|
|
@@ -316,20 +316,12 @@ describe('RoborockService - customGet/customGetInSecure/customSend', () => {
|
|
|
316
316
|
|
|
317
317
|
it('customGet should call getCustomMessage', async () => {
|
|
318
318
|
mockMessageProcessor.getCustomMessage.mockResolvedValue('result');
|
|
319
|
-
const result = await roborockService.customGet('duid', 'method');
|
|
320
|
-
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - customSend-message', 'method');
|
|
319
|
+
const result = await roborockService.customGet('duid', { method: 'method', params: undefined, secure: true } as RequestMessage);
|
|
320
|
+
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - customSend-message', 'method', undefined, true);
|
|
321
321
|
expect(mockMessageProcessor.getCustomMessage).toHaveBeenCalledWith('duid', expect.any(Object));
|
|
322
322
|
expect(result).toBe('result');
|
|
323
323
|
});
|
|
324
324
|
|
|
325
|
-
it('customGetInSecure should call getCustomMessage with secure', async () => {
|
|
326
|
-
mockMessageProcessor.getCustomMessage.mockResolvedValue('secureResult');
|
|
327
|
-
const result = await roborockService.customGetInSecure('duid', 'method');
|
|
328
|
-
expect(mockLogger.debug).toHaveBeenCalledWith('RoborockService - customGetInSecure-message', 'method');
|
|
329
|
-
expect(mockMessageProcessor.getCustomMessage).toHaveBeenCalledWith('duid', expect.objectContaining({ secure: true }));
|
|
330
|
-
expect(result).toBe('secureResult');
|
|
331
|
-
});
|
|
332
|
-
|
|
333
325
|
it('customSend should call sendCustomMessage', async () => {
|
|
334
326
|
const req = { foo: 'bar' } as any;
|
|
335
327
|
await roborockService.customSend('duid', req);
|