matterbridge-roborock-vacuum-plugin 1.0.7 → 1.0.8-rc01

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 (27) hide show
  1. package/dist/behaviorFactory.js +1 -1
  2. package/dist/behaviors/roborock.vacuum/QREVO_EDGE_5V1/a187.js +3 -3
  3. package/dist/behaviors/roborock.vacuum/S7_MAXV/a27.js +50 -7
  4. package/dist/roborockCommunication/broadcast/abstractClient.js +2 -2
  5. package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +7 -7
  6. package/dist/roborockCommunication/broadcast/client/MQTTClient.js +1 -1
  7. package/dist/roborockCommunication/broadcast/messageProcessor.js +15 -6
  8. package/dist/roborockCommunication/helper/messageDeserializer.js +9 -6
  9. package/dist/roborockCommunication/helper/messageSerializer.js +3 -1
  10. package/dist/share/function.js +6 -6
  11. package/matterbridge-roborock-vacuum-plugin.config.json +1 -1
  12. package/matterbridge-roborock-vacuum-plugin.schema.json +1 -1
  13. package/package.json +1 -1
  14. package/src/behaviorFactory.ts +1 -1
  15. package/src/behaviors/roborock.vacuum/QREVO_EDGE_5V1/a187.ts +3 -3
  16. package/src/behaviors/roborock.vacuum/QREVO_EDGE_5V1/runtimes.test.ts +79 -0
  17. package/src/behaviors/roborock.vacuum/S7_MAXV/a27.test.ts +134 -0
  18. package/src/behaviors/roborock.vacuum/S7_MAXV/a27.ts +62 -9
  19. package/src/roborockCommunication/broadcast/abstractClient.ts +2 -2
  20. package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +9 -8
  21. package/src/roborockCommunication/broadcast/client/MQTTClient.ts +1 -1
  22. package/src/roborockCommunication/broadcast/messageProcessor.ts +15 -9
  23. package/src/roborockCommunication/helper/messageDeserializer.ts +12 -11
  24. package/src/roborockCommunication/helper/messageSerializer.ts +4 -1
  25. package/src/share/function.ts +6 -6
  26. package/dist/appliances.js +0 -6
  27. package/src/appliances.ts +0 -15
@@ -12,7 +12,7 @@ export function configurateBehavior(model, duid, roborockService, cleanModeSetti
12
12
  }
13
13
  case DeviceModel.S7_MAXV: {
14
14
  const deviceHandler = new BehaviorDeviceGeneric(logger);
15
- setCommandHandlerA27(duid, deviceHandler, logger, roborockService);
15
+ setCommandHandlerA27(duid, deviceHandler, logger, roborockService, cleanModeSettings);
16
16
  return deviceHandler;
17
17
  }
18
18
  default: {
@@ -53,9 +53,9 @@ const RvcCleanMode = {
53
53
  };
54
54
  const CleanSetting = {
55
55
  [4]: { suctionPower: 0, waterFlow: 0, distance_off: 0, mopRoute: MopRouteA187.Smart },
56
- [5]: { suctionPower: VacuumSuctionPowerA187.Off, waterFlow: MopWaterFlowA187.Medium, distance_off: 0, mopRoute: MopRouteA187.Custom },
57
- [6]: { suctionPower: VacuumSuctionPowerA187.Balanced, waterFlow: MopWaterFlowA187.Off, distance_off: 0, mopRoute: MopRouteA187.Custom },
58
- [7]: { suctionPower: VacuumSuctionPowerA187.Balanced, waterFlow: MopWaterFlowA187.Medium, distance_off: 0, mopRoute: MopRouteA187.Custom },
56
+ [5]: { suctionPower: VacuumSuctionPowerA187.Off, waterFlow: MopWaterFlowA187.Medium, distance_off: 0, mopRoute: MopRouteA187.Standard },
57
+ [6]: { suctionPower: VacuumSuctionPowerA187.Balanced, waterFlow: MopWaterFlowA187.Off, distance_off: 0, mopRoute: MopRouteA187.Standard },
58
+ [7]: { suctionPower: VacuumSuctionPowerA187.Balanced, waterFlow: MopWaterFlowA187.Medium, distance_off: 0, mopRoute: MopRouteA187.Standard },
59
59
  [8]: { suctionPower: VacuumSuctionPowerA187.Custom, waterFlow: MopWaterFlowA187.Custom, distance_off: 0, mopRoute: MopRouteA187.Custom },
60
60
  };
61
61
  export function setCommandHandlerA187(duid, handler, logger, roborockService, cleanModeSettings) {
@@ -47,12 +47,12 @@ const RvcCleanMode = {
47
47
  [8]: 'Custom',
48
48
  };
49
49
  const CleanSetting = {
50
- [5]: { suctionPower: VacuumSuctionPowerA27.Off, waterFlow: MopWaterFlowA27.Medium, mopRoute: MopRouteA27.Custom },
51
- [6]: { suctionPower: VacuumSuctionPowerA27.Balanced, waterFlow: MopWaterFlowA27.Off, mopRoute: MopRouteA27.Custom },
52
- [7]: { suctionPower: VacuumSuctionPowerA27.Balanced, waterFlow: MopWaterFlowA27.Medium, mopRoute: MopRouteA27.Custom },
53
- [8]: { suctionPower: VacuumSuctionPowerA27.Custom, waterFlow: MopWaterFlowA27.Custom, mopRoute: MopRouteA27.Custom },
50
+ [5]: { suctionPower: VacuumSuctionPowerA27.Off, waterFlow: MopWaterFlowA27.Medium, distance_off: 0, mopRoute: MopRouteA27.Standard },
51
+ [6]: { suctionPower: VacuumSuctionPowerA27.Balanced, waterFlow: MopWaterFlowA27.Off, distance_off: 0, mopRoute: MopRouteA27.Standard },
52
+ [7]: { suctionPower: VacuumSuctionPowerA27.Balanced, waterFlow: MopWaterFlowA27.Medium, distance_off: 0, mopRoute: MopRouteA27.Standard },
53
+ [8]: { suctionPower: VacuumSuctionPowerA27.Custom, waterFlow: MopWaterFlowA27.Custom, distance_off: 0, mopRoute: MopRouteA27.Custom },
54
54
  };
55
- export function setCommandHandlerA27(duid, handler, logger, roborockService) {
55
+ export function setCommandHandlerA27(duid, handler, logger, roborockService, cleanModeSettings) {
56
56
  handler.setCommandHandler('changeToMode', async (newMode) => {
57
57
  const activity = RvcRunMode[newMode] || RvcCleanMode[newMode];
58
58
  switch (activity) {
@@ -63,10 +63,18 @@ export function setCommandHandlerA27(duid, handler, logger, roborockService) {
63
63
  }
64
64
  case 'Mop':
65
65
  case 'Vacuum':
66
- case 'Vac & Mop':
66
+ case 'Vac & Mop': {
67
+ const setting = cleanModeSettings ? getSettingFromCleanMode(activity, cleanModeSettings) : CleanSetting[newMode];
68
+ logger.notice(`BehaviorA27-ChangeCleanMode to: ${activity}, setting: ${debugStringify(setting ?? {})}`);
69
+ if (setting) {
70
+ await roborockService.changeCleanMode(duid, setting);
71
+ }
72
+ return;
73
+ }
67
74
  case 'Custom': {
68
75
  const setting = CleanSetting[newMode];
69
- logger.notice(`BehaviorA27-ChangeCleanMode to: ${activity}, code: ${debugStringify(setting)}`);
76
+ logger.notice(`BehaviorA27-ChangeCleanMode to: ${activity}, setting: ${debugStringify(setting)}`);
77
+ await roborockService.changeCleanMode(duid, setting);
70
78
  return;
71
79
  }
72
80
  default:
@@ -74,6 +82,41 @@ export function setCommandHandlerA27(duid, handler, logger, roborockService) {
74
82
  return;
75
83
  }
76
84
  });
85
+ const getSettingFromCleanMode = (activity, cleanModeSettings) => {
86
+ switch (activity) {
87
+ case 'Mop': {
88
+ const mopSetting = cleanModeSettings?.mopping;
89
+ const waterFlow = MopWaterFlowA27[mopSetting?.waterFlowMode] ?? MopWaterFlowA27.Medium;
90
+ return {
91
+ suctionPower: VacuumSuctionPowerA27.Off,
92
+ waterFlow,
93
+ distance_off: 0,
94
+ mopRoute: MopRouteA27[mopSetting?.mopRouteMode] ?? MopRouteA27.Standard,
95
+ };
96
+ }
97
+ case 'Vacuum': {
98
+ const vacuumSetting = cleanModeSettings?.vacuuming;
99
+ return {
100
+ suctionPower: VacuumSuctionPowerA27[vacuumSetting?.fanMode] ?? VacuumSuctionPowerA27.Balanced,
101
+ waterFlow: MopWaterFlowA27.Off,
102
+ distance_off: 0,
103
+ mopRoute: MopRouteA27[vacuumSetting?.mopRouteMode] ?? MopRouteA27.Standard,
104
+ };
105
+ }
106
+ case 'Vac & Mop': {
107
+ const vacmopSetting = cleanModeSettings?.vacmop;
108
+ const waterFlow = MopWaterFlowA27[vacmopSetting?.waterFlowMode] ?? MopWaterFlowA27.Medium;
109
+ return {
110
+ suctionPower: VacuumSuctionPowerA27[vacmopSetting?.fanMode] ?? VacuumSuctionPowerA27.Balanced,
111
+ waterFlow,
112
+ distance_off: 0,
113
+ mopRoute: MopRouteA27[vacmopSetting?.mopRouteMode] ?? MopRouteA27.Standard,
114
+ };
115
+ }
116
+ default:
117
+ return undefined;
118
+ }
119
+ };
77
120
  handler.setCommandHandler('selectAreas', async (newAreas) => {
78
121
  logger.notice(`BehaviorA27-selectAreas: ${newAreas}`);
79
122
  roborockService.setSelectedAreas(duid, newAreas ?? []);
@@ -14,8 +14,8 @@ export class AbstractClient {
14
14
  logger;
15
15
  constructor(logger, context) {
16
16
  this.context = context;
17
- this.serializer = new MessageSerializer(this.context);
18
- this.deserializer = new MessageDeserializer(this.context);
17
+ this.serializer = new MessageSerializer(this.context, logger);
18
+ this.deserializer = new MessageDeserializer(this.context, logger);
19
19
  this.syncMessageListener = new SyncMessageListener(logger);
20
20
  this.messageListeners.register(this.syncMessageListener);
21
21
  this.logger = logger;
@@ -87,31 +87,31 @@ export class LocalNetworkClient extends AbstractClient {
87
87
  }
88
88
  try {
89
89
  this.buffer.append(message);
90
- const recvBuffer = this.buffer.get();
91
- if (!this.isMessageComplete(recvBuffer)) {
90
+ const receivedBuffer = this.buffer.get();
91
+ if (!this.isMessageComplete(receivedBuffer)) {
92
92
  return;
93
93
  }
94
94
  this.buffer.reset();
95
95
  let offset = 0;
96
- while (offset + 4 <= recvBuffer.length) {
97
- const segmentLength = recvBuffer.readUInt32BE(offset);
96
+ while (offset + 4 <= receivedBuffer.length) {
97
+ const segmentLength = receivedBuffer.readUInt32BE(offset);
98
98
  if (segmentLength == 17) {
99
99
  offset += 4 + segmentLength;
100
100
  continue;
101
101
  }
102
102
  try {
103
- const currentBuffer = recvBuffer.subarray(offset + 4, offset + segmentLength + 4);
103
+ const currentBuffer = receivedBuffer.subarray(offset + 4, offset + segmentLength + 4);
104
104
  const response = this.deserializer.deserialize(this.duid, currentBuffer);
105
105
  await this.messageListeners.onMessage(response);
106
106
  }
107
107
  catch (error) {
108
- this.logger.error('unable to process message: ' + error);
108
+ this.logger.error('LocalNetworkClient: unable to process message with error: ' + error);
109
109
  }
110
110
  offset += 4 + segmentLength;
111
111
  }
112
112
  }
113
113
  catch (error) {
114
- this.logger.error('failed to read from a socket: ' + error);
114
+ this.logger.error('LocalNetworkClient: read socket buffer error: ' + error);
115
115
  }
116
116
  }
117
117
  isMessageComplete(buffer) {
@@ -97,7 +97,7 @@ export class MQTTClient extends AbstractClient {
97
97
  await this.messageListeners.onMessage(response);
98
98
  }
99
99
  catch (error) {
100
- this.logger.error('unable to process message from queue ' + topic + ': ' + error);
100
+ this.logger.error('MQTTClient: unable to process message with error: ' + topic + ': ' + error);
101
101
  }
102
102
  }
103
103
  }
@@ -73,20 +73,29 @@ export class MessageProcessor {
73
73
  async changeCleanMode(duid, suctionPower, waterFlow, mopRoute, distance_off) {
74
74
  this.logger?.notice(`Change clean mode for ${duid} to suctionPower: ${suctionPower}, waterFlow: ${waterFlow}, mopRoute: ${mopRoute}, distance_off: ${distance_off}`);
75
75
  const currentMopMode = await this.getCustomMessage(duid, new RequestMessage({ method: 'get_custom_mode' }));
76
- if (currentMopMode == 110) {
77
- await this.client.send(duid, new RequestMessage({ method: 'set_mop_mode', params: [302] }));
78
- }
79
- if (mopRoute && mopRoute != 0) {
80
- await this.client.send(duid, new RequestMessage({ method: 'set_mop_mode', params: [mopRoute] }));
76
+ const smartMopMode = 110;
77
+ const smartMopRoute = 306;
78
+ const customMopMode = 106;
79
+ const customMopRoute = 302;
80
+ if (currentMopMode == smartMopMode && mopRoute == smartMopRoute)
81
+ return;
82
+ if (currentMopMode == customMopMode && mopRoute == customMopRoute)
83
+ return;
84
+ if (currentMopMode == smartMopMode) {
85
+ await this.client.send(duid, new RequestMessage({ method: 'set_mop_mode', params: [customMopRoute] }));
81
86
  }
82
87
  if (suctionPower && suctionPower != 0) {
83
88
  await this.client.send(duid, new RequestMessage({ method: 'set_custom_mode', params: [suctionPower] }));
84
89
  }
85
- if (waterFlow && waterFlow == 207 && distance_off && distance_off != 0) {
90
+ const CustomizeWithDistanceOff = 207;
91
+ if (waterFlow && waterFlow == CustomizeWithDistanceOff && distance_off && distance_off != 0) {
86
92
  await this.client.send(duid, new RequestMessage({ method: 'set_water_box_custom_mode', params: { 'water_box_mode': waterFlow, 'distance_off': distance_off } }));
87
93
  }
88
94
  else if (waterFlow && waterFlow != 0) {
89
95
  await this.client.send(duid, new RequestMessage({ method: 'set_water_box_custom_mode', params: [waterFlow] }));
90
96
  }
97
+ if (mopRoute && mopRoute != 0) {
98
+ await this.client.send(duid, new RequestMessage({ method: 'set_mop_mode', params: [mopRoute] }));
99
+ }
91
100
  }
92
101
  }
@@ -7,10 +7,12 @@ import { CryptoUtils, MessageUtils } from './cryptoHelper.js';
7
7
  import { Protocol } from '../broadcast/model/protocol.js';
8
8
  export class MessageDeserializer {
9
9
  context;
10
- mqttMessageParser;
11
- constructor(context) {
10
+ messageParser;
11
+ logger;
12
+ constructor(context, logger) {
12
13
  this.context = context;
13
- this.mqttMessageParser = new Parser()
14
+ this.logger = logger;
15
+ this.messageParser = new Parser()
14
16
  .endianess('big')
15
17
  .string('version', {
16
18
  length: 3,
@@ -31,13 +33,13 @@ export class MessageDeserializer {
31
33
  throw new Error('unknown protocol version ' + version);
32
34
  }
33
35
  const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0;
34
- const expectedCrc32 = message.readUint32BE(message.length - 4);
36
+ const expectedCrc32 = message.readUInt32BE(message.length - 4);
35
37
  if (crc32 != expectedCrc32) {
36
- return new ResponseMessage(duid, { dps: { id: 0, result: null } });
38
+ throw new Error(`Wrong CRC32 ${crc32}, expected ${expectedCrc32}`);
37
39
  }
38
40
  const localKey = this.context.getLocalKey(duid);
39
41
  assert(localKey, 'unable to retrieve local key for ' + duid);
40
- const data = this.mqttMessageParser.parse(message);
42
+ const data = this.messageParser.parse(message);
41
43
  if (version == '1.0') {
42
44
  const aesKey = CryptoUtils.md5bin(MessageUtils.encodeTimestamp(data.timestamp) + localKey + MessageUtils.SALT);
43
45
  const decipher = crypto.createDecipheriv('aes-128-ecb', aesKey, null);
@@ -55,6 +57,7 @@ export class MessageDeserializer {
55
57
  return this.deserializeProtocolRpcResponse(duid, data);
56
58
  }
57
59
  else {
60
+ this.logger.error('unknown protocol: ' + data.protocol);
58
61
  return new ResponseMessage(duid, { dps: { id: 0, result: null } });
59
62
  }
60
63
  }
@@ -3,10 +3,12 @@ import crypto from 'crypto';
3
3
  import CRC32 from 'crc-32';
4
4
  export class MessageSerializer {
5
5
  context;
6
+ logger;
6
7
  seq = 1;
7
8
  random = 4711;
8
- constructor(context) {
9
+ constructor(context, logger) {
9
10
  this.context = context;
11
+ this.logger = logger;
10
12
  }
11
13
  serialize(duid, request) {
12
14
  const messageId = request.messageId;
@@ -63,12 +63,6 @@ export function state_to_matter_operational_status(state) {
63
63
  case OperationStatusCode.ZoneCleanMopCleaning:
64
64
  case OperationStatusCode.ZoneCleanMopMopping:
65
65
  case OperationStatusCode.RobotStatusMopping:
66
- case OperationStatusCode.WashingTheMop:
67
- case OperationStatusCode.WashingTheMop2:
68
- case OperationStatusCode.ReturnToDock:
69
- case OperationStatusCode.GoingToWashTheMop:
70
- case OperationStatusCode.BackToDockWashingDuster:
71
- case OperationStatusCode.EmptyingDustContainer:
72
66
  return RvcOperationalState.OperationalState.Running;
73
67
  case OperationStatusCode.InError:
74
68
  case OperationStatusCode.ChargingError:
@@ -82,7 +76,13 @@ export function state_to_matter_operational_status(state) {
82
76
  case OperationStatusCode.ShuttingDown:
83
77
  case OperationStatusCode.Locked:
84
78
  return RvcOperationalState.OperationalState.Stopped;
79
+ case OperationStatusCode.ReturnToDock:
85
80
  case OperationStatusCode.ReturningDock:
81
+ case OperationStatusCode.WashingTheMop:
82
+ case OperationStatusCode.WashingTheMop2:
83
+ case OperationStatusCode.GoingToWashTheMop:
84
+ case OperationStatusCode.BackToDockWashingDuster:
85
+ case OperationStatusCode.EmptyingDustContainer:
86
86
  return RvcOperationalState.OperationalState.SeekingCharger;
87
87
  case OperationStatusCode.Idle:
88
88
  case OperationStatusCode.Sleeping:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "matterbridge-roborock-vacuum-plugin",
3
3
  "type": "DynamicPlatform",
4
- "version": "1.0.7",
4
+ "version": "1.0.8-rc01",
5
5
  "whiteList": [],
6
6
  "blackList": [],
7
7
  "useInterval": true,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "title": "Matterbridge Roborock Vacuum Plugin",
3
- "description": "matterbridge-roborock-vacuum-plugin v. 1.0.7 by https://github.com/RinDevJunior",
3
+ "description": "matterbridge-roborock-vacuum-plugin v. 1.0.8-rc01 by https://github.com/RinDevJunior",
4
4
  "type": "object",
5
5
  "required": ["username", "password"],
6
6
  "properties": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge-roborock-vacuum-plugin",
3
- "version": "1.0.7",
3
+ "version": "1.0.8-rc01",
4
4
  "description": "Matterbridge Roborock Vacuum Plugin",
5
5
  "author": "https://github.com/RinDevJunior",
6
6
  "license": "MIT",
@@ -25,7 +25,7 @@ export function configurateBehavior(
25
25
 
26
26
  case DeviceModel.S7_MAXV: {
27
27
  const deviceHandler = new BehaviorDeviceGeneric<EndpointCommandsA27>(logger);
28
- setCommandHandlerA27(duid, deviceHandler, logger, roborockService);
28
+ setCommandHandlerA27(duid, deviceHandler, logger, roborockService, cleanModeSettings);
29
29
  return deviceHandler;
30
30
  }
31
31
 
@@ -71,9 +71,9 @@ const RvcCleanMode: Record<number, string> = {
71
71
 
72
72
  const CleanSetting: Record<number, { suctionPower: number; waterFlow: number; distance_off: number; mopRoute: number }> = {
73
73
  [4]: { suctionPower: 0, waterFlow: 0, distance_off: 0, mopRoute: MopRouteA187.Smart }, //'Smart Plan'
74
- [5]: { suctionPower: VacuumSuctionPowerA187.Off, waterFlow: MopWaterFlowA187.Medium, distance_off: 0, mopRoute: MopRouteA187.Custom }, //'Mop'
75
- [6]: { suctionPower: VacuumSuctionPowerA187.Balanced, waterFlow: MopWaterFlowA187.Off, distance_off: 0, mopRoute: MopRouteA187.Custom }, //'Vacuum'
76
- [7]: { suctionPower: VacuumSuctionPowerA187.Balanced, waterFlow: MopWaterFlowA187.Medium, distance_off: 0, mopRoute: MopRouteA187.Custom }, //'Vac & Mop'
74
+ [5]: { suctionPower: VacuumSuctionPowerA187.Off, waterFlow: MopWaterFlowA187.Medium, distance_off: 0, mopRoute: MopRouteA187.Standard }, //'Mop'
75
+ [6]: { suctionPower: VacuumSuctionPowerA187.Balanced, waterFlow: MopWaterFlowA187.Off, distance_off: 0, mopRoute: MopRouteA187.Standard }, //'Vacuum'
76
+ [7]: { suctionPower: VacuumSuctionPowerA187.Balanced, waterFlow: MopWaterFlowA187.Medium, distance_off: 0, mopRoute: MopRouteA187.Standard }, //'Vac & Mop'
77
77
  [8]: { suctionPower: VacuumSuctionPowerA187.Custom, waterFlow: MopWaterFlowA187.Custom, distance_off: 0, mopRoute: MopRouteA187.Custom }, // 'Custom'
78
78
  };
79
79
 
@@ -0,0 +1,79 @@
1
+ import { getCurrentCleanModeA187, getCurrentCleanModeFromFanPowerA187, getCurrentCleanModeFromWaterBoxModeA187 } from './runtimes';
2
+ import { MopWaterFlowA187, VacuumSuctionPowerA187 } from './a187';
3
+
4
+ describe('getCurrentCleanModeA187', () => {
5
+ it('returns undefined if fan_power or water_box_mode is undefined', () => {
6
+ expect(getCurrentCleanModeA187(undefined, 1)).toBeUndefined();
7
+ expect(getCurrentCleanModeA187(1, undefined)).toBeUndefined();
8
+ });
9
+
10
+ it('returns undefined if fan_power = 105 or water_box_mode = 202', () => {
11
+ expect(getCurrentCleanModeA187(105, 202)).toBe(5);
12
+ });
13
+
14
+ it('returns 4 for Smart plan', () => {
15
+ expect(getCurrentCleanModeA187(VacuumSuctionPowerA187.Smart, 1)).toBe(4);
16
+ expect(getCurrentCleanModeA187(1, MopWaterFlowA187.Smart)).toBe(4);
17
+ });
18
+
19
+ it('returns 8 for Custom', () => {
20
+ expect(getCurrentCleanModeA187(VacuumSuctionPowerA187.Custom, 1)).toBe(8);
21
+ expect(getCurrentCleanModeA187(1, MopWaterFlowA187.Custom)).toBe(8);
22
+ });
23
+
24
+ it('returns 5 for Mop', () => {
25
+ expect(getCurrentCleanModeA187(VacuumSuctionPowerA187.Off, 1)).toBe(5);
26
+ });
27
+
28
+ it('returns 6 for Vacuum', () => {
29
+ expect(getCurrentCleanModeA187(1, MopWaterFlowA187.Off)).toBe(6);
30
+ });
31
+
32
+ it('returns 7 for Vac & Mop', () => {
33
+ expect(getCurrentCleanModeA187(1, 1)).toBe(7);
34
+ });
35
+ });
36
+
37
+ describe('getCurrentCleanModeFromFanPowerA187', () => {
38
+ it('returns undefined if fan_power is undefined', () => {
39
+ expect(getCurrentCleanModeFromFanPowerA187(undefined)).toBeUndefined();
40
+ });
41
+
42
+ it('returns 4 for Smart plan', () => {
43
+ expect(getCurrentCleanModeFromFanPowerA187(VacuumSuctionPowerA187.Smart)).toBe(4);
44
+ });
45
+
46
+ it('returns 8 for Custom', () => {
47
+ expect(getCurrentCleanModeFromFanPowerA187(VacuumSuctionPowerA187.Custom)).toBe(8);
48
+ });
49
+
50
+ it('returns 5 for Mop', () => {
51
+ expect(getCurrentCleanModeFromFanPowerA187(VacuumSuctionPowerA187.Off)).toBe(5);
52
+ });
53
+
54
+ it('returns undefined for other values', () => {
55
+ expect(getCurrentCleanModeFromFanPowerA187(12345)).toBeUndefined();
56
+ });
57
+ });
58
+
59
+ describe('getCurrentCleanModeFromWaterBoxModeA187', () => {
60
+ it('returns undefined if water_box_mode is undefined', () => {
61
+ expect(getCurrentCleanModeFromWaterBoxModeA187(undefined)).toBeUndefined();
62
+ });
63
+
64
+ it('returns 4 for Smart plan', () => {
65
+ expect(getCurrentCleanModeFromWaterBoxModeA187(MopWaterFlowA187.Smart)).toBe(4);
66
+ });
67
+
68
+ it('returns 8 for Custom', () => {
69
+ expect(getCurrentCleanModeFromWaterBoxModeA187(MopWaterFlowA187.Custom)).toBe(8);
70
+ });
71
+
72
+ it('returns 6 for Vacuum', () => {
73
+ expect(getCurrentCleanModeFromWaterBoxModeA187(MopWaterFlowA187.Off)).toBe(6);
74
+ });
75
+
76
+ it('returns undefined for other values', () => {
77
+ expect(getCurrentCleanModeFromWaterBoxModeA187(12345)).toBeUndefined();
78
+ });
79
+ });
@@ -0,0 +1,134 @@
1
+ import { setCommandHandlerA27, VacuumSuctionPowerA27, MopWaterFlowA27, MopRouteA27 } from './a27';
2
+ import { BehaviorDeviceGeneric } from '../../BehaviorDeviceGeneric';
3
+ import { AnsiLogger } from 'matterbridge/logger';
4
+ import RoborockService from '../../../roborockService';
5
+ import { CleanModeSettings } from '../../../model/CleanModeSettings';
6
+ import { jest } from '@jest/globals';
7
+
8
+ describe('setCommandHandlerA27', () => {
9
+ let handler: BehaviorDeviceGeneric<any>;
10
+ let logger: AnsiLogger;
11
+ let roborockService: jest.Mocked<RoborockService>;
12
+ let cleanModeSettings: CleanModeSettings;
13
+ const duid = 'test-duid';
14
+
15
+ beforeEach(() => {
16
+ handler = {
17
+ setCommandHandler: jest.fn(),
18
+ } as any;
19
+
20
+ logger = {
21
+ notice: jest.fn(),
22
+ warn: jest.fn(),
23
+ } as any;
24
+
25
+ roborockService = {
26
+ startClean: jest.fn(),
27
+ changeCleanMode: jest.fn(),
28
+ setSelectedAreas: jest.fn(),
29
+ pauseClean: jest.fn(),
30
+ resumeClean: jest.fn(),
31
+ stopAndGoHome: jest.fn(),
32
+ playSoundToLocate: jest.fn(),
33
+ } as any;
34
+
35
+ cleanModeSettings = {
36
+ vacuuming: { fanMode: 'Max', mopRouteMode: 'DeepPlus' },
37
+ mopping: { waterFlowMode: 'High', mopRouteMode: 'Fast', distanceOff: 85 },
38
+ vacmop: {
39
+ fanMode: 'Turbo',
40
+ waterFlowMode: 'Low',
41
+ mopRouteMode: 'Deep',
42
+ distanceOff: 85,
43
+ },
44
+ };
45
+ });
46
+
47
+ it('should set all command handlers', () => {
48
+ setCommandHandlerA27(duid, handler, logger, roborockService, cleanModeSettings);
49
+ expect(handler.setCommandHandler).toHaveBeenCalledWith('changeToMode', expect.any(Function));
50
+ expect(handler.setCommandHandler).toHaveBeenCalledWith('selectAreas', expect.any(Function));
51
+ expect(handler.setCommandHandler).toHaveBeenCalledWith('pause', expect.any(Function));
52
+ expect(handler.setCommandHandler).toHaveBeenCalledWith('resume', expect.any(Function));
53
+ expect(handler.setCommandHandler).toHaveBeenCalledWith('goHome', expect.any(Function));
54
+ expect(handler.setCommandHandler).toHaveBeenCalledWith('PlaySoundToLocate', expect.any(Function));
55
+ });
56
+
57
+ it('should call startClean for Cleaning mode', async () => {
58
+ setCommandHandlerA27(duid, handler, logger, roborockService, cleanModeSettings);
59
+ const [[, changeToModeHandler]] = (handler.setCommandHandler as jest.Mock).mock.calls.filter(([cmd]) => cmd === 'changeToMode');
60
+ await (changeToModeHandler as (mode: number) => Promise<void>)(2); // 2 = Cleaning
61
+ expect(roborockService.startClean).toHaveBeenCalledWith(duid);
62
+ });
63
+
64
+ it('should call changeCleanMode for Mop with correct values', async () => {
65
+ setCommandHandlerA27(duid, handler, logger, roborockService, cleanModeSettings);
66
+ const [[, changeToModeHandler]] = (handler.setCommandHandler as jest.Mock).mock.calls.filter(([cmd]) => cmd === 'changeToMode');
67
+ await (changeToModeHandler as (mode: number) => Promise<void>)(5); // 5 = Mop
68
+ expect(roborockService.changeCleanMode).toHaveBeenCalledWith(duid, {
69
+ suctionPower: VacuumSuctionPowerA27.Off,
70
+ waterFlow: MopWaterFlowA27.High,
71
+ mopRoute: MopRouteA27.Fast,
72
+ distance_off: 0,
73
+ });
74
+ });
75
+
76
+ it('should call changeCleanMode for Vacuum with correct values', async () => {
77
+ setCommandHandlerA27(duid, handler, logger, roborockService, cleanModeSettings);
78
+ const [[, changeToModeHandler]] = (handler.setCommandHandler as jest.Mock).mock.calls.filter(([cmd]) => cmd === 'changeToMode');
79
+ await (changeToModeHandler as (mode: number) => Promise<void>)(6); // 6 = Vacuum
80
+ expect(roborockService.changeCleanMode).toHaveBeenCalledWith(duid, {
81
+ suctionPower: VacuumSuctionPowerA27.Max,
82
+ waterFlow: MopWaterFlowA27.Off,
83
+ mopRoute: MopRouteA27.DeepPlus,
84
+ distance_off: 0,
85
+ });
86
+ });
87
+
88
+ it('should call changeCleanMode for Vac & Mop with correct values', async () => {
89
+ setCommandHandlerA27(duid, handler, logger, roborockService, cleanModeSettings);
90
+ const [[, changeToModeHandler]] = (handler.setCommandHandler as jest.Mock).mock.calls.filter(([cmd]) => cmd === 'changeToMode');
91
+ await (changeToModeHandler as (mode: number) => Promise<void>)(7); // 7 = Vac & Mop
92
+ expect(roborockService.changeCleanMode).toHaveBeenCalledWith(duid, {
93
+ suctionPower: VacuumSuctionPowerA27.Turbo,
94
+ waterFlow: MopWaterFlowA27.Low,
95
+ mopRoute: MopRouteA27.Deep,
96
+ distance_off: 0,
97
+ });
98
+ });
99
+
100
+ it('should call setSelectedAreas', async () => {
101
+ setCommandHandlerA27(duid, handler, logger, roborockService, cleanModeSettings);
102
+ const [[, selectAreasHandler]] = (handler.setCommandHandler as jest.Mock).mock.calls.filter(([cmd]) => cmd === 'selectAreas');
103
+ await (selectAreasHandler as (areas: number[]) => Promise<void>)([1, 2, 3]);
104
+ expect(roborockService.setSelectedAreas).toHaveBeenCalledWith(duid, [1, 2, 3]);
105
+ });
106
+
107
+ it('should call pauseClean', async () => {
108
+ setCommandHandlerA27(duid, handler, logger, roborockService, cleanModeSettings);
109
+ const [[, pauseHandler]] = (handler.setCommandHandler as jest.Mock).mock.calls.filter(([cmd]) => cmd === 'pause');
110
+ await (pauseHandler as () => Promise<void>)();
111
+ expect(roborockService.pauseClean).toHaveBeenCalledWith(duid);
112
+ });
113
+
114
+ it('should call resumeClean', async () => {
115
+ setCommandHandlerA27(duid, handler, logger, roborockService, cleanModeSettings);
116
+ const [[, resumeHandler]] = (handler.setCommandHandler as jest.Mock).mock.calls.filter(([cmd]) => cmd === 'resume');
117
+ await (resumeHandler as () => Promise<void>)();
118
+ expect(roborockService.resumeClean).toHaveBeenCalledWith(duid);
119
+ });
120
+
121
+ it('should call stopAndGoHome', async () => {
122
+ setCommandHandlerA27(duid, handler, logger, roborockService, cleanModeSettings);
123
+ const [[, goHomeHandler]] = (handler.setCommandHandler as jest.Mock).mock.calls.filter(([cmd]) => cmd === 'goHome');
124
+ await (goHomeHandler as () => Promise<void>)();
125
+ expect(roborockService.stopAndGoHome).toHaveBeenCalledWith(duid);
126
+ });
127
+
128
+ it('should call playSoundToLocate', async () => {
129
+ setCommandHandlerA27(duid, handler, logger, roborockService, cleanModeSettings);
130
+ const [[, playSoundHandler]] = (handler.setCommandHandler as jest.Mock).mock.calls.filter(([cmd]) => cmd === 'PlaySoundToLocate');
131
+ await (playSoundHandler as (arg: number) => Promise<void>)(1);
132
+ expect(roborockService.playSoundToLocate).toHaveBeenCalledWith(duid);
133
+ });
134
+ });
@@ -2,6 +2,7 @@ import { MaybePromise } from 'matterbridge/matter';
2
2
  import { AnsiLogger, debugStringify } from 'matterbridge/logger';
3
3
  import { BehaviorDeviceGeneric, BehaviorRoborock, DeviceCommands } from '../../BehaviorDeviceGeneric.js';
4
4
  import RoborockService from '../../../roborockService.js';
5
+ import { CleanModeSettings } from '../../../model/CleanModeSettings.js';
5
6
 
6
7
  export interface EndpointCommandsA27 extends DeviceCommands {
7
8
  selectAreas: (newAreas: any) => MaybePromise;
@@ -63,14 +64,20 @@ const RvcCleanMode: Record<number, string> = {
63
64
  [8]: 'Custom',
64
65
  };
65
66
 
66
- const CleanSetting: Record<number, { suctionPower: number; waterFlow: number; mopRoute: number }> = {
67
- [5]: { suctionPower: VacuumSuctionPowerA27.Off, waterFlow: MopWaterFlowA27.Medium, mopRoute: MopRouteA27.Custom }, //'Mop'
68
- [6]: { suctionPower: VacuumSuctionPowerA27.Balanced, waterFlow: MopWaterFlowA27.Off, mopRoute: MopRouteA27.Custom }, //'Vacuum'
69
- [7]: { suctionPower: VacuumSuctionPowerA27.Balanced, waterFlow: MopWaterFlowA27.Medium, mopRoute: MopRouteA27.Custom }, //'Vac & Mop'
70
- [8]: { suctionPower: VacuumSuctionPowerA27.Custom, waterFlow: MopWaterFlowA27.Custom, mopRoute: MopRouteA27.Custom }, // 'Custom'
67
+ const CleanSetting: Record<number, { suctionPower: number; waterFlow: number; distance_off: number; mopRoute: number }> = {
68
+ [5]: { suctionPower: VacuumSuctionPowerA27.Off, waterFlow: MopWaterFlowA27.Medium, distance_off: 0, mopRoute: MopRouteA27.Standard }, //'Mop'
69
+ [6]: { suctionPower: VacuumSuctionPowerA27.Balanced, waterFlow: MopWaterFlowA27.Off, distance_off: 0, mopRoute: MopRouteA27.Standard }, //'Vacuum'
70
+ [7]: { suctionPower: VacuumSuctionPowerA27.Balanced, waterFlow: MopWaterFlowA27.Medium, distance_off: 0, mopRoute: MopRouteA27.Standard }, //'Vac & Mop'
71
+ [8]: { suctionPower: VacuumSuctionPowerA27.Custom, waterFlow: MopWaterFlowA27.Custom, distance_off: 0, mopRoute: MopRouteA27.Custom }, // 'Custom'
71
72
  };
72
73
 
73
- export function setCommandHandlerA27(duid: string, handler: BehaviorDeviceGeneric<DeviceCommands>, logger: AnsiLogger, roborockService: RoborockService): void {
74
+ export function setCommandHandlerA27(
75
+ duid: string,
76
+ handler: BehaviorDeviceGeneric<DeviceCommands>,
77
+ logger: AnsiLogger,
78
+ roborockService: RoborockService,
79
+ cleanModeSettings: CleanModeSettings | undefined,
80
+ ): void {
74
81
  handler.setCommandHandler('changeToMode', async (newMode: number) => {
75
82
  const activity = RvcRunMode[newMode] || RvcCleanMode[newMode];
76
83
  switch (activity) {
@@ -81,11 +88,18 @@ export function setCommandHandlerA27(duid: string, handler: BehaviorDeviceGeneri
81
88
  }
82
89
  case 'Mop':
83
90
  case 'Vacuum':
84
- case 'Vac & Mop':
91
+ case 'Vac & Mop': {
92
+ const setting = cleanModeSettings ? getSettingFromCleanMode(activity, cleanModeSettings) : CleanSetting[newMode];
93
+ logger.notice(`BehaviorA27-ChangeCleanMode to: ${activity}, setting: ${debugStringify(setting ?? {})}`);
94
+ if (setting) {
95
+ await roborockService.changeCleanMode(duid, setting);
96
+ }
97
+ return;
98
+ }
85
99
  case 'Custom': {
86
100
  const setting = CleanSetting[newMode];
87
- logger.notice(`BehaviorA27-ChangeCleanMode to: ${activity}, code: ${debugStringify(setting)}`);
88
- //await roborockService.changeCleanMode(duid, setting);
101
+ logger.notice(`BehaviorA27-ChangeCleanMode to: ${activity}, setting: ${debugStringify(setting)}`);
102
+ await roborockService.changeCleanMode(duid, setting);
89
103
  return;
90
104
  }
91
105
  default:
@@ -94,6 +108,45 @@ export function setCommandHandlerA27(duid: string, handler: BehaviorDeviceGeneri
94
108
  }
95
109
  });
96
110
 
111
+ const getSettingFromCleanMode = (
112
+ activity: string,
113
+ cleanModeSettings?: CleanModeSettings,
114
+ ): { suctionPower: number; waterFlow: number; distance_off: number; mopRoute: number } | undefined => {
115
+ switch (activity) {
116
+ case 'Mop': {
117
+ const mopSetting = cleanModeSettings?.mopping;
118
+ const waterFlow = MopWaterFlowA27[mopSetting?.waterFlowMode as keyof typeof MopWaterFlowA27] ?? MopWaterFlowA27.Medium;
119
+ return {
120
+ suctionPower: VacuumSuctionPowerA27.Off,
121
+ waterFlow,
122
+ distance_off: 0,
123
+ mopRoute: MopRouteA27[mopSetting?.mopRouteMode as keyof typeof MopRouteA27] ?? MopRouteA27.Standard,
124
+ };
125
+ }
126
+ case 'Vacuum': {
127
+ const vacuumSetting = cleanModeSettings?.vacuuming;
128
+ return {
129
+ suctionPower: VacuumSuctionPowerA27[vacuumSetting?.fanMode as keyof typeof VacuumSuctionPowerA27] ?? VacuumSuctionPowerA27.Balanced,
130
+ waterFlow: MopWaterFlowA27.Off,
131
+ distance_off: 0,
132
+ mopRoute: MopRouteA27[vacuumSetting?.mopRouteMode as keyof typeof MopRouteA27] ?? MopRouteA27.Standard,
133
+ };
134
+ }
135
+ case 'Vac & Mop': {
136
+ const vacmopSetting = cleanModeSettings?.vacmop;
137
+ const waterFlow = MopWaterFlowA27[vacmopSetting?.waterFlowMode as keyof typeof MopWaterFlowA27] ?? MopWaterFlowA27.Medium;
138
+ return {
139
+ suctionPower: VacuumSuctionPowerA27[vacmopSetting?.fanMode as keyof typeof VacuumSuctionPowerA27] ?? VacuumSuctionPowerA27.Balanced,
140
+ waterFlow,
141
+ distance_off: 0,
142
+ mopRoute: MopRouteA27[vacmopSetting?.mopRouteMode as keyof typeof MopRouteA27] ?? MopRouteA27.Standard,
143
+ };
144
+ }
145
+ default:
146
+ return undefined;
147
+ }
148
+ };
149
+
97
150
  handler.setCommandHandler('selectAreas', async (newAreas: number[]) => {
98
151
  logger.notice(`BehaviorA27-selectAreas: ${newAreas}`);
99
152
  roborockService.setSelectedAreas(duid, newAreas ?? []);
@@ -24,8 +24,8 @@ export abstract class AbstractClient implements Client {
24
24
 
25
25
  protected constructor(logger: AnsiLogger, context: MessageContext) {
26
26
  this.context = context;
27
- this.serializer = new MessageSerializer(this.context);
28
- this.deserializer = new MessageDeserializer(this.context);
27
+ this.serializer = new MessageSerializer(this.context, logger);
28
+ this.deserializer = new MessageDeserializer(this.context, logger);
29
29
 
30
30
  this.syncMessageListener = new SyncMessageListener(logger);
31
31
  this.messageListeners.register(this.syncMessageListener);
@@ -107,31 +107,32 @@ export class LocalNetworkClient extends AbstractClient {
107
107
  try {
108
108
  this.buffer.append(message);
109
109
 
110
- const recvBuffer = this.buffer.get();
111
- if (!this.isMessageComplete(recvBuffer)) {
110
+ const receivedBuffer = this.buffer.get();
111
+ if (!this.isMessageComplete(receivedBuffer)) {
112
112
  return;
113
113
  }
114
114
  this.buffer.reset();
115
115
 
116
116
  let offset = 0;
117
- while (offset + 4 <= recvBuffer.length) {
118
- const segmentLength = recvBuffer.readUInt32BE(offset);
117
+ while (offset + 4 <= receivedBuffer.length) {
118
+ const segmentLength = receivedBuffer.readUInt32BE(offset);
119
119
  if (segmentLength == 17) {
120
120
  offset += 4 + segmentLength;
121
121
  continue;
122
122
  }
123
123
 
124
124
  try {
125
- const currentBuffer = recvBuffer.subarray(offset + 4, offset + segmentLength + 4);
125
+ const currentBuffer = receivedBuffer.subarray(offset + 4, offset + segmentLength + 4);
126
126
  const response = this.deserializer.deserialize(this.duid, currentBuffer);
127
127
  await this.messageListeners.onMessage(response);
128
128
  } catch (error) {
129
- this.logger.error('unable to process message: ' + error);
129
+ this.logger.error('LocalNetworkClient: unable to process message with error: ' + error);
130
+ //unable to process message: TypeError: Cannot read properties of undefined (reading 'length')
130
131
  }
131
132
  offset += 4 + segmentLength;
132
133
  }
133
134
  } catch (error) {
134
- this.logger.error('failed to read from a socket: ' + error);
135
+ this.logger.error('LocalNetworkClient: read socket buffer error: ' + error);
135
136
  }
136
137
  }
137
138
 
@@ -145,7 +146,7 @@ export class LocalNetworkClient extends AbstractClient {
145
146
  offset += 4 + segmentLength;
146
147
 
147
148
  if (offset > buffer.length) {
148
- return false; // Data is not complete yet
149
+ return false;
149
150
  }
150
151
  }
151
152
 
@@ -124,7 +124,7 @@ export class MQTTClient extends AbstractClient {
124
124
  const response = this.deserializer.deserialize(duid, message);
125
125
  await this.messageListeners.onMessage(response);
126
126
  } catch (error) {
127
- this.logger.error('unable to process message from queue ' + topic + ': ' + error);
127
+ this.logger.error('MQTTClient: unable to process message with error: ' + topic + ': ' + error);
128
128
  }
129
129
  }
130
130
  }
@@ -103,26 +103,32 @@ export class MessageProcessor {
103
103
  this.logger?.notice(`Change clean mode for ${duid} to suctionPower: ${suctionPower}, waterFlow: ${waterFlow}, mopRoute: ${mopRoute}, distance_off: ${distance_off}`);
104
104
 
105
105
  const currentMopMode = await this.getCustomMessage(duid, new RequestMessage({ method: 'get_custom_mode' }));
106
+ const smartMopMode = 110;
107
+ const smartMopRoute = 306;
108
+ const customMopMode = 106;
109
+ const customMopRoute = 302;
106
110
 
107
- //110 == AI/Smart Plan
108
- //302 == Custom
109
- if (currentMopMode == 110) {
110
- await this.client.send(duid, new RequestMessage({ method: 'set_mop_mode', params: [302] }));
111
- }
111
+ if (currentMopMode == smartMopMode && mopRoute == smartMopRoute) return;
112
+ if (currentMopMode == customMopMode && mopRoute == customMopRoute) return;
112
113
 
113
- if (mopRoute && mopRoute != 0) {
114
- await this.client.send(duid, new RequestMessage({ method: 'set_mop_mode', params: [mopRoute] }));
114
+ //if change mode from smart plan, firstly change to custom
115
+ if (currentMopMode == smartMopMode) {
116
+ await this.client.send(duid, new RequestMessage({ method: 'set_mop_mode', params: [customMopRoute] }));
115
117
  }
116
118
 
117
119
  if (suctionPower && suctionPower != 0) {
118
120
  await this.client.send(duid, new RequestMessage({ method: 'set_custom_mode', params: [suctionPower] }));
119
121
  }
120
122
 
121
- //207 == CustomizeWithDistanceOff
122
- if (waterFlow && waterFlow == 207 && distance_off && distance_off != 0) {
123
+ const CustomizeWithDistanceOff = 207;
124
+ if (waterFlow && waterFlow == CustomizeWithDistanceOff && distance_off && distance_off != 0) {
123
125
  await this.client.send(duid, new RequestMessage({ method: 'set_water_box_custom_mode', params: { 'water_box_mode': waterFlow, 'distance_off': distance_off } }));
124
126
  } else if (waterFlow && waterFlow != 0) {
125
127
  await this.client.send(duid, new RequestMessage({ method: 'set_water_box_custom_mode', params: [waterFlow] }));
126
128
  }
129
+
130
+ if (mopRoute && mopRoute != 0) {
131
+ await this.client.send(duid, new RequestMessage({ method: 'set_mop_mode', params: [mopRoute] }));
132
+ }
127
133
  }
128
134
  }
@@ -7,6 +7,7 @@ import { ResponseMessage } from '../broadcast/model/responseMessage.js';
7
7
  import { CryptoUtils, MessageUtils } from './cryptoHelper.js';
8
8
  import { Protocol } from '../broadcast/model/protocol.js';
9
9
  import { MessageContext } from '../broadcast/model/messageContext.js';
10
+ import { AnsiLogger } from 'matterbridge/logger';
10
11
 
11
12
  export interface Message {
12
13
  version: string;
@@ -14,19 +15,21 @@ export interface Message {
14
15
  random: number;
15
16
  timestamp: number;
16
17
  protocol: number;
17
- payloadLen: number;
18
+ payloadLen?: number;
18
19
  payload: Buffer<ArrayBufferLike>;
19
20
  crc32: number;
20
21
  }
21
22
 
22
23
  export class MessageDeserializer {
23
24
  private readonly context: MessageContext;
24
- private readonly mqttMessageParser: Parser;
25
+ private readonly messageParser: Parser;
26
+ private readonly logger: AnsiLogger;
25
27
 
26
- constructor(context: MessageContext) {
28
+ constructor(context: MessageContext, logger: AnsiLogger) {
27
29
  this.context = context;
30
+ this.logger = logger;
28
31
 
29
- this.mqttMessageParser = new Parser()
32
+ this.messageParser = new Parser()
30
33
  .endianess('big')
31
34
  .string('version', {
32
35
  length: 3,
@@ -42,22 +45,20 @@ export class MessageDeserializer {
42
45
  .uint32('crc32');
43
46
  }
44
47
 
45
- deserialize(duid: string, message: Buffer<ArrayBufferLike>): ResponseMessage {
48
+ public deserialize(duid: string, message: Buffer<ArrayBufferLike>): ResponseMessage {
46
49
  const version = message.toString('latin1', 0, 3);
47
50
  if (version !== '1.0' && version !== 'A01') {
48
51
  throw new Error('unknown protocol version ' + version);
49
52
  }
50
53
 
51
54
  const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0;
52
- const expectedCrc32 = message.readUint32BE(message.length - 4);
55
+ const expectedCrc32 = message.readUInt32BE(message.length - 4);
53
56
  if (crc32 != expectedCrc32) {
54
- //throw new Error(`Wrong CRC32 ${crc32}, expected ${expectedCrc32}`);
55
- //ignore the error for now
56
- return new ResponseMessage(duid, { dps: { id: 0, result: null } });
57
+ throw new Error(`Wrong CRC32 ${crc32}, expected ${expectedCrc32}`);
57
58
  }
58
59
  const localKey = this.context.getLocalKey(duid);
59
60
  assert(localKey, 'unable to retrieve local key for ' + duid);
60
- const data: Message = this.mqttMessageParser.parse(message);
61
+ const data: Message = this.messageParser.parse(message);
61
62
 
62
63
  if (version == '1.0') {
63
64
  const aesKey = CryptoUtils.md5bin(MessageUtils.encodeTimestamp(data.timestamp) + localKey + MessageUtils.SALT);
@@ -77,7 +78,7 @@ export class MessageDeserializer {
77
78
  if (data.protocol == Protocol.rpc_response || data.protocol == Protocol.general_request) {
78
79
  return this.deserializeProtocolRpcResponse(duid, data);
79
80
  } else {
80
- //throw new Error('unknown protocol: ' + data.protocol);
81
+ this.logger.error('unknown protocol: ' + data.protocol);
81
82
  return new ResponseMessage(duid, { dps: { id: 0, result: null } });
82
83
  }
83
84
  }
@@ -4,14 +4,17 @@ import crypto from 'crypto';
4
4
  import CRC32 from 'crc-32';
5
5
  import { DpsPayload, Payload } from '../broadcast/model/dps.js';
6
6
  import { MessageContext } from '../broadcast/model/messageContext.js';
7
+ import { AnsiLogger } from 'matterbridge/logger';
7
8
 
8
9
  export class MessageSerializer {
9
10
  private readonly context: MessageContext;
11
+ private readonly logger: AnsiLogger;
10
12
  private seq = 1;
11
13
  private random = 4711;
12
14
 
13
- constructor(context: MessageContext) {
15
+ constructor(context: MessageContext, logger: AnsiLogger) {
14
16
  this.context = context;
17
+ this.logger = logger;
15
18
  }
16
19
 
17
20
  serialize(duid: string, request: RequestMessage): { messageId: number; buffer: Buffer<ArrayBufferLike> } {
@@ -68,12 +68,6 @@ export function state_to_matter_operational_status(state: number | undefined): R
68
68
  case OperationStatusCode.ZoneCleanMopCleaning:
69
69
  case OperationStatusCode.ZoneCleanMopMopping:
70
70
  case OperationStatusCode.RobotStatusMopping:
71
- case OperationStatusCode.WashingTheMop:
72
- case OperationStatusCode.WashingTheMop2:
73
- case OperationStatusCode.ReturnToDock:
74
- case OperationStatusCode.GoingToWashTheMop:
75
- case OperationStatusCode.BackToDockWashingDuster:
76
- case OperationStatusCode.EmptyingDustContainer:
77
71
  return RvcOperationalState.OperationalState.Running;
78
72
 
79
73
  case OperationStatusCode.InError:
@@ -91,7 +85,13 @@ export function state_to_matter_operational_status(state: number | undefined): R
91
85
  case OperationStatusCode.Locked:
92
86
  return RvcOperationalState.OperationalState.Stopped;
93
87
 
88
+ case OperationStatusCode.ReturnToDock:
94
89
  case OperationStatusCode.ReturningDock:
90
+ case OperationStatusCode.WashingTheMop:
91
+ case OperationStatusCode.WashingTheMop2:
92
+ case OperationStatusCode.GoingToWashTheMop:
93
+ case OperationStatusCode.BackToDockWashingDuster:
94
+ case OperationStatusCode.EmptyingDustContainer:
95
95
  return RvcOperationalState.OperationalState.SeekingCharger;
96
96
 
97
97
  case OperationStatusCode.Idle:
@@ -1,6 +0,0 @@
1
- import { MatterbridgeEndpoint, powerSource, } from "matterbridge";
2
- export class Appliances extends MatterbridgeEndpoint {
3
- constructor(deviceType, name, serial) {
4
- super([deviceType, powerSource], { uniqueStorageKey: `${name}-${serial}` }, true);
5
- }
6
- }
package/src/appliances.ts DELETED
@@ -1,15 +0,0 @@
1
- import {
2
- DeviceTypeDefinition,
3
- MatterbridgeEndpoint,
4
- powerSource,
5
- } from "matterbridge";
6
-
7
- export class Appliances extends MatterbridgeEndpoint {
8
- constructor(deviceType: DeviceTypeDefinition, name: string, serial: string) {
9
- super(
10
- [deviceType, powerSource],
11
- { uniqueStorageKey: `${name}-${serial}` },
12
- true,
13
- );
14
- }
15
- }