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.
- package/dist/behaviorFactory.js +1 -1
- package/dist/behaviors/roborock.vacuum/QREVO_EDGE_5V1/a187.js +3 -3
- package/dist/behaviors/roborock.vacuum/S7_MAXV/a27.js +50 -7
- package/dist/roborockCommunication/broadcast/abstractClient.js +2 -2
- package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +7 -7
- package/dist/roborockCommunication/broadcast/client/MQTTClient.js +1 -1
- package/dist/roborockCommunication/broadcast/messageProcessor.js +15 -6
- package/dist/roborockCommunication/helper/messageDeserializer.js +9 -6
- package/dist/roborockCommunication/helper/messageSerializer.js +3 -1
- package/dist/share/function.js +6 -6
- package/matterbridge-roborock-vacuum-plugin.config.json +1 -1
- package/matterbridge-roborock-vacuum-plugin.schema.json +1 -1
- package/package.json +1 -1
- package/src/behaviorFactory.ts +1 -1
- package/src/behaviors/roborock.vacuum/QREVO_EDGE_5V1/a187.ts +3 -3
- package/src/behaviors/roborock.vacuum/QREVO_EDGE_5V1/runtimes.test.ts +79 -0
- package/src/behaviors/roborock.vacuum/S7_MAXV/a27.test.ts +134 -0
- package/src/behaviors/roborock.vacuum/S7_MAXV/a27.ts +62 -9
- package/src/roborockCommunication/broadcast/abstractClient.ts +2 -2
- package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +9 -8
- package/src/roborockCommunication/broadcast/client/MQTTClient.ts +1 -1
- package/src/roborockCommunication/broadcast/messageProcessor.ts +15 -9
- package/src/roborockCommunication/helper/messageDeserializer.ts +12 -11
- package/src/roborockCommunication/helper/messageSerializer.ts +4 -1
- package/src/share/function.ts +6 -6
- package/dist/appliances.js +0 -6
- package/src/appliances.ts +0 -15
package/dist/behaviorFactory.js
CHANGED
|
@@ -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.
|
|
57
|
-
[6]: { suctionPower: VacuumSuctionPowerA187.Balanced, waterFlow: MopWaterFlowA187.Off, distance_off: 0, mopRoute: MopRouteA187.
|
|
58
|
-
[7]: { suctionPower: VacuumSuctionPowerA187.Balanced, waterFlow: MopWaterFlowA187.Medium, distance_off: 0, mopRoute: MopRouteA187.
|
|
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.
|
|
51
|
-
[6]: { suctionPower: VacuumSuctionPowerA27.Balanced, waterFlow: MopWaterFlowA27.Off, mopRoute: MopRouteA27.
|
|
52
|
-
[7]: { suctionPower: VacuumSuctionPowerA27.Balanced, waterFlow: MopWaterFlowA27.Medium, mopRoute: MopRouteA27.
|
|
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},
|
|
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
|
|
91
|
-
if (!this.isMessageComplete(
|
|
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 <=
|
|
97
|
-
const segmentLength =
|
|
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 =
|
|
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('
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
10
|
+
messageParser;
|
|
11
|
+
logger;
|
|
12
|
+
constructor(context, logger) {
|
|
12
13
|
this.context = context;
|
|
13
|
-
this.
|
|
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.
|
|
36
|
+
const expectedCrc32 = message.readUInt32BE(message.length - 4);
|
|
35
37
|
if (crc32 != expectedCrc32) {
|
|
36
|
-
|
|
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.
|
|
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;
|
package/dist/share/function.js
CHANGED
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "Matterbridge Roborock Vacuum Plugin",
|
|
3
|
-
"description": "matterbridge-roborock-vacuum-plugin v. 1.0.
|
|
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
package/src/behaviorFactory.ts
CHANGED
|
@@ -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.
|
|
75
|
-
[6]: { suctionPower: VacuumSuctionPowerA187.Balanced, waterFlow: MopWaterFlowA187.Off, distance_off: 0, mopRoute: MopRouteA187.
|
|
76
|
-
[7]: { suctionPower: VacuumSuctionPowerA187.Balanced, waterFlow: MopWaterFlowA187.Medium, distance_off: 0, mopRoute: MopRouteA187.
|
|
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.
|
|
68
|
-
[6]: { suctionPower: VacuumSuctionPowerA27.Balanced, waterFlow: MopWaterFlowA27.Off, mopRoute: MopRouteA27.
|
|
69
|
-
[7]: { suctionPower: VacuumSuctionPowerA27.Balanced, waterFlow: MopWaterFlowA27.Medium, mopRoute: MopRouteA27.
|
|
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(
|
|
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},
|
|
88
|
-
|
|
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
|
|
111
|
-
if (!this.isMessageComplete(
|
|
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 <=
|
|
118
|
-
const segmentLength =
|
|
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 =
|
|
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('
|
|
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;
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
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
|
|
114
|
-
|
|
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
|
-
|
|
122
|
-
if (waterFlow && waterFlow ==
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
55
|
+
const expectedCrc32 = message.readUInt32BE(message.length - 4);
|
|
53
56
|
if (crc32 != expectedCrc32) {
|
|
54
|
-
|
|
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.
|
|
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
|
-
|
|
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> } {
|
package/src/share/function.ts
CHANGED
|
@@ -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:
|
package/dist/appliances.js
DELETED
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
|
-
}
|