matterbridge-roborock-vacuum-plugin 1.1.0-rc09 → 1.1.0-rc11
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 +6 -4
- package/dist/roborockCommunication/RESTAPI/roborockIoTApi.js +22 -0
- package/dist/roborockCommunication/Zmodel/map.js +1 -0
- package/dist/roborockCommunication/Zmodel/mapInfo.js +26 -0
- package/dist/roborockCommunication/Zmodel/multipleMap.js +1 -0
- package/dist/roborockCommunication/broadcast/abstractClient.js +10 -2
- 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/broadcast/listener/implementation/syncMessageListener.js +3 -2
- package/dist/roborockCommunication/broadcast/messageProcessor.js +6 -3
- package/dist/roborockCommunication/helper/messageDeserializer.js +9 -3
- package/dist/roborockCommunication/helper/messageSerializer.js +6 -0
- package/dist/roborockCommunication/index.js +1 -0
- package/dist/roborockService.js +84 -13
- package/eslint.config.js +1 -1
- 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 +7 -5
- package/src/roborockCommunication/RESTAPI/roborockIoTApi.ts +25 -1
- package/src/roborockCommunication/Zmodel/device.ts +1 -0
- package/src/roborockCommunication/Zmodel/map.ts +14 -0
- package/src/roborockCommunication/Zmodel/mapInfo.ts +34 -0
- package/src/roborockCommunication/Zmodel/multipleMap.ts +8 -0
- package/src/roborockCommunication/Zmodel/userData.ts +0 -3
- package/src/roborockCommunication/broadcast/abstractClient.ts +13 -4
- package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +26 -2
- package/src/roborockCommunication/broadcast/client/MQTTClient.ts +4 -0
- package/src/roborockCommunication/broadcast/client.ts +1 -1
- package/src/roborockCommunication/broadcast/clientRouter.ts +3 -3
- package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts +10 -2
- package/src/roborockCommunication/broadcast/listener/implementation/syncMessageListener.ts +4 -3
- package/src/roborockCommunication/broadcast/messageProcessor.ts +9 -5
- package/src/roborockCommunication/helper/messageDeserializer.ts +9 -3
- package/src/roborockCommunication/helper/messageSerializer.ts +6 -0
- package/src/roborockCommunication/index.ts +2 -0
- package/src/roborockService.ts +96 -17
- package/src/tests/roborockCommunication/broadcast/listener/implementation/syncMessageListener.test.ts +5 -4
- package/src/tests/roborockService.test.ts +3 -11
- package/web-for-testing/README.md +47 -0
- package/web-for-testing/nodemon.json +7 -0
- package/web-for-testing/package-lock.json +6598 -0
- package/web-for-testing/package.json +36 -0
- package/web-for-testing/src/accountStore.ts +8 -0
- package/web-for-testing/src/app.ts +194 -0
- package/web-for-testing/tsconfig-ext.json +19 -0
- package/web-for-testing/tsconfig.json +23 -0
- package/web-for-testing/views/index.ejs +172 -0
- package/web-for-testing/watch.mjs +93 -0
package/dist/platform.js
CHANGED
|
@@ -140,7 +140,7 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
140
140
|
if (!configurateSuccess.get(duid)) {
|
|
141
141
|
continue;
|
|
142
142
|
}
|
|
143
|
-
|
|
143
|
+
this.roborockService.activateDeviceNotify(robot.device);
|
|
144
144
|
}
|
|
145
145
|
await this.platformRunner?.requestHomeData();
|
|
146
146
|
this.log.info('onConfigurateDevice finished');
|
|
@@ -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);
|
|
@@ -191,7 +196,4 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
191
196
|
this.log.logLevel = logLevel;
|
|
192
197
|
return Promise.resolve();
|
|
193
198
|
}
|
|
194
|
-
sleep(ms) {
|
|
195
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
196
|
-
}
|
|
197
199
|
}
|
|
@@ -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;
|
|
@@ -70,4 +81,15 @@ export class RoborockIoTApi {
|
|
|
70
81
|
return undefined;
|
|
71
82
|
}
|
|
72
83
|
}
|
|
84
|
+
async getCustom(url) {
|
|
85
|
+
const result = await this.api.get(url);
|
|
86
|
+
const apiResponse = result.data;
|
|
87
|
+
if (apiResponse.result) {
|
|
88
|
+
return apiResponse.result;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.logger.error('Failed to execute scene');
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
73
95
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
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.rooms.length > 0
|
|
10
|
+
? map.rooms.map((room) => {
|
|
11
|
+
return {
|
|
12
|
+
id: room.iot_name_id,
|
|
13
|
+
name: room.iot_name,
|
|
14
|
+
};
|
|
15
|
+
})
|
|
16
|
+
: [],
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
getById(id) {
|
|
21
|
+
return this.maps.find((m) => m.id === id)?.name;
|
|
22
|
+
}
|
|
23
|
+
getByName(name) {
|
|
24
|
+
return this.maps.find((m) => m.name === name.toLowerCase())?.id;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -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,13 +24,20 @@ 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) {
|
|
30
31
|
return new Promise((resolve, reject) => {
|
|
31
|
-
this.syncMessageListener.waitFor(request.messageId, (response) => resolve(response), reject);
|
|
32
|
+
this.syncMessageListener.waitFor(request.messageId, request, (response) => resolve(response), reject);
|
|
32
33
|
this.send(duid, request);
|
|
34
|
+
})
|
|
35
|
+
.then((result) => {
|
|
36
|
+
return result;
|
|
37
|
+
})
|
|
38
|
+
.catch((error) => {
|
|
39
|
+
this.logger.error(error.message);
|
|
40
|
+
return undefined;
|
|
33
41
|
});
|
|
34
42
|
}
|
|
35
43
|
registerDevice(duid, localKey, pv) {
|
|
@@ -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.`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { debugStringify } from 'matterbridge/logger';
|
|
1
2
|
import { Protocol } from '../../model/protocol.js';
|
|
2
3
|
export class SyncMessageListener {
|
|
3
4
|
pending = new Map();
|
|
@@ -5,11 +6,11 @@ export class SyncMessageListener {
|
|
|
5
6
|
constructor(logger) {
|
|
6
7
|
this.logger = logger;
|
|
7
8
|
}
|
|
8
|
-
waitFor(messageId, resolve, reject) {
|
|
9
|
+
waitFor(messageId, request, resolve, reject) {
|
|
9
10
|
this.pending.set(messageId, resolve);
|
|
10
11
|
setTimeout(() => {
|
|
11
12
|
this.pending.delete(messageId);
|
|
12
|
-
reject();
|
|
13
|
+
reject(new Error(`Message timeout for messageId: ${messageId}, request: ${debugStringify(request)}`));
|
|
13
14
|
}, 10000);
|
|
14
15
|
}
|
|
15
16
|
async onMessage(message) {
|
|
@@ -25,12 +25,15 @@ export class MessageProcessor {
|
|
|
25
25
|
async getDeviceStatus(duid) {
|
|
26
26
|
const request = new RequestMessage({ method: 'get_status' });
|
|
27
27
|
const response = await this.client.get(duid, request);
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
if (response) {
|
|
29
|
+
this.logger?.debug('Device status: ', debugStringify(response));
|
|
30
|
+
return new DeviceStatus(response);
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
30
33
|
}
|
|
31
34
|
async getRooms(duid, rooms) {
|
|
32
35
|
const request = new RequestMessage({ method: 'get_room_mapping' });
|
|
33
|
-
return this.client.get(duid, request).then((response) => new RoomInfo(rooms, response));
|
|
36
|
+
return this.client.get(duid, request).then((response) => new RoomInfo(rooms, response ?? []));
|
|
34
37
|
}
|
|
35
38
|
async gotoDock(duid) {
|
|
36
39
|
const request = new RequestMessage({ method: 'app_charge' });
|
|
@@ -8,6 +8,7 @@ export class MessageDeserializer {
|
|
|
8
8
|
context;
|
|
9
9
|
messageParser;
|
|
10
10
|
logger;
|
|
11
|
+
supportedVersions = ['1.0', 'A01', 'B01'];
|
|
11
12
|
constructor(context, logger) {
|
|
12
13
|
this.context = context;
|
|
13
14
|
this.logger = logger;
|
|
@@ -28,7 +29,7 @@ export class MessageDeserializer {
|
|
|
28
29
|
}
|
|
29
30
|
deserialize(duid, message) {
|
|
30
31
|
const version = message.toString('latin1', 0, 3);
|
|
31
|
-
if (
|
|
32
|
+
if (!this.supportedVersions.includes(version)) {
|
|
32
33
|
throw new Error('unknown protocol version ' + version);
|
|
33
34
|
}
|
|
34
35
|
const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0;
|
|
@@ -52,18 +53,23 @@ export class MessageDeserializer {
|
|
|
52
53
|
const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
|
|
53
54
|
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
54
55
|
}
|
|
56
|
+
else if (version == 'B01') {
|
|
57
|
+
const iv = CryptoUtils.md5hex(data.random.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25);
|
|
58
|
+
const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
|
|
59
|
+
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
60
|
+
}
|
|
55
61
|
if (data.protocol == Protocol.map_response) {
|
|
56
62
|
return new ResponseMessage(duid, { dps: { id: 0, result: null } });
|
|
57
63
|
}
|
|
58
64
|
if (data.protocol == Protocol.rpc_response || data.protocol == Protocol.general_request) {
|
|
59
|
-
return this.
|
|
65
|
+
return this.deserializeRpcResponse(duid, data);
|
|
60
66
|
}
|
|
61
67
|
else {
|
|
62
68
|
this.logger.error('unknown protocol: ' + data.protocol);
|
|
63
69
|
return new ResponseMessage(duid, { dps: { id: 0, result: null } });
|
|
64
70
|
}
|
|
65
71
|
}
|
|
66
|
-
|
|
72
|
+
deserializeRpcResponse(duid, data) {
|
|
67
73
|
const payload = JSON.parse(data.payload.toString());
|
|
68
74
|
const dps = payload.dps;
|
|
69
75
|
this.parseJsonInDps(dps, Protocol.general_request);
|
|
@@ -54,6 +54,12 @@ export class MessageSerializer {
|
|
|
54
54
|
const cipher = crypto.createCipheriv('aes-128-cbc', encoder.encode(localKey), iv);
|
|
55
55
|
encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
56
56
|
}
|
|
57
|
+
else if (version == 'B01') {
|
|
58
|
+
const encoder = new TextEncoder();
|
|
59
|
+
const iv = CryptoUtils.md5hex(this.random.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25);
|
|
60
|
+
const cipher = crypto.createCipheriv('aes-128-cbc', encoder.encode(localKey), iv);
|
|
61
|
+
encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
62
|
+
}
|
|
57
63
|
else {
|
|
58
64
|
throw new Error('unable to build the message: unsupported protocol version: ' + version);
|
|
59
65
|
}
|
|
@@ -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;
|
|
@@ -15,12 +15,14 @@ export default class RoborockService {
|
|
|
15
15
|
messageProcessorMap = new Map();
|
|
16
16
|
ipMap = new Map();
|
|
17
17
|
localClientMap = new Map();
|
|
18
|
+
mqttAlwaysOnDevices = new Map();
|
|
18
19
|
clientManager;
|
|
19
20
|
refreshInterval;
|
|
20
21
|
requestDeviceStatusInterval;
|
|
21
22
|
supportedAreas = new Map();
|
|
22
23
|
supportedRoutines = new Map();
|
|
23
24
|
selectedAreas = new Map();
|
|
25
|
+
vacuumNeedAPIV3 = ['roborock.vacuum.ss07'];
|
|
24
26
|
constructor(authenticateApiSupplier = (logger) => new RoborockAuthenticateApi(logger), iotApiSupplier = (logger, ud) => new RoborockIoTApi(ud, logger), refreshInterval, clientManager, logger) {
|
|
25
27
|
this.logger = logger;
|
|
26
28
|
this.loginApi = authenticateApiSupplier(logger);
|
|
@@ -69,6 +71,18 @@ export default class RoborockService {
|
|
|
69
71
|
}
|
|
70
72
|
return data;
|
|
71
73
|
}
|
|
74
|
+
async getRoomIdFromMap(duid) {
|
|
75
|
+
const data = (await this.customGet(duid, new RequestMessage({ method: 'get_map_v1' })));
|
|
76
|
+
return data?.vacuumRoom;
|
|
77
|
+
}
|
|
78
|
+
async getMapInformation(duid) {
|
|
79
|
+
this.logger.debug('RoborockService - getMapInformation', duid);
|
|
80
|
+
assert(this.messageClient !== undefined);
|
|
81
|
+
return this.messageClient.get(duid, new RequestMessage({ method: 'get_multi_maps_list' })).then((response) => {
|
|
82
|
+
this.logger.debug('RoborockService - getMapInformation response', debugStringify(response ?? []));
|
|
83
|
+
return response ? new MapInfo(response[0]) : undefined;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
72
86
|
async changeCleanMode(duid, { suctionPower, waterFlow, distance_off, mopRoute }) {
|
|
73
87
|
this.logger.notice('RoborockService - changeCleanMode');
|
|
74
88
|
return this.getMessageProcessor(duid)?.changeCleanMode(duid, suctionPower, waterFlow, mopRoute, distance_off);
|
|
@@ -129,17 +143,24 @@ export default class RoborockService {
|
|
|
129
143
|
this.logger.debug('RoborockService - findMe');
|
|
130
144
|
await this.getMessageProcessor(duid)?.findMyRobot(duid);
|
|
131
145
|
}
|
|
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 }));
|
|
146
|
+
async customGet(duid, request) {
|
|
147
|
+
this.logger.debug('RoborockService - customSend-message', request.method, request.params, request.secure);
|
|
148
|
+
return this.getMessageProcessor(duid)?.getCustomMessage(duid, request);
|
|
139
149
|
}
|
|
140
150
|
async customSend(duid, request) {
|
|
141
151
|
return this.getMessageProcessor(duid)?.sendCustomMessage(duid, request);
|
|
142
152
|
}
|
|
153
|
+
async getCustomAPI(url) {
|
|
154
|
+
this.logger.debug('RoborockService - getCustomAPI', url);
|
|
155
|
+
assert(this.iotApi !== undefined);
|
|
156
|
+
try {
|
|
157
|
+
return await this.iotApi.getCustom(url);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
this.logger.error(`Failed to get custom API with url ${url}: ${error ? debugStringify(error) : 'undefined'}`);
|
|
161
|
+
return { result: undefined, error: `Failed to get custom API with url ${url}` };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
143
164
|
stopService() {
|
|
144
165
|
if (this.messageClient) {
|
|
145
166
|
this.messageClient.disconnect();
|
|
@@ -168,14 +189,14 @@ export default class RoborockService {
|
|
|
168
189
|
setDeviceNotify(callback) {
|
|
169
190
|
this.deviceNotify = callback;
|
|
170
191
|
}
|
|
171
|
-
|
|
192
|
+
activateDeviceNotify(device) {
|
|
172
193
|
const self = this;
|
|
173
194
|
this.logger.debug('Requesting device info for device', device.duid);
|
|
174
195
|
const messageProcessor = this.getMessageProcessor(device.duid);
|
|
175
196
|
this.requestDeviceStatusInterval = setInterval(async () => {
|
|
176
197
|
if (messageProcessor) {
|
|
177
198
|
await messageProcessor.getDeviceStatus(device.duid).then((response) => {
|
|
178
|
-
if (self.deviceNotify) {
|
|
199
|
+
if (self.deviceNotify && response) {
|
|
179
200
|
const message = { duid: device.duid, ...response.errorStatus, ...response.message };
|
|
180
201
|
self.logger.debug('Device status update', debugStringify(message));
|
|
181
202
|
self.deviceNotify(NotifyMessageTypes.LocalMessage, message);
|
|
@@ -201,6 +222,27 @@ export default class RoborockService {
|
|
|
201
222
|
const scenes = (await this.iotApi.getScenes(homeDetails.rrHomeId)) ?? [];
|
|
202
223
|
const products = new Map();
|
|
203
224
|
homeData.products.forEach((p) => products.set(p.id, p.model));
|
|
225
|
+
if (homeData.products.some((p) => this.vacuumNeedAPIV3.includes(p.model))) {
|
|
226
|
+
this.logger.debug('Using v3 API for home data retrieval');
|
|
227
|
+
const homeDataV3 = await this.iotApi.getHomev3(homeDetails.rrHomeId);
|
|
228
|
+
if (!homeDataV3) {
|
|
229
|
+
throw new Error('Failed to retrieve the home data from v3 API');
|
|
230
|
+
}
|
|
231
|
+
homeData.devices = [...homeData.devices, ...homeDataV3.devices.filter((d) => !homeData.devices.some((x) => x.duid === d.duid))];
|
|
232
|
+
homeData.receivedDevices = [...homeData.receivedDevices, ...homeDataV3.receivedDevices.filter((d) => !homeData.receivedDevices.some((x) => x.duid === d.duid))];
|
|
233
|
+
}
|
|
234
|
+
if (homeData.rooms.length === 0) {
|
|
235
|
+
const homeDataV2 = await this.iotApi.getHomev2(homeDetails.rrHomeId);
|
|
236
|
+
if (homeDataV2 && homeDataV2.rooms && homeDataV2.rooms.length > 0) {
|
|
237
|
+
homeData.rooms = homeDataV2.rooms;
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
const homeDataV3 = await this.iotApi.getHomev3(homeDetails.rrHomeId);
|
|
241
|
+
if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
|
|
242
|
+
homeData.rooms = homeDataV3.rooms;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
204
246
|
const devices = [...homeData.devices, ...homeData.receivedDevices];
|
|
205
247
|
const result = devices.map((device) => {
|
|
206
248
|
return {
|
|
@@ -218,6 +260,7 @@ export default class RoborockService {
|
|
|
218
260
|
model: homeData.products.find((p) => p.id === device.productId)?.model,
|
|
219
261
|
category: homeData.products.find((p) => p.id === device.productId)?.category,
|
|
220
262
|
batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100,
|
|
263
|
+
schema: homeData.products.find((p) => p.id === device.productId)?.schema,
|
|
221
264
|
},
|
|
222
265
|
store: {
|
|
223
266
|
username: username,
|
|
@@ -240,6 +283,18 @@ export default class RoborockService {
|
|
|
240
283
|
const products = new Map();
|
|
241
284
|
homeData.products.forEach((p) => products.set(p.id, p.model));
|
|
242
285
|
const devices = homeData.devices.length > 0 ? homeData.devices : homeData.receivedDevices;
|
|
286
|
+
if (homeData.rooms.length === 0) {
|
|
287
|
+
const homeDataV3 = await this.iotApi.getHomev3(homeid);
|
|
288
|
+
if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
|
|
289
|
+
homeData.rooms = homeDataV3.rooms;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const homeDataV1 = await this.iotApi.getHome(homeid);
|
|
293
|
+
if (homeDataV1 && homeDataV1.rooms && homeDataV1.rooms.length > 0) {
|
|
294
|
+
homeData.rooms = homeDataV1.rooms;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
243
298
|
const dvs = devices.map((device) => {
|
|
244
299
|
return {
|
|
245
300
|
...device,
|
|
@@ -253,6 +308,7 @@ export default class RoborockService {
|
|
|
253
308
|
model: homeData.products.find((p) => p.id === device.productId)?.model,
|
|
254
309
|
category: homeData.products.find((p) => p.id === device.productId)?.category,
|
|
255
310
|
batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100,
|
|
311
|
+
schema: homeData.products.find((p) => p.id === device.productId)?.schema,
|
|
256
312
|
},
|
|
257
313
|
store: {
|
|
258
314
|
userData: this.userdata,
|
|
@@ -278,9 +334,9 @@ export default class RoborockService {
|
|
|
278
334
|
getRoomMappings(duid) {
|
|
279
335
|
if (!this.messageClient) {
|
|
280
336
|
this.logger.warn('messageClient not initialized. Waititing for next execution');
|
|
281
|
-
return undefined;
|
|
337
|
+
return Promise.resolve(undefined);
|
|
282
338
|
}
|
|
283
|
-
return this.messageClient.get(duid, new RequestMessage({ method: 'get_room_mapping' }));
|
|
339
|
+
return this.messageClient.get(duid, new RequestMessage({ method: 'get_room_mapping', secure: this.isRequestSecure(duid) }));
|
|
284
340
|
}
|
|
285
341
|
async initializeMessageClient(username, device, userdata) {
|
|
286
342
|
if (this.clientManager === undefined) {
|
|
@@ -332,6 +388,15 @@ export default class RoborockService {
|
|
|
332
388
|
},
|
|
333
389
|
});
|
|
334
390
|
this.messageProcessorMap.set(device.duid, messageProcessor);
|
|
391
|
+
this.logger.debug('Checking if device supports local connection', device.pv, device.data.model, device.duid);
|
|
392
|
+
if (device.pv === 'B01') {
|
|
393
|
+
this.logger.warn('Device does not support local connection', device.duid);
|
|
394
|
+
this.mqttAlwaysOnDevices.set(device.duid, true);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
this.mqttAlwaysOnDevices.set(device.duid, false);
|
|
399
|
+
}
|
|
335
400
|
this.logger.debug('Local device', device.duid);
|
|
336
401
|
let localIp = this.ipMap.get(device.duid);
|
|
337
402
|
try {
|
|
@@ -347,7 +412,7 @@ export default class RoborockService {
|
|
|
347
412
|
}
|
|
348
413
|
if (localIp) {
|
|
349
414
|
this.logger.debug('initializing the local connection for this client towards ' + localIp);
|
|
350
|
-
const localClient = this.messageClient.registerClient(device.duid, localIp);
|
|
415
|
+
const localClient = this.messageClient.registerClient(device.duid, localIp, this.onLocalClientDisconnect);
|
|
351
416
|
localClient.connect();
|
|
352
417
|
let count = 0;
|
|
353
418
|
while (!localClient.isConnected() && count < 20) {
|
|
@@ -369,6 +434,9 @@ export default class RoborockService {
|
|
|
369
434
|
}
|
|
370
435
|
return true;
|
|
371
436
|
}
|
|
437
|
+
onLocalClientDisconnect(duid) {
|
|
438
|
+
this.mqttAlwaysOnDevices.set(duid, true);
|
|
439
|
+
}
|
|
372
440
|
sleep(ms) {
|
|
373
441
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
374
442
|
}
|
|
@@ -377,4 +445,7 @@ export default class RoborockService {
|
|
|
377
445
|
this.iotApi = this.iotApiFactory(this.logger, userdata);
|
|
378
446
|
return userdata;
|
|
379
447
|
}
|
|
448
|
+
isRequestSecure(duid) {
|
|
449
|
+
return this.mqttAlwaysOnDevices.get(duid) ?? false;
|
|
450
|
+
}
|
|
380
451
|
}
|
package/eslint.config.js
CHANGED
|
@@ -9,7 +9,7 @@ import eslintPluginN from 'eslint-plugin-n';
|
|
|
9
9
|
export default [
|
|
10
10
|
{
|
|
11
11
|
name: 'global ignores',
|
|
12
|
-
ignores: ['dist/', 'build/', 'node_modules/', 'coverage/', 'frontend/', 'rock-s0/', 'webui/', 'exampleData/', '.shouldnotcommit/'],
|
|
12
|
+
ignores: ['dist/', 'build/', 'node_modules/', 'coverage/', 'frontend/', 'rock-s0/', 'webui/', 'exampleData/', '.shouldnotcommit/', 'web-for-testing/'],
|
|
13
13
|
},
|
|
14
14
|
eslint.configs.recommended,
|
|
15
15
|
...tseslint.configs.strict,
|
|
@@ -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-rc11 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
|
@@ -199,7 +199,7 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
199
199
|
if (!configurateSuccess.get(duid)) {
|
|
200
200
|
continue;
|
|
201
201
|
}
|
|
202
|
-
|
|
202
|
+
this.roborockService.activateDeviceNotify(robot.device);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
await this.platformRunner?.requestHomeData();
|
|
@@ -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));
|
|
@@ -273,8 +279,4 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
273
279
|
this.log.logLevel = logLevel;
|
|
274
280
|
return Promise.resolve();
|
|
275
281
|
}
|
|
276
|
-
|
|
277
|
-
private sleep(ms: number): Promise<void> {
|
|
278
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
279
|
-
}
|
|
280
282
|
}
|
|
@@ -46,7 +46,19 @@ export class RoborockIoTApi {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
public async getHomev2(homeId: number): Promise<Home | undefined> {
|
|
49
|
-
const result = await this.api.get('v2/user/homes/' + homeId);
|
|
49
|
+
const result = await this.api.get('v2/user/homes/' + homeId);
|
|
50
|
+
|
|
51
|
+
const apiResponse: ApiResponse<Home> = result.data;
|
|
52
|
+
if (apiResponse.result) {
|
|
53
|
+
return apiResponse.result;
|
|
54
|
+
} else {
|
|
55
|
+
this.logger.error('Failed to retrieve the home data');
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
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
|
|
50
62
|
|
|
51
63
|
const apiResponse: ApiResponse<Home> = result.data;
|
|
52
64
|
if (apiResponse.result) {
|
|
@@ -80,4 +92,16 @@ export class RoborockIoTApi {
|
|
|
80
92
|
return undefined;
|
|
81
93
|
}
|
|
82
94
|
}
|
|
95
|
+
|
|
96
|
+
public async getCustom(url: string): Promise<unknown> {
|
|
97
|
+
const result = await this.api.get(url);
|
|
98
|
+
const apiResponse: ApiResponse<unknown> = result.data;
|
|
99
|
+
|
|
100
|
+
if (apiResponse.result) {
|
|
101
|
+
return apiResponse.result;
|
|
102
|
+
} else {
|
|
103
|
+
this.logger.error('Failed to execute scene');
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
83
107
|
}
|