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.
Files changed (59) hide show
  1. package/dist/helper.js +2 -7
  2. package/dist/model/RoomMap.js +1 -1
  3. package/dist/model/roomIndexMap.js +8 -4
  4. package/dist/platform.js +4 -3
  5. package/dist/platformRunner.js +10 -268
  6. package/dist/roborockCommunication/broadcast/abstractClient.js +3 -3
  7. package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +1 -0
  8. package/dist/roborockCommunication/broadcast/client/LocalNetworkUDPClient.js +1 -1
  9. package/dist/roborockCommunication/broadcast/clientRouter.js +5 -2
  10. package/dist/roborockCommunication/broadcast/model/contentMessage.js +1 -0
  11. package/dist/roborockCommunication/broadcast/model/headerMessage.js +1 -0
  12. package/dist/roborockCommunication/broadcast/model/messageContext.js +18 -7
  13. package/dist/roborockCommunication/broadcast/model/requestMessage.js +5 -0
  14. package/dist/roborockCommunication/helper/messageDeserializer.js +31 -18
  15. package/dist/roborockCommunication/helper/messageSerializer.js +17 -11
  16. package/dist/roborockService.js +6 -1
  17. package/dist/runtimes/handleCloudMessage.js +110 -0
  18. package/dist/runtimes/handleHomeDataMessage.js +57 -0
  19. package/dist/runtimes/handleLocalMessage.js +169 -0
  20. package/dist/rvc.js +2 -1
  21. package/dist/tests/testData/mockData.js +359 -0
  22. package/matterbridge-roborock-vacuum-plugin.config.json +2 -2
  23. package/matterbridge-roborock-vacuum-plugin.schema.json +10 -37
  24. package/package.json +2 -3
  25. package/src/behaviors/roborock.vacuum/default/runtimes.ts +1 -1
  26. package/src/behaviors/roborock.vacuum/smart/runtimes.ts +1 -1
  27. package/src/helper.ts +2 -12
  28. package/src/initialData/getSupportedAreas.ts +1 -9
  29. package/src/model/RoomMap.ts +4 -30
  30. package/src/model/roomIndexMap.ts +10 -6
  31. package/src/platform.ts +6 -3
  32. package/src/platformRunner.ts +12 -350
  33. package/src/roborockCommunication/Zmodel/device.ts +13 -1
  34. package/src/roborockCommunication/Zmodel/messageResult.ts +28 -27
  35. package/src/roborockCommunication/Zmodel/userData.ts +2 -1
  36. package/src/roborockCommunication/broadcast/abstractClient.ts +3 -3
  37. package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +4 -2
  38. package/src/roborockCommunication/broadcast/client/LocalNetworkUDPClient.ts +3 -3
  39. package/src/roborockCommunication/broadcast/clientRouter.ts +6 -2
  40. package/src/roborockCommunication/broadcast/model/contentMessage.ts +5 -0
  41. package/src/roborockCommunication/broadcast/model/headerMessage.ts +7 -0
  42. package/src/roborockCommunication/broadcast/model/messageContext.ts +24 -11
  43. package/src/roborockCommunication/broadcast/model/requestMessage.ts +12 -5
  44. package/src/roborockCommunication/helper/messageDeserializer.ts +42 -31
  45. package/src/roborockCommunication/helper/messageSerializer.ts +19 -14
  46. package/src/roborockService.ts +8 -1
  47. package/src/runtimes/handleCloudMessage.ts +134 -0
  48. package/src/runtimes/handleHomeDataMessage.ts +67 -0
  49. package/src/runtimes/handleLocalMessage.ts +209 -0
  50. package/src/rvc.ts +2 -2
  51. package/src/share/runtimeHelper.ts +1 -1
  52. package/src/tests/helper.test.ts +59 -10
  53. package/src/tests/roborockCommunication/broadcast/client/LocalNetworkClient.test.ts +0 -19
  54. package/src/tests/roborockCommunication/broadcast/client/MQTTClient.test.ts +0 -32
  55. package/src/tests/roborockService.setSelectedAreas.test.ts +61 -0
  56. package/src/tests/runtimes/handleCloudMessage.test.ts +200 -0
  57. package/src/tests/runtimes/handleHomeDataMessage.test.ts +53 -0
  58. package/src/tests/runtimes/handleLocalMessage.test.ts +222 -0
  59. 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.notice(`getRoomMapFromDevice - mapInfo: ${mapInfo ? debugStringify(mapInfo) : 'undefined'}`);
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
  }
@@ -9,7 +9,7 @@ export class RoomMap {
9
9
  return {
10
10
  id,
11
11
  globalId: globalId !== undefined ? Number(globalId) : undefined,
12
- displayName: room?.name,
12
+ displayName: room?.name ?? `Room ${id}`,
13
13
  alternativeId: `${id}${tag}`,
14
14
  mapId,
15
15
  };
@@ -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, areaId);
7
+ for (const [areaId, { roomId, mapId }] of roomMap.entries()) {
8
+ this.roomMap.set(`${roomId}:${mapId}`, areaId);
9
9
  }
10
10
  }
11
- getAreaId(roomId) {
12
- return this.roomMap.get(roomId);
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.1.8')) {
30
- throw new Error(`This plugin requires Matterbridge version >= "3.1.8". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend.`);
29
+ if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.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);
@@ -1,15 +1,10 @@
1
- import { RvcRunMode, PowerSource, ServiceArea, RvcOperationalState, RvcCleanMode } from 'matterbridge/matter/clusters';
2
- import { getRoomMap, getRoomMapFromDevice, getVacuumProperty, isStatusUpdate } from './helper.js';
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
- this.updateFromHomeData(homeData);
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 (!robot) {
78
- platform.log.error(`Error2: Robot with DUID ${duid} not found`);
72
+ if (robot && data) {
73
+ await handleLocalMessage(data, platform, duid);
79
74
  return;
80
75
  }
81
- if (data) {
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
- this.handlerCloudMessage(data, duid);
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
  }
@@ -1,5 +1,5 @@
1
1
  import * as dgram from 'node:dgram';
2
- import { Parser } from 'binary-parser';
2
+ import { Parser } from 'binary-parser/dist/binary_parser.js';
3
3
  import crypto from 'node:crypto';
4
4
  import CRC32 from 'crc-32';
5
5
  import { AbstractClient } from '../abstractClient.js';
@@ -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);
@@ -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 = randomBytes(16);
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
- getNonceAsHex() {
15
- return this.nonce.toString('hex').toUpperCase();
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
- messageParser;
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.messageParser = new Parser()
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('random')
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 version = message.toString('latin1', 0, 3);
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
- const data = this.messageParser.parse(message);
46
- if (version == '1.0') {
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(data.random.toString(16).padStart(8, '0') + '726f626f726f636b2d67a6d6da').substring(8, 24);
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(data.random.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25);
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 (data.protocol == Protocol.map_response) {
74
+ if (header.protocol == Protocol.map_response) {
62
75
  return new ResponseMessage(duid, { dps: { id: 0, result: null } });
63
76
  }
64
- if (data.protocol == Protocol.rpc_response || data.protocol == Protocol.general_request) {
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: ' + data.protocol);
81
+ this.logger.error('unknown protocol: ' + header.protocol);
69
82
  return new ResponseMessage(duid, { dps: { id: 0, result: null } });
70
83
  }
71
84
  }