matterbridge-roborock-vacuum-plugin 1.1.0-rc18 → 1.1.1-rc02
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/helper.js +2 -7
- package/dist/model/RoomMap.js +1 -1
- package/dist/model/roomIndexMap.js +8 -4
- package/dist/platform.js +4 -3
- package/dist/platformRunner.js +10 -268
- package/dist/roborockCommunication/broadcast/abstractClient.js +3 -3
- package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +1 -0
- package/dist/roborockCommunication/broadcast/client/LocalNetworkUDPClient.js +1 -1
- package/dist/roborockCommunication/broadcast/clientRouter.js +5 -2
- package/dist/roborockCommunication/broadcast/model/contentMessage.js +1 -0
- package/dist/roborockCommunication/broadcast/model/headerMessage.js +1 -0
- package/dist/roborockCommunication/broadcast/model/messageContext.js +18 -7
- package/dist/roborockCommunication/broadcast/model/requestMessage.js +5 -0
- package/dist/roborockCommunication/helper/messageDeserializer.js +31 -18
- package/dist/roborockCommunication/helper/messageSerializer.js +17 -11
- package/dist/roborockService.js +6 -1
- package/dist/runtimes/handleCloudMessage.js +110 -0
- package/dist/runtimes/handleHomeDataMessage.js +57 -0
- package/dist/runtimes/handleLocalMessage.js +169 -0
- package/dist/rvc.js +2 -1
- package/dist/tests/testData/mockData.js +359 -0
- package/matterbridge-roborock-vacuum-plugin.config.json +2 -2
- package/matterbridge-roborock-vacuum-plugin.schema.json +10 -37
- package/package.json +2 -3
- package/src/behaviors/roborock.vacuum/default/runtimes.ts +1 -1
- package/src/behaviors/roborock.vacuum/smart/runtimes.ts +1 -1
- package/src/helper.ts +2 -12
- package/src/initialData/getSupportedAreas.ts +1 -9
- package/src/model/RoomMap.ts +4 -30
- package/src/model/roomIndexMap.ts +10 -6
- package/src/platform.ts +6 -3
- package/src/platformRunner.ts +12 -350
- package/src/roborockCommunication/Zmodel/device.ts +13 -1
- package/src/roborockCommunication/Zmodel/messageResult.ts +28 -27
- package/src/roborockCommunication/Zmodel/userData.ts +2 -1
- package/src/roborockCommunication/broadcast/abstractClient.ts +3 -3
- package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +4 -2
- package/src/roborockCommunication/broadcast/client/LocalNetworkUDPClient.ts +3 -3
- package/src/roborockCommunication/broadcast/clientRouter.ts +6 -2
- package/src/roborockCommunication/broadcast/model/contentMessage.ts +5 -0
- package/src/roborockCommunication/broadcast/model/headerMessage.ts +7 -0
- package/src/roborockCommunication/broadcast/model/messageContext.ts +24 -11
- package/src/roborockCommunication/broadcast/model/requestMessage.ts +12 -5
- package/src/roborockCommunication/helper/messageDeserializer.ts +42 -31
- package/src/roborockCommunication/helper/messageSerializer.ts +19 -14
- package/src/roborockService.ts +8 -1
- package/src/runtimes/handleCloudMessage.ts +134 -0
- package/src/runtimes/handleHomeDataMessage.ts +67 -0
- package/src/runtimes/handleLocalMessage.ts +209 -0
- package/src/rvc.ts +2 -2
- package/src/share/runtimeHelper.ts +1 -1
- package/src/tests/helper.test.ts +59 -10
- package/src/tests/roborockCommunication/broadcast/client/LocalNetworkClient.test.ts +0 -19
- package/src/tests/roborockCommunication/broadcast/client/MQTTClient.test.ts +0 -32
- package/src/tests/roborockService.setSelectedAreas.test.ts +61 -0
- package/src/tests/runtimes/handleCloudMessage.test.ts +200 -0
- package/src/tests/runtimes/handleHomeDataMessage.test.ts +53 -0
- package/src/tests/runtimes/handleLocalMessage.test.ts +222 -0
- package/src/tests/testData/mockData.ts +370 -0
package/dist/helper.js
CHANGED
|
@@ -55,16 +55,13 @@ export async function getRoomMap(duid, platform) {
|
|
|
55
55
|
export async function getRoomMapFromDevice(device, platform) {
|
|
56
56
|
const rooms = device?.rooms ?? [];
|
|
57
57
|
const enableMultipleMap = (platform.enableExperimentalFeature?.enableExperimentalFeature && platform.enableExperimentalFeature?.advancedFeature.enableMultipleMap) ?? false;
|
|
58
|
-
platform.log.notice('-------------------------------------------0--------------------------------------------------------');
|
|
59
|
-
platform.log.notice(`getRoomMapFromDevice - device.rooms: ${debugStringify(rooms)}`);
|
|
60
58
|
if (device && platform.roborockService) {
|
|
61
59
|
const mapInfo = await platform.roborockService.getMapInformation(device.duid);
|
|
62
|
-
platform.log.
|
|
60
|
+
platform.log.debug(`getRoomMapFromDevice - mapInfo: ${mapInfo ? debugStringify(mapInfo) : 'undefined'}`);
|
|
61
|
+
platform.log.debug(`getRoomMapFromDevice - rooms: ${debugStringify(rooms)}`);
|
|
63
62
|
if (mapInfo && mapInfo.allRooms && mapInfo.allRooms.length > 0) {
|
|
64
63
|
const roomDataMap = mapInfo.allRooms;
|
|
65
64
|
const roomMap = new RoomMap(roomDataMap, rooms, mapInfo.maps, enableMultipleMap);
|
|
66
|
-
platform.log.notice(`getRoomMapFromDevice - roomMap: ${debugStringify(roomMap)}`);
|
|
67
|
-
platform.log.notice('-------------------------------------------2--------------------------------------------------------');
|
|
68
65
|
return roomMap;
|
|
69
66
|
}
|
|
70
67
|
const roomData = await platform.roborockService.getRoomMappings(device.duid);
|
|
@@ -72,8 +69,6 @@ export async function getRoomMapFromDevice(device, platform) {
|
|
|
72
69
|
platform.log.notice(`getRoomMapFromDevice - roomData: ${debugStringify(roomData ?? [])}`);
|
|
73
70
|
const roomDataMap = roomData.map((r) => ({ id: r[0], iot_name_id: String(r[1]), globalId: r[1], tag: r[2], mapId: 0, displayName: undefined }));
|
|
74
71
|
const roomMap = new RoomMap(roomDataMap ?? [], rooms, [], enableMultipleMap);
|
|
75
|
-
platform.log.notice(`getRoomMapFromDevice - roomMap: ${debugStringify(roomMap)}`);
|
|
76
|
-
platform.log.notice('-------------------------------------------1--------------------------------------------------------');
|
|
77
72
|
return roomMap;
|
|
78
73
|
}
|
|
79
74
|
}
|
package/dist/model/RoomMap.js
CHANGED
|
@@ -4,12 +4,16 @@ export class RoomIndexMap {
|
|
|
4
4
|
constructor(roomMap) {
|
|
5
5
|
this.indexMap = roomMap;
|
|
6
6
|
this.roomMap = new Map();
|
|
7
|
-
for (const [areaId, { roomId }] of roomMap.entries()) {
|
|
8
|
-
this.roomMap.set(roomId
|
|
7
|
+
for (const [areaId, { roomId, mapId }] of roomMap.entries()) {
|
|
8
|
+
this.roomMap.set(`${roomId}:${mapId}`, areaId);
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
getAreaId(roomId) {
|
|
12
|
-
|
|
11
|
+
getAreaId(roomId, mapId) {
|
|
12
|
+
const areaId = this.roomMap.get(`${roomId}:${mapId}`);
|
|
13
|
+
if (areaId === undefined) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return areaId;
|
|
13
17
|
}
|
|
14
18
|
getRoomId(areaId) {
|
|
15
19
|
return this.indexMap.get(areaId)?.roomId;
|
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.
|
|
30
|
-
throw new Error(`This plugin requires Matterbridge version >= "3.
|
|
29
|
+
if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.2.0')) {
|
|
30
|
+
throw new Error(`This plugin requires Matterbridge version >= "3.2.0". 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)
|
|
@@ -52,6 +52,7 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
52
52
|
}
|
|
53
53
|
const axiosInstance = axios.default ?? axios;
|
|
54
54
|
this.enableExperimentalFeature = this.config.enableExperimental;
|
|
55
|
+
this.enableExperimentalFeature.advancedFeature.enableMultipleMap = false;
|
|
55
56
|
if (this.enableExperimentalFeature?.enableExperimentalFeature && this.enableExperimentalFeature?.cleanModeSettings?.enableCleanModeMapping) {
|
|
56
57
|
this.cleanModeSettings = this.enableExperimentalFeature.cleanModeSettings;
|
|
57
58
|
this.log.notice(`Experimental Feature has been enable`);
|
|
@@ -171,7 +172,7 @@ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
|
|
|
171
172
|
this.roborockService.setSupportedAreaIndexMap(vacuum.duid, roomIndexMap);
|
|
172
173
|
let routineAsRoom = [];
|
|
173
174
|
if (this.enableExperimentalFeature?.enableExperimentalFeature && this.enableExperimentalFeature.advancedFeature?.showRoutinesAsRoom) {
|
|
174
|
-
routineAsRoom = getSupportedScenes(vacuum.scenes, this.log);
|
|
175
|
+
routineAsRoom = getSupportedScenes(vacuum.scenes ?? [], this.log);
|
|
175
176
|
this.roborockService.setSupportedScenes(vacuum.duid, routineAsRoom);
|
|
176
177
|
}
|
|
177
178
|
const robot = new RoborockVacuumCleaner(username, vacuum, roomMap, routineAsRoom, this.enableExperimentalFeature, this.log);
|
package/dist/platformRunner.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { getRunningMode } from './initialData/getSupportedRunModes.js';
|
|
4
|
-
import { state_to_matter_operational_status, state_to_matter_state } from './share/function.js';
|
|
5
|
-
import { getBatteryState, getBatteryStatus, getOperationalErrorState, getSupportedAreas } from './initialData/index.js';
|
|
1
|
+
import { PowerSource, RvcOperationalState } from 'matterbridge/matter/clusters';
|
|
2
|
+
import { getBatteryStatus, getOperationalErrorState } from './initialData/index.js';
|
|
6
3
|
import { NotifyMessageTypes } from './notifyMessageTypes.js';
|
|
7
|
-
import { Protocol } from './roborockCommunication/broadcast/model/protocol.js';
|
|
8
|
-
import { hasDockingStationError, parseDockingStationStatus } from './model/DockingStationStatus.js';
|
|
9
|
-
import { AdditionalPropCode } from './roborockCommunication/index.js';
|
|
10
|
-
import { OperationStatusCode } from './roborockCommunication/Zenum/operationStatusCode.js';
|
|
11
|
-
import { getCurrentCleanModeFunc } from './share/runtimeHelper.js';
|
|
12
4
|
import { debugStringify } from 'matterbridge/logger';
|
|
5
|
+
import { handleLocalMessage } from './runtimes/handleLocalMessage.js';
|
|
6
|
+
import { handleCloudMessage } from './runtimes/handleCloudMessage.js';
|
|
7
|
+
import { updateFromHomeData } from './runtimes/handleHomeDataMessage.js';
|
|
13
8
|
export class PlatformRunner {
|
|
14
9
|
platform;
|
|
15
10
|
constructor(platform) {
|
|
@@ -17,7 +12,7 @@ export class PlatformRunner {
|
|
|
17
12
|
}
|
|
18
13
|
async updateRobot(messageSource, homeData) {
|
|
19
14
|
if (messageSource === NotifyMessageTypes.HomeData) {
|
|
20
|
-
|
|
15
|
+
updateFromHomeData(homeData, this.platform);
|
|
21
16
|
}
|
|
22
17
|
else {
|
|
23
18
|
await this.updateFromMQTTMessage(messageSource, homeData);
|
|
@@ -74,275 +69,22 @@ export class PlatformRunner {
|
|
|
74
69
|
case NotifyMessageTypes.LocalMessage: {
|
|
75
70
|
const data = messageData;
|
|
76
71
|
const robot = platform.robots.get(duid);
|
|
77
|
-
if (
|
|
78
|
-
|
|
72
|
+
if (robot && data) {
|
|
73
|
+
await handleLocalMessage(data, platform, duid);
|
|
79
74
|
return;
|
|
80
75
|
}
|
|
81
|
-
|
|
82
|
-
const state = state_to_matter_state(data.state);
|
|
83
|
-
if (state) {
|
|
84
|
-
robot.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(state), platform.log);
|
|
85
|
-
}
|
|
86
|
-
if (data.state === OperationStatusCode.Idle) {
|
|
87
|
-
const selectedAreas = platform.roborockService?.getSelectedAreas(duid) ?? [];
|
|
88
|
-
robot.updateAttribute(ServiceArea.Cluster.id, 'selectedAreas', selectedAreas, platform.log);
|
|
89
|
-
}
|
|
90
|
-
if (state === RvcRunMode.ModeTag.Cleaning && !data.cleaning_info) {
|
|
91
|
-
robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', null, platform.log);
|
|
92
|
-
robot.updateAttribute(ServiceArea.Cluster.id, 'selectedAreas', [], platform.log);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
const currentMappedAreas = this.platform.roborockService?.getSupportedAreas(duid);
|
|
96
|
-
const roomIndexMap = this.platform.roborockService?.getSupportedAreasIndexMap(duid);
|
|
97
|
-
const roomMap = await getRoomMap(duid, this.platform);
|
|
98
|
-
const segment_id = data.cleaning_info?.segment_id ?? -1;
|
|
99
|
-
const target_segment_id = data.cleaning_info?.target_segment_id ?? -1;
|
|
100
|
-
let target_room_id = roomMap?.rooms.find((x) => x.id === segment_id || x.alternativeId === segment_id.toString())?.id ?? -1;
|
|
101
|
-
this.platform.log.debug(`Target segment id: ${segment_id}, targetRoom: ${target_room_id}`);
|
|
102
|
-
const isMappedArea = currentMappedAreas?.some((x) => x.areaId == segment_id);
|
|
103
|
-
if (segment_id !== -1 && isMappedArea) {
|
|
104
|
-
this.platform.log.debug(`RoomMap: ${roomMap ? debugStringify(roomMap) : 'undefined'}`);
|
|
105
|
-
this.platform.log.debug(`Part1: CurrentRoom: ${segment_id}, room name: ${roomMap?.rooms.find((x) => x.id === segment_id || x.alternativeId === segment_id.toString())?.displayName ?? 'unknown'}`);
|
|
106
|
-
const areaId = roomIndexMap?.getAreaId(segment_id) ?? segment_id;
|
|
107
|
-
this.platform.log.notice(`AreaId: ${areaId}, segment_id: ${segment_id}`);
|
|
108
|
-
robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', areaId, platform.log);
|
|
109
|
-
}
|
|
110
|
-
if (segment_id == -1) {
|
|
111
|
-
const isTargetMappedArea = currentMappedAreas?.some((x) => x.areaId == target_segment_id);
|
|
112
|
-
target_room_id = roomMap?.rooms.find((x) => x.id == target_segment_id || x.alternativeId === target_segment_id.toString())?.id ?? -1;
|
|
113
|
-
this.platform.log.debug(`Target segment id: ${target_segment_id}, targetRoom: ${target_room_id}`);
|
|
114
|
-
if (target_segment_id !== -1 && isTargetMappedArea) {
|
|
115
|
-
this.platform.log.debug(`RoomMap: ${roomMap ? debugStringify(roomMap) : 'undefined'}`);
|
|
116
|
-
this.platform.log.debug(`Part2: TargetRoom: ${target_segment_id}, room name: ${roomMap?.rooms.find((x) => x.id === target_segment_id || x.alternativeId === segment_id.toString())?.displayName ?? 'unknown'}`);
|
|
117
|
-
const areaId = roomIndexMap?.getAreaId(target_segment_id) ?? target_room_id;
|
|
118
|
-
this.platform.log.notice(`AreaId: ${areaId}, target_segment_id: ${target_segment_id}`);
|
|
119
|
-
robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', areaId, platform.log);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (target_segment_id == -1 && segment_id == -1) {
|
|
123
|
-
robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', null, platform.log);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (data.battery) {
|
|
127
|
-
const batteryLevel = data.battery;
|
|
128
|
-
robot.updateAttribute(PowerSource.Cluster.id, 'batPercentRemaining', batteryLevel * 2, platform.log);
|
|
129
|
-
robot.updateAttribute(PowerSource.Cluster.id, 'batChargeState', getBatteryState(data.state, data.battery), platform.log);
|
|
130
|
-
robot.updateAttribute(PowerSource.Cluster.id, 'batChargeLevel', getBatteryStatus(batteryLevel), platform.log);
|
|
131
|
-
}
|
|
132
|
-
const currentCleanModeSetting = {
|
|
133
|
-
suctionPower: data.cleaning_info?.fan_power ?? data.fan_power,
|
|
134
|
-
waterFlow: data.cleaning_info?.water_box_status ?? data.water_box_mode,
|
|
135
|
-
distance_off: data.distance_off,
|
|
136
|
-
mopRoute: data.cleaning_info?.mop_mode ?? data.mop_mode,
|
|
137
|
-
segment_id: data.cleaning_info?.segment_id,
|
|
138
|
-
target_segment_id: data.cleaning_info?.target_segment_id,
|
|
139
|
-
};
|
|
140
|
-
this.platform.log.debug(`data: ${debugStringify(data)}`);
|
|
141
|
-
this.platform.log.notice(`currentCleanModeSetting: ${debugStringify(currentCleanModeSetting)}`);
|
|
142
|
-
if (currentCleanModeSetting.mopRoute && currentCleanModeSetting.suctionPower && currentCleanModeSetting.waterFlow) {
|
|
143
|
-
const currentCleanMode = getCurrentCleanModeFunc(deviceData.model, this.platform.enableExperimentalFeature?.advancedFeature?.forceRunAtDefault ?? false)(currentCleanModeSetting);
|
|
144
|
-
this.platform.log.debug(`Current clean mode: ${currentCleanMode}`);
|
|
145
|
-
if (currentCleanMode) {
|
|
146
|
-
robot.updateAttribute(RvcCleanMode.Cluster.id, 'currentMode', currentCleanMode, platform.log);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
this.processAdditionalProps(robot, data, duid);
|
|
150
|
-
}
|
|
76
|
+
platform.log.error(`Error2: Robot with DUID ${duid} not found`);
|
|
151
77
|
break;
|
|
152
78
|
}
|
|
153
79
|
case NotifyMessageTypes.CloudMessage: {
|
|
154
80
|
const data = messageData;
|
|
155
81
|
if (!data)
|
|
156
82
|
return;
|
|
157
|
-
|
|
83
|
+
await handleCloudMessage(data, platform, this, duid);
|
|
158
84
|
break;
|
|
159
85
|
}
|
|
160
86
|
default:
|
|
161
87
|
break;
|
|
162
88
|
}
|
|
163
89
|
}
|
|
164
|
-
handlerCloudMessage(data, duid) {
|
|
165
|
-
const platform = this.platform;
|
|
166
|
-
const messageTypes = Object.keys(data.dps).map(Number);
|
|
167
|
-
const self = this;
|
|
168
|
-
const robot = platform.robots.get(duid);
|
|
169
|
-
if (robot === undefined) {
|
|
170
|
-
platform.log.error(`Error3: Robot with DUID ${duid} not found`);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
messageTypes.forEach(async (messageType) => {
|
|
174
|
-
switch (messageType) {
|
|
175
|
-
case Protocol.status_update: {
|
|
176
|
-
const status = Number(data.dps[messageType]);
|
|
177
|
-
const matterState = state_to_matter_state(status);
|
|
178
|
-
if (matterState) {
|
|
179
|
-
robot.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(matterState), platform.log);
|
|
180
|
-
}
|
|
181
|
-
const operationalStateId = state_to_matter_operational_status(status);
|
|
182
|
-
if (operationalStateId) {
|
|
183
|
-
const dssHasError = hasDockingStationError(robot.dockStationStatus);
|
|
184
|
-
if (!(dssHasError && self.triggerDssError(robot))) {
|
|
185
|
-
robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', operationalStateId, platform.log);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
case Protocol.rpc_response: {
|
|
191
|
-
const response = data.dps[messageType];
|
|
192
|
-
if (!isStatusUpdate(response.result)) {
|
|
193
|
-
platform.log.debug('Ignore message:', debugStringify(data));
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
let roboStatus;
|
|
197
|
-
if (Array.isArray(response.result) && response.result.length > 0) {
|
|
198
|
-
roboStatus = response.result[0];
|
|
199
|
-
}
|
|
200
|
-
if (roboStatus) {
|
|
201
|
-
const message = { ...roboStatus };
|
|
202
|
-
platform.log.debug('rpc_response:', debugStringify(message));
|
|
203
|
-
await self.updateFromMQTTMessage(NotifyMessageTypes.LocalMessage, message, duid, true);
|
|
204
|
-
}
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
case Protocol.suction_power:
|
|
208
|
-
case Protocol.water_box_mode: {
|
|
209
|
-
await platform.roborockService?.getCleanModeData(duid).then((cleanModeData) => {
|
|
210
|
-
if (cleanModeData) {
|
|
211
|
-
const currentCleanMode = getCurrentCleanModeFunc(robot.device.data.model, platform.enableExperimentalFeature?.advancedFeature?.forceRunAtDefault ?? false)({
|
|
212
|
-
suctionPower: cleanModeData.suctionPower,
|
|
213
|
-
waterFlow: cleanModeData.waterFlow,
|
|
214
|
-
distance_off: cleanModeData.distance_off,
|
|
215
|
-
mopRoute: cleanModeData.mopRoute,
|
|
216
|
-
});
|
|
217
|
-
platform.log.debug(`Clean mode data: ${debugStringify(cleanModeData)}`);
|
|
218
|
-
platform.log.debug(`Current clean mode: ${currentCleanMode}`);
|
|
219
|
-
if (currentCleanMode) {
|
|
220
|
-
robot.updateAttribute(RvcCleanMode.Cluster.id, 'currentMode', currentCleanMode, platform.log);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
case Protocol.additional_props: {
|
|
227
|
-
platform.log.notice(`Received additional properties for robot ${duid}: ${debugStringify(data)}`);
|
|
228
|
-
const propCode = data.dps[Protocol.additional_props];
|
|
229
|
-
platform.log.debug(`DPS for additional properties: ${propCode}, AdditionalPropCode: ${AdditionalPropCode[propCode]}`);
|
|
230
|
-
const enableMultipleMap = (platform.enableExperimentalFeature?.enableExperimentalFeature && platform.enableExperimentalFeature?.advancedFeature?.enableMultipleMap) ?? false;
|
|
231
|
-
if (propCode === AdditionalPropCode.map_change) {
|
|
232
|
-
platform.log.notice('------------------------ get roomData ----------------------------');
|
|
233
|
-
const roomMap = await getRoomMapFromDevice(robot.device, platform);
|
|
234
|
-
platform.log.notice('------------------------ Room map updated ------------------------');
|
|
235
|
-
const { supportedAreas, supportedMaps, roomIndexMap } = getSupportedAreas(robot.device.rooms, roomMap, enableMultipleMap, platform.log);
|
|
236
|
-
platform.log.notice(`Supported areas: ${debugStringify(supportedAreas)}`);
|
|
237
|
-
platform.log.notice('------------------------ Supported areas updated ------------------');
|
|
238
|
-
platform.roborockService?.setSupportedAreas(duid, supportedAreas);
|
|
239
|
-
platform.roborockService?.setSelectedAreas(duid, []);
|
|
240
|
-
robot.updateAttribute(ServiceArea.Cluster.id, 'supportedAreas', supportedAreas, platform.log);
|
|
241
|
-
robot.updateAttribute(ServiceArea.Cluster.id, 'selectedAreas', [], platform.log);
|
|
242
|
-
robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', null, platform.log);
|
|
243
|
-
if (enableMultipleMap) {
|
|
244
|
-
platform.roborockService?.setSupportedAreaIndexMap(duid, roomIndexMap);
|
|
245
|
-
robot.updateAttribute(ServiceArea.Cluster.id, 'supportedMaps', supportedMaps, platform.log);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
|
-
case Protocol.back_type: {
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
253
|
-
default: {
|
|
254
|
-
platform.log.notice(`Unknown message type ${messageType}, protocol: ${Protocol[messageType]}, message: ${debugStringify(data)}`);
|
|
255
|
-
break;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
async processAdditionalProps(robot, message, duid) {
|
|
261
|
-
const dssStatus = this.getDssStatus(message, duid);
|
|
262
|
-
if (dssStatus) {
|
|
263
|
-
this.triggerDssError(robot);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
getDssStatus(message, duid) {
|
|
267
|
-
const platform = this.platform;
|
|
268
|
-
const robot = platform.robots.get(duid);
|
|
269
|
-
if (robot === undefined) {
|
|
270
|
-
platform.log.error(`Error4: Robot with DUID ${duid} not found`);
|
|
271
|
-
return undefined;
|
|
272
|
-
}
|
|
273
|
-
if (platform.enableExperimentalFeature &&
|
|
274
|
-
platform.enableExperimentalFeature.enableExperimentalFeature &&
|
|
275
|
-
platform.enableExperimentalFeature.advancedFeature.includeDockStationStatus &&
|
|
276
|
-
message.dss !== undefined) {
|
|
277
|
-
const dss = parseDockingStationStatus(message.dss);
|
|
278
|
-
if (dss && robot) {
|
|
279
|
-
robot.dockStationStatus = dss;
|
|
280
|
-
}
|
|
281
|
-
if (dss && hasDockingStationError(dss)) {
|
|
282
|
-
return RvcOperationalState.OperationalState.Error;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
return undefined;
|
|
286
|
-
}
|
|
287
|
-
updateFromHomeData(homeData) {
|
|
288
|
-
const platform = this.platform;
|
|
289
|
-
if (platform.robots.size === 0)
|
|
290
|
-
return;
|
|
291
|
-
const devices = homeData.devices.filter((d) => platform.robots.has(d.duid));
|
|
292
|
-
for (const device of devices) {
|
|
293
|
-
const robot = platform.robots.get(device.duid);
|
|
294
|
-
if (robot === undefined) {
|
|
295
|
-
platform.log.error(`Error5: Robot with DUID ${device.duid} not found`);
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
const deviceData = robot.device.data;
|
|
299
|
-
if (!device || deviceData === undefined) {
|
|
300
|
-
platform.log.error('Device not found in home data');
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
device.schema = homeData.products.find((prd) => prd.id === device.productId || prd.model === device.data.model)?.schema ?? [];
|
|
304
|
-
this.platform.log.debug('updateFromHomeData-homeData:', debugStringify(homeData));
|
|
305
|
-
this.platform.log.debug('updateFromHomeData-device:', debugStringify(device));
|
|
306
|
-
const batteryLevel = getVacuumProperty(device, 'battery');
|
|
307
|
-
this.platform.log.debug('updateFromHomeData-schema:' + debugStringify(device.schema));
|
|
308
|
-
this.platform.log.debug('updateFromHomeData-battery:' + debugStringify(device.deviceStatus));
|
|
309
|
-
if (batteryLevel) {
|
|
310
|
-
robot.updateAttribute(PowerSource.Cluster.id, 'batPercentRemaining', batteryLevel ? batteryLevel * 2 : 200, platform.log);
|
|
311
|
-
robot.updateAttribute(PowerSource.Cluster.id, 'batChargeLevel', getBatteryStatus(batteryLevel), platform.log);
|
|
312
|
-
}
|
|
313
|
-
const state = getVacuumProperty(device, 'state');
|
|
314
|
-
const matterState = state_to_matter_state(state);
|
|
315
|
-
if (!state || !matterState) {
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
this.platform.log.debug(`updateFromHomeData-RvcRunMode code: ${state} name: ${OperationStatusCode[state]}, matterState: ${RvcRunMode.ModeTag[matterState]}`);
|
|
319
|
-
if (matterState) {
|
|
320
|
-
robot.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(matterState), platform.log);
|
|
321
|
-
}
|
|
322
|
-
const operationalStateId = state_to_matter_operational_status(state);
|
|
323
|
-
if (operationalStateId) {
|
|
324
|
-
const dssHasError = hasDockingStationError(robot.dockStationStatus);
|
|
325
|
-
this.platform.log.debug(`dssHasError: ${dssHasError}, dockStationStatus: ${debugStringify(robot.dockStationStatus ?? {})}`);
|
|
326
|
-
if (!(dssHasError && this.triggerDssError(robot))) {
|
|
327
|
-
this.platform.log.debug(`updateFromHomeData-OperationalState: ${RvcOperationalState.OperationalState[operationalStateId]}`);
|
|
328
|
-
robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', operationalStateId, platform.log);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
if (batteryLevel) {
|
|
332
|
-
robot.updateAttribute(PowerSource.Cluster.id, 'batChargeState', getBatteryState(state, batteryLevel), platform.log);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
triggerDssError(robot) {
|
|
337
|
-
const platform = this.platform;
|
|
338
|
-
const currentOperationState = robot.getAttribute(RvcOperationalState.Cluster.id, 'operationalState');
|
|
339
|
-
if (currentOperationState === RvcOperationalState.OperationalState.Error) {
|
|
340
|
-
return true;
|
|
341
|
-
}
|
|
342
|
-
if (currentOperationState === RvcOperationalState.OperationalState.Docked) {
|
|
343
|
-
robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.Error, platform.log);
|
|
344
|
-
return true;
|
|
345
|
-
}
|
|
346
|
-
return false;
|
|
347
|
-
}
|
|
348
90
|
}
|
|
@@ -11,9 +11,9 @@ export class AbstractClient {
|
|
|
11
11
|
messageListeners = new ChainedMessageListener();
|
|
12
12
|
serializer;
|
|
13
13
|
deserializer;
|
|
14
|
+
context;
|
|
14
15
|
connected = false;
|
|
15
16
|
logger;
|
|
16
|
-
context;
|
|
17
17
|
syncMessageListener;
|
|
18
18
|
constructor(logger, context) {
|
|
19
19
|
this.context = context;
|
|
@@ -40,8 +40,8 @@ export class AbstractClient {
|
|
|
40
40
|
return undefined;
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
-
registerDevice(duid, localKey, pv) {
|
|
44
|
-
this.context.registerDevice(duid, localKey, pv);
|
|
43
|
+
registerDevice(duid, localKey, pv, nonce) {
|
|
44
|
+
this.context.registerDevice(duid, localKey, pv, nonce);
|
|
45
45
|
}
|
|
46
46
|
registerConnectionListener(listener) {
|
|
47
47
|
this.connectionListeners.register(listener);
|
|
@@ -148,6 +148,7 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
148
148
|
const request = new RequestMessage({
|
|
149
149
|
protocol: Protocol.hello_request,
|
|
150
150
|
messageId: this.messageIdSeq.next(),
|
|
151
|
+
nonce: this.context.nonce,
|
|
151
152
|
});
|
|
152
153
|
await this.send(this.duid, request);
|
|
153
154
|
}
|
|
@@ -17,8 +17,11 @@ export class ClientRouter {
|
|
|
17
17
|
this.mqttClient.registerConnectionListener(this.connectionListeners);
|
|
18
18
|
this.mqttClient.registerMessageListener(this.messageListeners);
|
|
19
19
|
}
|
|
20
|
-
registerDevice(duid, localKey, pv) {
|
|
21
|
-
this.context.registerDevice(duid, localKey, pv);
|
|
20
|
+
registerDevice(duid, localKey, pv, nonce) {
|
|
21
|
+
this.context.registerDevice(duid, localKey, pv, nonce);
|
|
22
|
+
}
|
|
23
|
+
updateNonce(duid, nonce) {
|
|
24
|
+
this.context.updateNonce(duid, nonce);
|
|
22
25
|
}
|
|
23
26
|
registerClient(duid, ip) {
|
|
24
27
|
const localClient = new LocalNetworkClient(this.logger, this.context, duid, ip);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
1
|
+
import { randomBytes, randomInt } from 'node:crypto';
|
|
2
2
|
import * as CryptoUtils from '../../helper/cryptoHelper.js';
|
|
3
3
|
export class MessageContext {
|
|
4
4
|
endpoint;
|
|
5
|
-
nonce;
|
|
6
5
|
devices = new Map();
|
|
6
|
+
nonce;
|
|
7
|
+
serializeNonce;
|
|
7
8
|
constructor(userdata) {
|
|
8
9
|
this.endpoint = CryptoUtils.md5bin(userdata.rriot.k).subarray(8, 14).toString('base64');
|
|
9
|
-
this.nonce =
|
|
10
|
+
this.nonce = randomInt(1000, 1000000);
|
|
11
|
+
this.serializeNonce = randomBytes(16);
|
|
10
12
|
}
|
|
11
|
-
registerDevice(duid, localKey, pv) {
|
|
12
|
-
this.devices.set(duid, { localKey: localKey, protocolVersion: pv });
|
|
13
|
+
registerDevice(duid, localKey, pv, nonce) {
|
|
14
|
+
this.devices.set(duid, { localKey: localKey, protocolVersion: pv, nonce });
|
|
13
15
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
updateNonce(duid, nonce) {
|
|
17
|
+
const device = this.devices.get(duid);
|
|
18
|
+
if (device) {
|
|
19
|
+
device.nonce = nonce;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
getSerializeNonceAsHex() {
|
|
23
|
+
return this.serializeNonce.toString('hex').toUpperCase();
|
|
16
24
|
}
|
|
17
25
|
getLocalKey(duid) {
|
|
18
26
|
return this.devices.get(duid)?.localKey;
|
|
@@ -20,6 +28,9 @@ export class MessageContext {
|
|
|
20
28
|
getProtocolVersion(duid) {
|
|
21
29
|
return this.devices.get(duid)?.protocolVersion;
|
|
22
30
|
}
|
|
31
|
+
getDeviceNonce(duid) {
|
|
32
|
+
return this.devices.get(duid)?.nonce;
|
|
33
|
+
}
|
|
23
34
|
getEndpoint() {
|
|
24
35
|
return this.endpoint;
|
|
25
36
|
}
|
|
@@ -6,12 +6,16 @@ export class RequestMessage {
|
|
|
6
6
|
method;
|
|
7
7
|
params;
|
|
8
8
|
secure;
|
|
9
|
+
timestamp;
|
|
10
|
+
nonce;
|
|
9
11
|
constructor(args) {
|
|
10
12
|
this.messageId = args.messageId ?? randomInt(10000, 32767);
|
|
11
13
|
this.protocol = args.protocol ?? Protocol.rpc_request;
|
|
12
14
|
this.method = args.method;
|
|
13
15
|
this.params = args.params;
|
|
14
16
|
this.secure = args.secure ?? false;
|
|
17
|
+
this.nonce = args.nonce ?? randomInt(10000, 32767);
|
|
18
|
+
this.timestamp = args.timestamp ?? Math.floor(Date.now() / 1000);
|
|
15
19
|
}
|
|
16
20
|
toMqttRequest() {
|
|
17
21
|
return this;
|
|
@@ -24,6 +28,7 @@ export class RequestMessage {
|
|
|
24
28
|
method: this.method,
|
|
25
29
|
params: this.params,
|
|
26
30
|
secure: this.secure,
|
|
31
|
+
timestamp: this.timestamp,
|
|
27
32
|
});
|
|
28
33
|
}
|
|
29
34
|
else {
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import CRC32 from 'crc-32';
|
|
3
|
-
import { Parser } from 'binary-parser';
|
|
4
3
|
import { ResponseMessage } from '../broadcast/model/responseMessage.js';
|
|
5
4
|
import * as CryptoUtils from './cryptoHelper.js';
|
|
6
5
|
import { Protocol } from '../broadcast/model/protocol.js';
|
|
6
|
+
import { Parser } from 'binary-parser/dist/binary_parser.js';
|
|
7
7
|
export class MessageDeserializer {
|
|
8
8
|
context;
|
|
9
|
-
|
|
9
|
+
headerMessageParser;
|
|
10
|
+
contentMessageParser;
|
|
10
11
|
logger;
|
|
11
12
|
supportedVersions = ['1.0', 'A01', 'B01'];
|
|
12
13
|
constructor(context, logger) {
|
|
13
14
|
this.context = context;
|
|
14
15
|
this.logger = logger;
|
|
15
|
-
this.
|
|
16
|
+
this.headerMessageParser = new Parser()
|
|
16
17
|
.endianness('big')
|
|
17
18
|
.string('version', {
|
|
18
19
|
length: 3,
|
|
19
20
|
})
|
|
20
21
|
.uint32('seq')
|
|
21
|
-
.uint32('
|
|
22
|
+
.uint32('nonce')
|
|
22
23
|
.uint32('timestamp')
|
|
23
|
-
.uint16('protocol')
|
|
24
|
+
.uint16('protocol');
|
|
25
|
+
this.contentMessageParser = new Parser()
|
|
26
|
+
.endianness('big')
|
|
24
27
|
.uint16('payloadLen')
|
|
25
28
|
.buffer('payload', {
|
|
26
29
|
length: 'payloadLen',
|
|
@@ -28,10 +31,21 @@ export class MessageDeserializer {
|
|
|
28
31
|
.uint32('crc32');
|
|
29
32
|
}
|
|
30
33
|
deserialize(duid, message) {
|
|
31
|
-
const
|
|
32
|
-
if (!this.supportedVersions.includes(version)) {
|
|
33
|
-
throw new Error('unknown protocol version ' + version);
|
|
34
|
+
const header = this.headerMessageParser.parse(message);
|
|
35
|
+
if (!this.supportedVersions.includes(header.version)) {
|
|
36
|
+
throw new Error('unknown protocol version ' + header.version);
|
|
37
|
+
}
|
|
38
|
+
if (header.protocol === Protocol.hello_response || header.protocol === Protocol.ping_response) {
|
|
39
|
+
const dpsValue = {
|
|
40
|
+
id: header.seq,
|
|
41
|
+
result: {
|
|
42
|
+
version: header.version,
|
|
43
|
+
nonce: header.nonce,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
return new ResponseMessage(duid, { [header.protocol.toString()]: dpsValue });
|
|
34
47
|
}
|
|
48
|
+
const data = this.contentMessageParser.parse(message.subarray(this.headerMessageParser.sizeOf()));
|
|
35
49
|
const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0;
|
|
36
50
|
const expectedCrc32 = message.readUInt32BE(message.length - 4);
|
|
37
51
|
if (crc32 != expectedCrc32) {
|
|
@@ -42,30 +56,29 @@ export class MessageDeserializer {
|
|
|
42
56
|
this.logger.notice(`Unable to retrieve local key for ${duid}, it should be from other vacuums`);
|
|
43
57
|
return new ResponseMessage(duid, { dps: { id: 0, result: null } });
|
|
44
58
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const aesKey = CryptoUtils.md5bin(CryptoUtils.encodeTimestamp(data.timestamp) + localKey + CryptoUtils.SALT);
|
|
59
|
+
if (header.version == '1.0') {
|
|
60
|
+
const aesKey = CryptoUtils.md5bin(CryptoUtils.encodeTimestamp(header.timestamp) + localKey + CryptoUtils.SALT);
|
|
48
61
|
const decipher = crypto.createDecipheriv('aes-128-ecb', aesKey, null);
|
|
49
62
|
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
50
63
|
}
|
|
51
|
-
else if (version == 'A01') {
|
|
52
|
-
const iv = CryptoUtils.md5hex(
|
|
64
|
+
else if (header.version == 'A01') {
|
|
65
|
+
const iv = CryptoUtils.md5hex(header.nonce.toString(16).padStart(8, '0') + '726f626f726f636b2d67a6d6da').substring(8, 24);
|
|
53
66
|
const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
|
|
54
67
|
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
55
68
|
}
|
|
56
|
-
else if (version == 'B01') {
|
|
57
|
-
const iv = CryptoUtils.md5hex(
|
|
69
|
+
else if (header.version == 'B01') {
|
|
70
|
+
const iv = CryptoUtils.md5hex(header.nonce.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25);
|
|
58
71
|
const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
|
|
59
72
|
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
60
73
|
}
|
|
61
|
-
if (
|
|
74
|
+
if (header.protocol == Protocol.map_response) {
|
|
62
75
|
return new ResponseMessage(duid, { dps: { id: 0, result: null } });
|
|
63
76
|
}
|
|
64
|
-
if (
|
|
77
|
+
if (header.protocol == Protocol.rpc_response || header.protocol == Protocol.general_request) {
|
|
65
78
|
return this.deserializeRpcResponse(duid, data);
|
|
66
79
|
}
|
|
67
80
|
else {
|
|
68
|
-
this.logger.error('unknown protocol: ' +
|
|
81
|
+
this.logger.error('unknown protocol: ' + header.protocol);
|
|
69
82
|
return new ResponseMessage(duid, { dps: { id: 0, result: null } });
|
|
70
83
|
}
|
|
71
84
|
}
|