matterbridge-roborock-vacuum-plugin 1.1.0-rc10 → 1.1.0-rc12
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/initialData/getSupportedAreas.js +6 -1
- package/dist/platform.js +4 -6
- package/dist/platformRunner.js +33 -6
- package/dist/roborockCommunication/RESTAPI/roborockIoTApi.js +11 -0
- package/dist/roborockCommunication/Zmodel/mapInfo.js +8 -6
- package/dist/roborockCommunication/broadcast/abstractClient.js +8 -1
- package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js +3 -1
- package/dist/roborockCommunication/broadcast/listener/implementation/syncMessageListener.js +3 -2
- package/dist/roborockCommunication/broadcast/messageProcessor.js +6 -3
- package/dist/roborockCommunication/helper/messageDeserializer.js +7 -1
- package/dist/roborockCommunication/helper/messageSerializer.js +6 -0
- package/dist/roborockService.js +53 -6
- package/eslint.config.js +1 -1
- package/matterbridge-roborock-vacuum-plugin.config.json +2 -2
- package/matterbridge-roborock-vacuum-plugin.schema.json +37 -10
- package/package.json +1 -1
- package/src/initialData/getSupportedAreas.ts +8 -1
- package/src/platform.ts +4 -7
- package/src/platformRunner.ts +40 -8
- package/src/roborockCommunication/RESTAPI/roborockIoTApi.ts +13 -1
- package/src/roborockCommunication/Zmodel/mapInfo.ts +9 -6
- package/src/roborockCommunication/Zmodel/userData.ts +0 -3
- package/src/roborockCommunication/broadcast/abstractClient.ts +10 -3
- package/src/roborockCommunication/broadcast/client.ts +1 -1
- package/src/roborockCommunication/broadcast/clientRouter.ts +1 -1
- package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts +3 -1
- package/src/roborockCommunication/broadcast/listener/implementation/syncMessageListener.ts +4 -3
- package/src/roborockCommunication/broadcast/messageProcessor.ts +9 -5
- package/src/roborockCommunication/helper/messageDeserializer.ts +7 -1
- package/src/roborockCommunication/helper/messageSerializer.ts +6 -0
- package/src/roborockService.ts +62 -14
- package/src/tests/roborockCommunication/broadcast/listener/implementation/syncMessageListener.test.ts +5 -4
- 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 +10 -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
|
@@ -4,7 +4,12 @@ export function getSupportedAreas(rooms, roomMap, log) {
|
|
|
4
4
|
log?.debug('getSupportedAreas', debugStringify(rooms));
|
|
5
5
|
log?.debug('getSupportedAreas', roomMap ? debugStringify(roomMap) : 'undefined');
|
|
6
6
|
if (!rooms || rooms.length === 0 || !roomMap?.rooms || roomMap.rooms.length == 0) {
|
|
7
|
-
|
|
7
|
+
if (!rooms || rooms.length === 0) {
|
|
8
|
+
log?.error('No rooms found');
|
|
9
|
+
}
|
|
10
|
+
if (!roomMap || !roomMap.rooms || roomMap.rooms.length == 0) {
|
|
11
|
+
log?.error('No room map found');
|
|
12
|
+
}
|
|
8
13
|
return [
|
|
9
14
|
{
|
|
10
15
|
areaId: 1,
|
package/dist/platform.js
CHANGED
|
@@ -26,8 +26,8 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
26
26
|
rrHomeId;
|
|
27
27
|
constructor(matterbridge, log, config) {
|
|
28
28
|
super(matterbridge, log, config);
|
|
29
|
-
if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.1.
|
|
30
|
-
throw new Error(`This plugin requires Matterbridge version >= "3.1.
|
|
29
|
+
if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.1.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.`);
|
|
31
31
|
}
|
|
32
32
|
this.log.info('Initializing platform:', this.config.name);
|
|
33
33
|
if (config.whiteList === undefined)
|
|
@@ -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');
|
|
@@ -157,6 +157,7 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
157
157
|
return false;
|
|
158
158
|
}
|
|
159
159
|
if (vacuum.rooms === undefined || vacuum.rooms.length === 0) {
|
|
160
|
+
this.log.error(`Fetching map information for device: ${vacuum.name} (${vacuum.duid}) to get rooms`);
|
|
160
161
|
const map_info = await this.roborockService.getMapInformation(vacuum.duid);
|
|
161
162
|
const rooms = map_info?.maps?.[0]?.rooms ?? [];
|
|
162
163
|
vacuum.rooms = rooms;
|
|
@@ -196,7 +197,4 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
196
197
|
this.log.logLevel = logLevel;
|
|
197
198
|
return Promise.resolve();
|
|
198
199
|
}
|
|
199
|
-
sleep(ms) {
|
|
200
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
201
|
-
}
|
|
202
200
|
}
|
package/dist/platformRunner.js
CHANGED
|
@@ -35,9 +35,19 @@ export class PlatformRunner {
|
|
|
35
35
|
async getRoomMapFromDevice(device) {
|
|
36
36
|
const platform = this.platform;
|
|
37
37
|
const rooms = device?.rooms ?? [];
|
|
38
|
+
platform.log.error(`getRoomMapFromDevice: ${debugStringify(rooms)}`);
|
|
38
39
|
if (device && platform.roborockService) {
|
|
39
40
|
const roomData = await platform.roborockService.getRoomMappings(device.duid);
|
|
40
|
-
|
|
41
|
+
if (roomData !== undefined && roomData.length > 0) {
|
|
42
|
+
platform.log.error(`getRoomMapFromDevice - roomData: ${debugStringify(roomData ?? [])}`);
|
|
43
|
+
return new RoomMap(roomData ?? [], rooms);
|
|
44
|
+
}
|
|
45
|
+
const mapInfo = await platform.roborockService.getMapInformation(device.duid);
|
|
46
|
+
if (mapInfo && mapInfo.maps && mapInfo.maps.length > 0) {
|
|
47
|
+
platform.log.error(`getRoomMapFromDevice - mapInfo: ${debugStringify(mapInfo.maps)}`);
|
|
48
|
+
const roomDataMap = mapInfo.maps[0].rooms.map((r) => [r.id, parseInt(r.name)]);
|
|
49
|
+
return new RoomMap(roomDataMap, rooms);
|
|
50
|
+
}
|
|
41
51
|
}
|
|
42
52
|
return new RoomMap([], rooms);
|
|
43
53
|
}
|
|
@@ -53,8 +63,18 @@ export class PlatformRunner {
|
|
|
53
63
|
const rooms = robot.device.rooms ?? [];
|
|
54
64
|
if (robot.roomInfo === undefined) {
|
|
55
65
|
const roomData = await platform.roborockService.getRoomMappings(robot.device.duid);
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
if (roomData !== undefined && roomData.length > 0) {
|
|
67
|
+
robot.roomInfo = new RoomMap(roomData ?? [], rooms);
|
|
68
|
+
return robot.roomInfo;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (robot.roomInfo === undefined) {
|
|
72
|
+
const mapInfo = await platform.roborockService.getMapInformation(robot.device.duid);
|
|
73
|
+
if (mapInfo && mapInfo.maps && mapInfo.maps.length > 0) {
|
|
74
|
+
platform.log.error(`getRoomMap - mapInfo: ${debugStringify(mapInfo.maps)}`);
|
|
75
|
+
const roomDataMap = mapInfo.maps[0].rooms.map((r) => [r.id, parseInt(r.name)]);
|
|
76
|
+
robot.roomInfo = new RoomMap(roomDataMap, rooms);
|
|
77
|
+
}
|
|
58
78
|
}
|
|
59
79
|
return robot.roomInfo;
|
|
60
80
|
}
|
|
@@ -109,13 +129,20 @@ export class PlatformRunner {
|
|
|
109
129
|
if (state) {
|
|
110
130
|
robot.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(state), platform.log);
|
|
111
131
|
}
|
|
112
|
-
const currentRoom = data.cleaning_info?.segment_id ?? -1;
|
|
113
132
|
const currentMappedAreas = this.platform.roborockService?.getSupportedAreas(duid);
|
|
133
|
+
const roomMap = await this.getRoomMap(duid);
|
|
134
|
+
const targetRoom = data.cleaning_info?.target_segment_id ?? -1;
|
|
135
|
+
const isTargetMappedArea = currentMappedAreas?.some((x) => x.areaId == targetRoom);
|
|
136
|
+
if (targetRoom !== -1 && isTargetMappedArea) {
|
|
137
|
+
this.platform.log.debug(`RoomMap: ${roomMap ? debugStringify(roomMap) : 'undefined'}`);
|
|
138
|
+
this.platform.log.debug(`TargetRoom: ${targetRoom}, room name: ${roomMap?.rooms.find((x) => x.id === targetRoom)?.displayName ?? 'unknown'}`);
|
|
139
|
+
robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', targetRoom, platform.log);
|
|
140
|
+
}
|
|
141
|
+
const currentRoom = data.cleaning_info?.segment_id ?? -1;
|
|
114
142
|
const isMappedArea = currentMappedAreas?.some((x) => x.areaId == currentRoom);
|
|
115
143
|
if (currentRoom !== -1 && isMappedArea) {
|
|
116
|
-
const roomMap = await this.getRoomMap(duid);
|
|
117
144
|
this.platform.log.debug(`RoomMap: ${roomMap ? debugStringify(roomMap) : 'undefined'}`);
|
|
118
|
-
this.platform.log.debug(
|
|
145
|
+
this.platform.log.debug(`CurrentRoom: ${currentRoom}, room name: ${roomMap?.rooms.find((x) => x.id === currentRoom)?.displayName ?? 'unknown'}`);
|
|
119
146
|
robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', currentRoom, platform.log);
|
|
120
147
|
}
|
|
121
148
|
if (data.battery) {
|
|
@@ -81,4 +81,15 @@ export class RoborockIoTApi {
|
|
|
81
81
|
return undefined;
|
|
82
82
|
}
|
|
83
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
|
+
}
|
|
84
95
|
}
|
|
@@ -6,12 +6,14 @@ export class MapInfo {
|
|
|
6
6
|
this.maps.push({
|
|
7
7
|
id: map.mapFlag,
|
|
8
8
|
name: decodeComponent(map.name)?.toLowerCase(),
|
|
9
|
-
rooms: map.rooms.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
rooms: map.rooms && map.rooms.length > 0
|
|
10
|
+
? map.rooms.map((room) => {
|
|
11
|
+
return {
|
|
12
|
+
id: room.id,
|
|
13
|
+
name: room.iot_name_id,
|
|
14
|
+
};
|
|
15
|
+
})
|
|
16
|
+
: [],
|
|
15
17
|
});
|
|
16
18
|
});
|
|
17
19
|
}
|
|
@@ -29,8 +29,15 @@ export class AbstractClient {
|
|
|
29
29
|
}
|
|
30
30
|
async get(duid, request) {
|
|
31
31
|
return new Promise((resolve, reject) => {
|
|
32
|
-
this.syncMessageListener.waitFor(request.messageId, (response) => resolve(response), reject);
|
|
32
|
+
this.syncMessageListener.waitFor(request.messageId, request, (response) => resolve(response), reject);
|
|
33
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;
|
|
34
41
|
});
|
|
35
42
|
}
|
|
36
43
|
registerDevice(duid, localKey, pv) {
|
package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js
CHANGED
|
@@ -21,7 +21,9 @@ export class ConnectionStateListener {
|
|
|
21
21
|
}
|
|
22
22
|
if (this.client.retryCount > 10) {
|
|
23
23
|
this.logger.error(`Device with DUID ${duid} has exceeded retry limit, not re-registering.`);
|
|
24
|
-
this.changeToSecureConnection
|
|
24
|
+
if (this.changeToSecureConnection) {
|
|
25
|
+
this.changeToSecureConnection(duid);
|
|
26
|
+
}
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
27
29
|
this.client.retryCount++;
|
|
@@ -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,6 +53,11 @@ 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
|
}
|
|
@@ -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
|
}
|
package/dist/roborockService.js
CHANGED
|
@@ -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);
|
|
@@ -77,7 +79,8 @@ export default class RoborockService {
|
|
|
77
79
|
this.logger.debug('RoborockService - getMapInformation', duid);
|
|
78
80
|
assert(this.messageClient !== undefined);
|
|
79
81
|
return this.messageClient.get(duid, new RequestMessage({ method: 'get_multi_maps_list' })).then((response) => {
|
|
80
|
-
|
|
82
|
+
this.logger.debug('RoborockService - getMapInformation response', debugStringify(response ?? []));
|
|
83
|
+
return response ? new MapInfo(response[0]) : undefined;
|
|
81
84
|
});
|
|
82
85
|
}
|
|
83
86
|
async changeCleanMode(duid, { suctionPower, waterFlow, distance_off, mopRoute }) {
|
|
@@ -147,6 +150,17 @@ export default class RoborockService {
|
|
|
147
150
|
async customSend(duid, request) {
|
|
148
151
|
return this.getMessageProcessor(duid)?.sendCustomMessage(duid, request);
|
|
149
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
|
+
}
|
|
150
164
|
stopService() {
|
|
151
165
|
if (this.messageClient) {
|
|
152
166
|
this.messageClient.disconnect();
|
|
@@ -175,14 +189,14 @@ export default class RoborockService {
|
|
|
175
189
|
setDeviceNotify(callback) {
|
|
176
190
|
this.deviceNotify = callback;
|
|
177
191
|
}
|
|
178
|
-
|
|
192
|
+
activateDeviceNotify(device) {
|
|
179
193
|
const self = this;
|
|
180
194
|
this.logger.debug('Requesting device info for device', device.duid);
|
|
181
195
|
const messageProcessor = this.getMessageProcessor(device.duid);
|
|
182
196
|
this.requestDeviceStatusInterval = setInterval(async () => {
|
|
183
197
|
if (messageProcessor) {
|
|
184
198
|
await messageProcessor.getDeviceStatus(device.duid).then((response) => {
|
|
185
|
-
if (self.deviceNotify) {
|
|
199
|
+
if (self.deviceNotify && response) {
|
|
186
200
|
const message = { duid: device.duid, ...response.errorStatus, ...response.message };
|
|
187
201
|
self.logger.debug('Device status update', debugStringify(message));
|
|
188
202
|
self.deviceNotify(NotifyMessageTypes.LocalMessage, message);
|
|
@@ -208,6 +222,15 @@ export default class RoborockService {
|
|
|
208
222
|
const scenes = (await this.iotApi.getScenes(homeDetails.rrHomeId)) ?? [];
|
|
209
223
|
const products = new Map();
|
|
210
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
|
+
}
|
|
211
234
|
if (homeData.rooms.length === 0) {
|
|
212
235
|
const homeDataV2 = await this.iotApi.getHomev2(homeDetails.rrHomeId);
|
|
213
236
|
if (homeDataV2 && homeDataV2.rooms && homeDataV2.rooms.length > 0) {
|
|
@@ -259,6 +282,18 @@ export default class RoborockService {
|
|
|
259
282
|
const products = new Map();
|
|
260
283
|
homeData.products.forEach((p) => products.set(p.id, p.model));
|
|
261
284
|
const devices = homeData.devices.length > 0 ? homeData.devices : homeData.receivedDevices;
|
|
285
|
+
if (homeData.rooms.length === 0) {
|
|
286
|
+
const homeDataV3 = await this.iotApi.getHomev3(homeid);
|
|
287
|
+
if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
|
|
288
|
+
homeData.rooms = homeDataV3.rooms;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const homeDataV1 = await this.iotApi.getHome(homeid);
|
|
292
|
+
if (homeDataV1 && homeDataV1.rooms && homeDataV1.rooms.length > 0) {
|
|
293
|
+
homeData.rooms = homeDataV1.rooms;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
262
297
|
const dvs = devices.map((device) => {
|
|
263
298
|
return {
|
|
264
299
|
...device,
|
|
@@ -297,9 +332,9 @@ export default class RoborockService {
|
|
|
297
332
|
getRoomMappings(duid) {
|
|
298
333
|
if (!this.messageClient) {
|
|
299
334
|
this.logger.warn('messageClient not initialized. Waititing for next execution');
|
|
300
|
-
return undefined;
|
|
335
|
+
return Promise.resolve(undefined);
|
|
301
336
|
}
|
|
302
|
-
return this.messageClient.get(duid, new RequestMessage({ method: 'get_room_mapping' }));
|
|
337
|
+
return this.messageClient.get(duid, new RequestMessage({ method: 'get_room_mapping', secure: this.isRequestSecure(duid) }));
|
|
303
338
|
}
|
|
304
339
|
async initializeMessageClient(username, device, userdata) {
|
|
305
340
|
if (this.clientManager === undefined) {
|
|
@@ -351,6 +386,15 @@ export default class RoborockService {
|
|
|
351
386
|
},
|
|
352
387
|
});
|
|
353
388
|
this.messageProcessorMap.set(device.duid, messageProcessor);
|
|
389
|
+
this.logger.debug('Checking if device supports local connection', device.pv, device.data.model, device.duid);
|
|
390
|
+
if (device.pv === 'B01') {
|
|
391
|
+
this.logger.warn('Device does not support local connection', device.duid);
|
|
392
|
+
this.mqttAlwaysOnDevices.set(device.duid, true);
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
this.mqttAlwaysOnDevices.set(device.duid, false);
|
|
397
|
+
}
|
|
354
398
|
this.logger.debug('Local device', device.duid);
|
|
355
399
|
let localIp = this.ipMap.get(device.duid);
|
|
356
400
|
try {
|
|
@@ -389,7 +433,7 @@ export default class RoborockService {
|
|
|
389
433
|
return true;
|
|
390
434
|
}
|
|
391
435
|
onLocalClientDisconnect(duid) {
|
|
392
|
-
this.
|
|
436
|
+
this.mqttAlwaysOnDevices.set(duid, true);
|
|
393
437
|
}
|
|
394
438
|
sleep(ms) {
|
|
395
439
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -399,4 +443,7 @@ export default class RoborockService {
|
|
|
399
443
|
this.iotApi = this.iotApiFactory(this.logger, userdata);
|
|
400
444
|
return userdata;
|
|
401
445
|
}
|
|
446
|
+
isRequestSecure(duid) {
|
|
447
|
+
return this.mqttAlwaysOnDevices.get(duid) ?? false;
|
|
448
|
+
}
|
|
402
449
|
}
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "matterbridge-roborock-vacuum-plugin",
|
|
3
3
|
"type": "DynamicPlatform",
|
|
4
|
-
"version": "1.1.0-
|
|
4
|
+
"version": "1.1.0-rc12",
|
|
5
5
|
"whiteList": [],
|
|
6
6
|
"blackList": [],
|
|
7
7
|
"useInterval": true,
|
|
@@ -37,4 +37,4 @@
|
|
|
37
37
|
"debug": true,
|
|
38
38
|
"unregisterOnShutdown": false,
|
|
39
39
|
"enableExperimentalFeature": false
|
|
40
|
-
}
|
|
40
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
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-rc12 by https://github.com/RinDevJunior",
|
|
4
4
|
"type": "object",
|
|
5
|
-
"required": [
|
|
5
|
+
"required": [
|
|
6
|
+
"username",
|
|
7
|
+
"password"
|
|
8
|
+
],
|
|
6
9
|
"properties": {
|
|
7
10
|
"name": {
|
|
8
11
|
"description": "Plugin name",
|
|
@@ -57,7 +60,9 @@
|
|
|
57
60
|
"const": true
|
|
58
61
|
}
|
|
59
62
|
},
|
|
60
|
-
"required": [
|
|
63
|
+
"required": [
|
|
64
|
+
"enableExperimentalFeature"
|
|
65
|
+
]
|
|
61
66
|
},
|
|
62
67
|
"then": {
|
|
63
68
|
"properties": {
|
|
@@ -114,7 +119,9 @@
|
|
|
114
119
|
"const": true
|
|
115
120
|
}
|
|
116
121
|
},
|
|
117
|
-
"required": [
|
|
122
|
+
"required": [
|
|
123
|
+
"enableCleanModeMapping"
|
|
124
|
+
]
|
|
118
125
|
},
|
|
119
126
|
"then": {
|
|
120
127
|
"properties": {
|
|
@@ -159,7 +166,9 @@
|
|
|
159
166
|
"default": 25
|
|
160
167
|
}
|
|
161
168
|
},
|
|
162
|
-
"required": [
|
|
169
|
+
"required": [
|
|
170
|
+
"distanceOff"
|
|
171
|
+
]
|
|
163
172
|
}
|
|
164
173
|
}
|
|
165
174
|
]
|
|
@@ -196,7 +205,9 @@
|
|
|
196
205
|
"default": 25
|
|
197
206
|
}
|
|
198
207
|
},
|
|
199
|
-
"required": [
|
|
208
|
+
"required": [
|
|
209
|
+
"distanceOff"
|
|
210
|
+
]
|
|
200
211
|
}
|
|
201
212
|
}
|
|
202
213
|
]
|
|
@@ -226,20 +237,36 @@
|
|
|
226
237
|
"fanMode": {
|
|
227
238
|
"type": "string",
|
|
228
239
|
"description": "Suction power mode to use (e.g., 'Quiet', 'Balanced', 'Turbo', 'Max', 'MaxPlus').",
|
|
229
|
-
"enum": [
|
|
240
|
+
"enum": [
|
|
241
|
+
"Quiet",
|
|
242
|
+
"Balanced",
|
|
243
|
+
"Turbo",
|
|
244
|
+
"Max",
|
|
245
|
+
"MaxPlus"
|
|
246
|
+
],
|
|
230
247
|
"default": "Balanced"
|
|
231
248
|
},
|
|
232
249
|
"waterFlowMode": {
|
|
233
250
|
"type": "string",
|
|
234
251
|
"description": "Water flow mode to use (e.g., 'Low', 'Medium', 'High', 'CustomizeWithDistanceOff').",
|
|
235
|
-
"enum": [
|
|
252
|
+
"enum": [
|
|
253
|
+
"Low",
|
|
254
|
+
"Medium",
|
|
255
|
+
"High",
|
|
256
|
+
"CustomizeWithDistanceOff"
|
|
257
|
+
],
|
|
236
258
|
"default": "Medium"
|
|
237
259
|
},
|
|
238
260
|
"mopRouteMode": {
|
|
239
261
|
"type": "string",
|
|
240
262
|
"description": "Mop route intensity to use (e.g., 'Standard', 'Deep', 'DeepPlus', 'Fast').",
|
|
241
|
-
"enum": [
|
|
263
|
+
"enum": [
|
|
264
|
+
"Standard",
|
|
265
|
+
"Deep",
|
|
266
|
+
"DeepPlus",
|
|
267
|
+
"Fast"
|
|
268
|
+
],
|
|
242
269
|
"default": "Standard"
|
|
243
270
|
}
|
|
244
271
|
}
|
|
245
|
-
}
|
|
272
|
+
}
|
package/package.json
CHANGED
|
@@ -9,7 +9,14 @@ export function getSupportedAreas(rooms: Room[], roomMap: RoomMap | undefined, l
|
|
|
9
9
|
log?.debug('getSupportedAreas', roomMap ? debugStringify(roomMap) : 'undefined');
|
|
10
10
|
|
|
11
11
|
if (!rooms || rooms.length === 0 || !roomMap?.rooms || roomMap.rooms.length == 0) {
|
|
12
|
-
|
|
12
|
+
if (!rooms || rooms.length === 0) {
|
|
13
|
+
log?.error('No rooms found');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!roomMap || !roomMap.rooms || roomMap.rooms.length == 0) {
|
|
17
|
+
log?.error('No room map found');
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
return [
|
|
14
21
|
{
|
|
15
22
|
areaId: 1,
|
package/src/platform.ts
CHANGED
|
@@ -32,9 +32,9 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
32
32
|
super(matterbridge, log, config);
|
|
33
33
|
|
|
34
34
|
// Verify that Matterbridge is the correct version
|
|
35
|
-
if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.1.
|
|
35
|
+
if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.1.6')) {
|
|
36
36
|
throw new Error(
|
|
37
|
-
`This plugin requires Matterbridge version >= "3.1.
|
|
37
|
+
`This plugin requires Matterbridge version >= "3.1.6". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend.`,
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
this.log.info('Initializing platform:', this.config.name);
|
|
@@ -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();
|
|
@@ -224,6 +224,7 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
if (vacuum.rooms === undefined || vacuum.rooms.length === 0) {
|
|
227
|
+
this.log.error(`Fetching map information for device: ${vacuum.name} (${vacuum.duid}) to get rooms`);
|
|
227
228
|
const map_info = await this.roborockService.getMapInformation(vacuum.duid);
|
|
228
229
|
const rooms = map_info?.maps?.[0]?.rooms ?? [];
|
|
229
230
|
vacuum.rooms = rooms;
|
|
@@ -279,8 +280,4 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
279
280
|
this.log.logLevel = logLevel;
|
|
280
281
|
return Promise.resolve();
|
|
281
282
|
}
|
|
282
|
-
|
|
283
|
-
private sleep(ms: number): Promise<void> {
|
|
284
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
285
|
-
}
|
|
286
283
|
}
|