matterbridge-roborock-vacuum-plugin 1.1.0-rc18 → 1.1.1-rc02
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/helper.js +2 -7
- package/dist/model/RoomMap.js +1 -1
- package/dist/model/roomIndexMap.js +8 -4
- package/dist/platform.js +4 -3
- package/dist/platformRunner.js +10 -268
- package/dist/roborockCommunication/broadcast/abstractClient.js +3 -3
- package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +1 -0
- package/dist/roborockCommunication/broadcast/client/LocalNetworkUDPClient.js +1 -1
- package/dist/roborockCommunication/broadcast/clientRouter.js +5 -2
- package/dist/roborockCommunication/broadcast/model/contentMessage.js +1 -0
- package/dist/roborockCommunication/broadcast/model/headerMessage.js +1 -0
- package/dist/roborockCommunication/broadcast/model/messageContext.js +18 -7
- package/dist/roborockCommunication/broadcast/model/requestMessage.js +5 -0
- package/dist/roborockCommunication/helper/messageDeserializer.js +31 -18
- package/dist/roborockCommunication/helper/messageSerializer.js +17 -11
- package/dist/roborockService.js +6 -1
- package/dist/runtimes/handleCloudMessage.js +110 -0
- package/dist/runtimes/handleHomeDataMessage.js +57 -0
- package/dist/runtimes/handleLocalMessage.js +169 -0
- package/dist/rvc.js +2 -1
- package/dist/tests/testData/mockData.js +359 -0
- package/matterbridge-roborock-vacuum-plugin.config.json +2 -2
- package/matterbridge-roborock-vacuum-plugin.schema.json +10 -37
- package/package.json +2 -3
- package/src/behaviors/roborock.vacuum/default/runtimes.ts +1 -1
- package/src/behaviors/roborock.vacuum/smart/runtimes.ts +1 -1
- package/src/helper.ts +2 -12
- package/src/initialData/getSupportedAreas.ts +1 -9
- package/src/model/RoomMap.ts +4 -30
- package/src/model/roomIndexMap.ts +10 -6
- package/src/platform.ts +6 -3
- package/src/platformRunner.ts +12 -350
- package/src/roborockCommunication/Zmodel/device.ts +13 -1
- package/src/roborockCommunication/Zmodel/messageResult.ts +28 -27
- package/src/roborockCommunication/Zmodel/userData.ts +2 -1
- package/src/roborockCommunication/broadcast/abstractClient.ts +3 -3
- package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +4 -2
- package/src/roborockCommunication/broadcast/client/LocalNetworkUDPClient.ts +3 -3
- package/src/roborockCommunication/broadcast/clientRouter.ts +6 -2
- package/src/roborockCommunication/broadcast/model/contentMessage.ts +5 -0
- package/src/roborockCommunication/broadcast/model/headerMessage.ts +7 -0
- package/src/roborockCommunication/broadcast/model/messageContext.ts +24 -11
- package/src/roborockCommunication/broadcast/model/requestMessage.ts +12 -5
- package/src/roborockCommunication/helper/messageDeserializer.ts +42 -31
- package/src/roborockCommunication/helper/messageSerializer.ts +19 -14
- package/src/roborockService.ts +8 -1
- package/src/runtimes/handleCloudMessage.ts +134 -0
- package/src/runtimes/handleHomeDataMessage.ts +67 -0
- package/src/runtimes/handleLocalMessage.ts +209 -0
- package/src/rvc.ts +2 -2
- package/src/share/runtimeHelper.ts +1 -1
- package/src/tests/helper.test.ts +59 -10
- package/src/tests/roborockCommunication/broadcast/client/LocalNetworkClient.test.ts +0 -19
- package/src/tests/roborockCommunication/broadcast/client/MQTTClient.test.ts +0 -32
- package/src/tests/roborockService.setSelectedAreas.test.ts +61 -0
- package/src/tests/runtimes/handleCloudMessage.test.ts +200 -0
- package/src/tests/runtimes/handleHomeDataMessage.test.ts +53 -0
- package/src/tests/runtimes/handleLocalMessage.test.ts +222 -0
- package/src/tests/testData/mockData.ts +370 -0
|
@@ -1,34 +1,47 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
1
|
+
import { randomBytes, randomInt } from 'node:crypto';
|
|
2
2
|
import * as CryptoUtils from '../../helper/cryptoHelper.js';
|
|
3
3
|
import { UserData } from '../../Zmodel/userData.js';
|
|
4
4
|
|
|
5
5
|
export class MessageContext {
|
|
6
6
|
private readonly endpoint: string;
|
|
7
|
-
readonly nonce:
|
|
8
|
-
|
|
7
|
+
private readonly devices = new Map<string, { localKey: string; protocolVersion: string; nonce: number | undefined }>();
|
|
8
|
+
public readonly nonce: number;
|
|
9
|
+
public readonly serializeNonce: Buffer;
|
|
9
10
|
|
|
10
11
|
constructor(userdata: UserData) {
|
|
11
12
|
this.endpoint = CryptoUtils.md5bin(userdata.rriot.k).subarray(8, 14).toString('base64');
|
|
12
|
-
this.nonce =
|
|
13
|
+
this.nonce = randomInt(1000, 1000000);
|
|
14
|
+
this.serializeNonce = randomBytes(16);
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
registerDevice(duid: string, localKey: string, pv: string) {
|
|
16
|
-
this.devices.set(duid, { localKey: localKey, protocolVersion: pv });
|
|
17
|
+
public registerDevice(duid: string, localKey: string, pv: string, nonce: number | undefined) {
|
|
18
|
+
this.devices.set(duid, { localKey: localKey, protocolVersion: pv, nonce });
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
public updateNonce(duid: string, nonce: number): void {
|
|
22
|
+
const device = this.devices.get(duid);
|
|
23
|
+
if (device) {
|
|
24
|
+
device.nonce = nonce;
|
|
25
|
+
}
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
public getSerializeNonceAsHex(): string {
|
|
29
|
+
return this.serializeNonce.toString('hex').toUpperCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public getLocalKey(duid: string): string | undefined {
|
|
24
33
|
return this.devices.get(duid)?.localKey;
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
getProtocolVersion(duid: string): string | undefined {
|
|
36
|
+
public getProtocolVersion(duid: string): string | undefined {
|
|
28
37
|
return this.devices.get(duid)?.protocolVersion;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
public getDeviceNonce(duid: string): number | undefined {
|
|
41
|
+
return this.devices.get(duid)?.nonce;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public getEndpoint(): string {
|
|
32
45
|
return this.endpoint;
|
|
33
46
|
}
|
|
34
47
|
}
|
|
@@ -7,14 +7,18 @@ export interface ProtocolRequest {
|
|
|
7
7
|
method?: string | undefined;
|
|
8
8
|
params?: unknown[] | Record<string, unknown> | undefined;
|
|
9
9
|
secure?: boolean;
|
|
10
|
+
nonce?: number;
|
|
11
|
+
timestamp?: number;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export class RequestMessage {
|
|
13
|
-
readonly messageId: number;
|
|
14
|
-
readonly protocol: Protocol;
|
|
15
|
-
readonly method: string | undefined;
|
|
16
|
-
readonly params: unknown[] | Record<string, unknown> | undefined;
|
|
17
|
-
readonly secure: boolean;
|
|
15
|
+
public readonly messageId: number;
|
|
16
|
+
public readonly protocol: Protocol;
|
|
17
|
+
public readonly method: string | undefined;
|
|
18
|
+
public readonly params: unknown[] | Record<string, unknown> | undefined;
|
|
19
|
+
public readonly secure: boolean;
|
|
20
|
+
public readonly timestamp: number;
|
|
21
|
+
public readonly nonce: number;
|
|
18
22
|
|
|
19
23
|
constructor(args: ProtocolRequest) {
|
|
20
24
|
this.messageId = args.messageId ?? randomInt(10000, 32767);
|
|
@@ -22,6 +26,8 @@ export class RequestMessage {
|
|
|
22
26
|
this.method = args.method;
|
|
23
27
|
this.params = args.params;
|
|
24
28
|
this.secure = args.secure ?? false;
|
|
29
|
+
this.nonce = args.nonce ?? randomInt(10000, 32767);
|
|
30
|
+
this.timestamp = args.timestamp ?? Math.floor(Date.now() / 1000);
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
toMqttRequest() {
|
|
@@ -36,6 +42,7 @@ export class RequestMessage {
|
|
|
36
42
|
method: this.method,
|
|
37
43
|
params: this.params,
|
|
38
44
|
secure: this.secure,
|
|
45
|
+
timestamp: this.timestamp,
|
|
39
46
|
});
|
|
40
47
|
} else {
|
|
41
48
|
return this;
|
|
@@ -1,26 +1,19 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import CRC32 from 'crc-32';
|
|
3
|
-
import { Parser } from 'binary-parser';
|
|
4
3
|
import { ResponseMessage } from '../broadcast/model/responseMessage.js';
|
|
5
4
|
import * as CryptoUtils from './cryptoHelper.js';
|
|
6
5
|
import { Protocol } from '../broadcast/model/protocol.js';
|
|
7
6
|
import { MessageContext } from '../broadcast/model/messageContext.js';
|
|
8
7
|
import { AnsiLogger } from 'matterbridge/logger';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
random: number;
|
|
14
|
-
timestamp: number;
|
|
15
|
-
protocol: number;
|
|
16
|
-
payloadLen?: number;
|
|
17
|
-
payload: Buffer<ArrayBufferLike>;
|
|
18
|
-
crc32: number;
|
|
19
|
-
}
|
|
8
|
+
import { Parser } from 'binary-parser/dist/binary_parser.js';
|
|
9
|
+
import { ContentMessage } from '../broadcast/model/contentMessage.js';
|
|
10
|
+
import { HeaderMessage } from '../broadcast/model/headerMessage.js';
|
|
11
|
+
import { DpsPayload } from '../broadcast/model/dps.js';
|
|
20
12
|
|
|
21
13
|
export class MessageDeserializer {
|
|
22
14
|
private readonly context: MessageContext;
|
|
23
|
-
private readonly
|
|
15
|
+
private readonly headerMessageParser: Parser;
|
|
16
|
+
private readonly contentMessageParser: Parser;
|
|
24
17
|
private readonly logger: AnsiLogger;
|
|
25
18
|
private readonly supportedVersions: string[] = ['1.0', 'A01', 'B01'];
|
|
26
19
|
|
|
@@ -28,15 +21,18 @@ export class MessageDeserializer {
|
|
|
28
21
|
this.context = context;
|
|
29
22
|
this.logger = logger;
|
|
30
23
|
|
|
31
|
-
this.
|
|
24
|
+
this.headerMessageParser = new Parser()
|
|
32
25
|
.endianness('big')
|
|
33
26
|
.string('version', {
|
|
34
27
|
length: 3,
|
|
35
28
|
})
|
|
36
29
|
.uint32('seq')
|
|
37
|
-
.uint32('
|
|
30
|
+
.uint32('nonce')
|
|
38
31
|
.uint32('timestamp')
|
|
39
|
-
.uint16('protocol')
|
|
32
|
+
.uint16('protocol');
|
|
33
|
+
|
|
34
|
+
this.contentMessageParser = new Parser()
|
|
35
|
+
.endianness('big')
|
|
40
36
|
.uint16('payloadLen')
|
|
41
37
|
.buffer('payload', {
|
|
42
38
|
length: 'payloadLen',
|
|
@@ -45,53 +41,68 @@ export class MessageDeserializer {
|
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
public deserialize(duid: string, message: Buffer<ArrayBufferLike>): ResponseMessage {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
// check message header
|
|
45
|
+
const header: HeaderMessage = this.headerMessageParser.parse(message);
|
|
46
|
+
if (!this.supportedVersions.includes(header.version)) {
|
|
47
|
+
throw new Error('unknown protocol version ' + header.version);
|
|
51
48
|
}
|
|
52
49
|
|
|
50
|
+
if (header.protocol === Protocol.hello_response || header.protocol === Protocol.ping_response) {
|
|
51
|
+
const dpsValue: DpsPayload = {
|
|
52
|
+
id: header.seq,
|
|
53
|
+
result: {
|
|
54
|
+
version: header.version,
|
|
55
|
+
nonce: header.nonce,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
return new ResponseMessage(duid, { [header.protocol.toString()]: dpsValue });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// parse message content
|
|
62
|
+
const data: ContentMessage = this.contentMessageParser.parse(message.subarray(this.headerMessageParser.sizeOf()));
|
|
63
|
+
|
|
64
|
+
// check crc32
|
|
53
65
|
const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0;
|
|
54
66
|
const expectedCrc32 = message.readUInt32BE(message.length - 4);
|
|
55
67
|
if (crc32 != expectedCrc32) {
|
|
56
68
|
throw new Error(`Wrong CRC32 ${crc32}, expected ${expectedCrc32}`);
|
|
57
69
|
}
|
|
70
|
+
|
|
58
71
|
const localKey = this.context.getLocalKey(duid);
|
|
59
72
|
if (!localKey) {
|
|
60
73
|
this.logger.notice(`Unable to retrieve local key for ${duid}, it should be from other vacuums`);
|
|
61
74
|
return new ResponseMessage(duid, { dps: { id: 0, result: null } });
|
|
62
75
|
}
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (version == '1.0') {
|
|
67
|
-
const aesKey = CryptoUtils.md5bin(CryptoUtils.encodeTimestamp(data.timestamp) + localKey + CryptoUtils.SALT);
|
|
77
|
+
if (header.version == '1.0') {
|
|
78
|
+
const aesKey = CryptoUtils.md5bin(CryptoUtils.encodeTimestamp(header.timestamp) + localKey + CryptoUtils.SALT);
|
|
68
79
|
const decipher = crypto.createDecipheriv('aes-128-ecb', aesKey, null);
|
|
69
80
|
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
70
|
-
} else if (version == 'A01') {
|
|
71
|
-
const iv = CryptoUtils.md5hex(
|
|
81
|
+
} else if (header.version == 'A01') {
|
|
82
|
+
const iv = CryptoUtils.md5hex(header.nonce.toString(16).padStart(8, '0') + '726f626f726f636b2d67a6d6da').substring(8, 24);
|
|
72
83
|
const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
|
|
73
84
|
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
74
|
-
} else if (version == 'B01') {
|
|
75
|
-
const iv = CryptoUtils.md5hex(
|
|
85
|
+
} else if (header.version == 'B01') {
|
|
86
|
+
const iv = CryptoUtils.md5hex(header.nonce.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25);
|
|
76
87
|
const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
|
|
77
88
|
// unpad ??
|
|
78
89
|
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
// map visualization not support
|
|
82
|
-
if (
|
|
93
|
+
if (header.protocol == Protocol.map_response) {
|
|
83
94
|
return new ResponseMessage(duid, { dps: { id: 0, result: null } });
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
if (
|
|
97
|
+
if (header.protocol == Protocol.rpc_response || header.protocol == Protocol.general_request) {
|
|
87
98
|
return this.deserializeRpcResponse(duid, data);
|
|
88
99
|
} else {
|
|
89
|
-
this.logger.error('unknown protocol: ' +
|
|
100
|
+
this.logger.error('unknown protocol: ' + header.protocol);
|
|
90
101
|
return new ResponseMessage(duid, { dps: { id: 0, result: null } });
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
104
|
|
|
94
|
-
private deserializeRpcResponse(duid: string, data:
|
|
105
|
+
private deserializeRpcResponse(duid: string, data: ContentMessage): ResponseMessage {
|
|
95
106
|
const payload = JSON.parse(data.payload.toString());
|
|
96
107
|
const dps = payload.dps;
|
|
97
108
|
this.parseJsonInDps(dps, Protocol.general_request);
|
|
@@ -5,19 +5,20 @@ 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
7
|
import { AnsiLogger } from 'matterbridge/logger';
|
|
8
|
+
import { Protocol } from '../broadcast/model/protocol.js';
|
|
8
9
|
|
|
9
10
|
export class MessageSerializer {
|
|
10
11
|
private readonly context: MessageContext;
|
|
11
12
|
private readonly logger: AnsiLogger;
|
|
12
|
-
private
|
|
13
|
-
private
|
|
13
|
+
private sequence = 1;
|
|
14
|
+
private readonly supportedVersions: string[] = ['1.0', 'A01', 'B01'];
|
|
14
15
|
|
|
15
16
|
constructor(context: MessageContext, logger: AnsiLogger) {
|
|
16
17
|
this.context = context;
|
|
17
18
|
this.logger = logger;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
serialize(duid: string, request: RequestMessage): { messageId: number; buffer: Buffer<ArrayBufferLike> } {
|
|
21
|
+
public serialize(duid: string, request: RequestMessage): { messageId: number; buffer: Buffer<ArrayBufferLike> } {
|
|
21
22
|
const messageId = request.messageId;
|
|
22
23
|
const buffer = this.buildBuffer(duid, messageId, request);
|
|
23
24
|
return { messageId: messageId, buffer: buffer };
|
|
@@ -35,27 +36,31 @@ export class MessageSerializer {
|
|
|
35
36
|
if (request.secure) {
|
|
36
37
|
data.security = {
|
|
37
38
|
endpoint: this.context.getEndpoint(),
|
|
38
|
-
nonce: this.context.
|
|
39
|
+
nonce: this.context.getSerializeNonceAsHex(),
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
43
42
|
return {
|
|
44
43
|
dps: {
|
|
45
44
|
[request.protocol]: JSON.stringify(data),
|
|
46
45
|
},
|
|
47
|
-
t: timestamp,
|
|
46
|
+
t: request.timestamp,
|
|
48
47
|
};
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
private buildBuffer(duid: string, messageId: number, request: RequestMessage): Buffer<ArrayBufferLike> {
|
|
52
51
|
const version = this.context.getProtocolVersion(duid);
|
|
53
|
-
|
|
52
|
+
if (!version || !this.supportedVersions.includes(version)) {
|
|
53
|
+
throw new Error('unknown protocol version ' + version);
|
|
54
|
+
}
|
|
55
|
+
let encrypted;
|
|
56
|
+
if (request.protocol === Protocol.hello_response || request.protocol === Protocol.ping_response) {
|
|
57
|
+
encrypted = Buffer.alloc(0);
|
|
58
|
+
}
|
|
54
59
|
|
|
60
|
+
const localKey = this.context.getLocalKey(duid);
|
|
55
61
|
const payloadData = this.buildPayload(messageId, request);
|
|
56
62
|
const payload = JSON.stringify(payloadData);
|
|
57
63
|
|
|
58
|
-
let encrypted;
|
|
59
64
|
if (version == '1.0') {
|
|
60
65
|
const aesKey = CryptoUtils.md5bin(CryptoUtils.encodeTimestamp(payloadData.t) + localKey + CryptoUtils.SALT);
|
|
61
66
|
const cipher = crypto.createCipheriv('aes-128-ecb', aesKey, null);
|
|
@@ -63,12 +68,12 @@ export class MessageSerializer {
|
|
|
63
68
|
} else if (version == 'A01') {
|
|
64
69
|
// 726f626f726f636b2d67a6d6da is in version 4.0 of the roborock app
|
|
65
70
|
const encoder = new TextEncoder();
|
|
66
|
-
const iv = CryptoUtils.md5hex(
|
|
71
|
+
const iv = CryptoUtils.md5hex(request.nonce.toString(16).padStart(8, '0') + '726f626f726f636b2d67a6d6da').substring(8, 24);
|
|
67
72
|
const cipher = crypto.createCipheriv('aes-128-cbc', encoder.encode(localKey), iv);
|
|
68
73
|
encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
69
74
|
} else if (version == 'B01') {
|
|
70
75
|
const encoder = new TextEncoder();
|
|
71
|
-
const iv = CryptoUtils.md5hex(
|
|
76
|
+
const iv = CryptoUtils.md5hex(request.nonce.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25);
|
|
72
77
|
const cipher = crypto.createCipheriv('aes-128-cbc', encoder.encode(localKey), iv);
|
|
73
78
|
// pad ??
|
|
74
79
|
encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
@@ -78,9 +83,9 @@ export class MessageSerializer {
|
|
|
78
83
|
|
|
79
84
|
const msg = Buffer.alloc(23 + encrypted.length);
|
|
80
85
|
msg.write(version);
|
|
81
|
-
msg.writeUint32BE(this.
|
|
82
|
-
msg.writeUint32BE(
|
|
83
|
-
msg.writeUint32BE(
|
|
86
|
+
msg.writeUint32BE(this.sequence++ & 0xffffffff, 3);
|
|
87
|
+
msg.writeUint32BE(request.nonce & 0xffffffff, 7);
|
|
88
|
+
msg.writeUint32BE(request.timestamp, 11);
|
|
84
89
|
msg.writeUint16BE(request.protocol, 15);
|
|
85
90
|
msg.writeUint16BE(encrypted.length, 17);
|
|
86
91
|
encrypted.copy(msg, 19);
|
package/src/roborockService.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type { AbstractMessageHandler, AbstractMessageListener, BatteryMessage, D
|
|
|
25
25
|
import { ServiceArea } from 'matterbridge/matter/clusters';
|
|
26
26
|
import { LocalNetworkClient } from './roborockCommunication/broadcast/client/LocalNetworkClient.js';
|
|
27
27
|
import { RoomIndexMap } from './model/roomIndexMap.js';
|
|
28
|
+
import { DpsPayload } from './roborockCommunication/broadcast/model/dps.js';
|
|
28
29
|
export type Factory<A, T> = (logger: AnsiLogger, arg: A) => T;
|
|
29
30
|
|
|
30
31
|
export default class RoborockService {
|
|
@@ -479,7 +480,7 @@ export default class RoborockService {
|
|
|
479
480
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
480
481
|
const self = this;
|
|
481
482
|
this.messageClient = this.clientManager.get(username, userdata);
|
|
482
|
-
this.messageClient.registerDevice(device.duid, device.localKey, device.pv);
|
|
483
|
+
this.messageClient.registerDevice(device.duid, device.localKey, device.pv, undefined);
|
|
483
484
|
|
|
484
485
|
this.messageClient.registerMessageListener({
|
|
485
486
|
onMessage: (message: ResponseMessage) => {
|
|
@@ -493,6 +494,12 @@ export default class RoborockService {
|
|
|
493
494
|
self.deviceNotify(NotifyMessageTypes.CloudMessage, message);
|
|
494
495
|
}
|
|
495
496
|
}
|
|
497
|
+
|
|
498
|
+
if (message instanceof ResponseMessage && message.contain(Protocol.hello_response)) {
|
|
499
|
+
const dps = message.dps[Protocol.hello_response] as DpsPayload;
|
|
500
|
+
const result = dps.result as { nonce: number };
|
|
501
|
+
self.messageClient?.updateNonce(message.duid, result.nonce);
|
|
502
|
+
}
|
|
496
503
|
},
|
|
497
504
|
} as AbstractMessageListener);
|
|
498
505
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { getRunningMode } from '../initialData/getSupportedRunModes.js';
|
|
2
|
+
import { CloudMessageModel } from '../model/CloudMessageModel.js';
|
|
3
|
+
import { hasDockingStationError } from '../model/DockingStationStatus.js';
|
|
4
|
+
import { RoborockMatterbridgePlatform } from '../platform.js';
|
|
5
|
+
import { AdditionalPropCode, DeviceStatusNotify, Protocol } from '../roborockCommunication/index.js';
|
|
6
|
+
import { state_to_matter_operational_status, state_to_matter_state } from '../share/function.js';
|
|
7
|
+
import { RvcCleanMode, RvcOperationalState, RvcRunMode, ServiceArea } from 'matterbridge/matter/clusters';
|
|
8
|
+
import { triggerDssError } from './handleLocalMessage.js';
|
|
9
|
+
import { DpsPayload } from '../roborockCommunication/broadcast/model/dps.js';
|
|
10
|
+
import { getRoomMapFromDevice, isStatusUpdate } from '../helper.js';
|
|
11
|
+
import { debugStringify } from 'matterbridge/logger';
|
|
12
|
+
import { CloudMessageResult } from '../roborockCommunication/Zmodel/messageResult.js';
|
|
13
|
+
import { NotifyMessageTypes } from '../notifyMessageTypes.js';
|
|
14
|
+
import { getCurrentCleanModeFunc } from '../share/runtimeHelper.js';
|
|
15
|
+
import { getSupportedAreas } from '../initialData/getSupportedAreas.js';
|
|
16
|
+
import { PlatformRunner } from '../platformRunner.js';
|
|
17
|
+
import { RoborockVacuumCleaner } from '../rvc.js';
|
|
18
|
+
|
|
19
|
+
export async function handleCloudMessage(data: CloudMessageModel, platform: RoborockMatterbridgePlatform, runner: PlatformRunner, duid: string): Promise<void> {
|
|
20
|
+
const messageTypes = Object.keys(data.dps).map(Number);
|
|
21
|
+
const robot = platform.robots.get(duid);
|
|
22
|
+
if (robot === undefined) {
|
|
23
|
+
platform.log.error(`Error3: Robot with DUID ${duid} not found`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Known: 122, 121, 102,
|
|
28
|
+
// Unknown: 128, 139
|
|
29
|
+
messageTypes.forEach(async (messageType) => {
|
|
30
|
+
switch (messageType) {
|
|
31
|
+
case Protocol.status_update: {
|
|
32
|
+
const status = Number(data.dps[messageType]);
|
|
33
|
+
const matterState = state_to_matter_state(status);
|
|
34
|
+
|
|
35
|
+
if (matterState) {
|
|
36
|
+
robot.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(matterState), platform.log);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const operationalStateId = state_to_matter_operational_status(status);
|
|
40
|
+
if (operationalStateId) {
|
|
41
|
+
const dssHasError = hasDockingStationError(robot.dockStationStatus);
|
|
42
|
+
if (!(dssHasError && triggerDssError(robot, platform))) {
|
|
43
|
+
robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', operationalStateId, platform.log);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case Protocol.rpc_response: {
|
|
49
|
+
const response = data.dps[messageType] as DpsPayload;
|
|
50
|
+
// ignore network info
|
|
51
|
+
if (!isStatusUpdate(response.result)) {
|
|
52
|
+
platform.log.debug('Ignore message:', debugStringify(data));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let roboStatus: CloudMessageResult | undefined;
|
|
57
|
+
if (Array.isArray(response.result) && response.result.length > 0) {
|
|
58
|
+
roboStatus = response.result[0] as CloudMessageResult;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (roboStatus) {
|
|
62
|
+
const message = { ...roboStatus } as DeviceStatusNotify;
|
|
63
|
+
platform.log.debug('rpc_response:', debugStringify(message));
|
|
64
|
+
await runner.updateFromMQTTMessage(NotifyMessageTypes.LocalMessage, message, duid, true);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case Protocol.suction_power:
|
|
69
|
+
case Protocol.water_box_mode: {
|
|
70
|
+
await platform.roborockService?.getCleanModeData(duid).then((cleanModeData) => {
|
|
71
|
+
if (cleanModeData) {
|
|
72
|
+
const currentCleanMode = getCurrentCleanModeFunc(
|
|
73
|
+
robot.device.data.model,
|
|
74
|
+
platform.enableExperimentalFeature?.advancedFeature?.forceRunAtDefault ?? false,
|
|
75
|
+
)({
|
|
76
|
+
suctionPower: cleanModeData.suctionPower,
|
|
77
|
+
waterFlow: cleanModeData.waterFlow,
|
|
78
|
+
distance_off: cleanModeData.distance_off,
|
|
79
|
+
mopRoute: cleanModeData.mopRoute,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
platform.log.debug(`Clean mode data: ${debugStringify(cleanModeData)}`);
|
|
83
|
+
platform.log.debug(`Current clean mode: ${currentCleanMode}`);
|
|
84
|
+
if (currentCleanMode) {
|
|
85
|
+
robot.updateAttribute(RvcCleanMode.Cluster.id, 'currentMode', currentCleanMode, platform.log);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
break; // Do nothing, handled in local message
|
|
90
|
+
}
|
|
91
|
+
case Protocol.additional_props: {
|
|
92
|
+
platform.log.notice(`Received additional properties for robot ${duid}: ${debugStringify(data)}`);
|
|
93
|
+
const propCode = data.dps[Protocol.additional_props] as number;
|
|
94
|
+
platform.log.debug(`DPS for additional properties: ${propCode}, AdditionalPropCode: ${AdditionalPropCode[propCode]}`);
|
|
95
|
+
if (propCode === AdditionalPropCode.map_change) {
|
|
96
|
+
await handleMapChange(robot, platform, duid);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case Protocol.back_type: {
|
|
101
|
+
// TODO: check if this is needed
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
default: {
|
|
105
|
+
platform.log.notice(`Unknown message type ${messageType}, protocol: ${Protocol[messageType]}, message: ${debugStringify(data)}`);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function handleMapChange(robot: RoborockVacuumCleaner, platform: RoborockMatterbridgePlatform, duid: string): Promise<void> {
|
|
113
|
+
const enableMultipleMap = (platform.enableExperimentalFeature?.enableExperimentalFeature && platform.enableExperimentalFeature?.advancedFeature?.enableMultipleMap) ?? false;
|
|
114
|
+
if (!enableMultipleMap) return;
|
|
115
|
+
|
|
116
|
+
await getRoomMapFromDevice(robot.device, platform).then((roomMap) => {
|
|
117
|
+
const { supportedAreas, supportedMaps, roomIndexMap } = getSupportedAreas(robot.device.rooms, roomMap, enableMultipleMap, platform.log);
|
|
118
|
+
|
|
119
|
+
platform.log.debug(`handleMapChange - supportedAreas: ${debugStringify(supportedAreas)}`);
|
|
120
|
+
platform.log.debug(`handleMapChange - supportedMaps: ${debugStringify(supportedMaps)}`);
|
|
121
|
+
platform.log.debug(`handleMapChange - roomIndexMap: `, roomIndexMap);
|
|
122
|
+
|
|
123
|
+
platform.roborockService?.setSupportedAreas(duid, supportedAreas);
|
|
124
|
+
platform.roborockService?.setSelectedAreas(duid, []);
|
|
125
|
+
robot.updateAttribute(ServiceArea.Cluster.id, 'supportedAreas', supportedAreas, platform.log);
|
|
126
|
+
robot.updateAttribute(ServiceArea.Cluster.id, 'selectedAreas', [], platform.log);
|
|
127
|
+
robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', null, platform.log);
|
|
128
|
+
|
|
129
|
+
if (enableMultipleMap) {
|
|
130
|
+
platform.roborockService?.setSupportedAreaIndexMap(duid, roomIndexMap);
|
|
131
|
+
robot.updateAttribute(ServiceArea.Cluster.id, 'supportedMaps', supportedMaps, platform.log);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { PowerSource, RvcOperationalState, RvcRunMode } from 'matterbridge/matter/clusters';
|
|
2
|
+
import { getVacuumProperty } from '../helper.js';
|
|
3
|
+
import { RoborockMatterbridgePlatform } from '../platform.js';
|
|
4
|
+
import { Device, Home } from '../roborockCommunication/index.js';
|
|
5
|
+
import { debugStringify } from 'matterbridge/logger';
|
|
6
|
+
import { getBatteryState, getBatteryStatus } from '../initialData/index.js';
|
|
7
|
+
import { state_to_matter_operational_status, state_to_matter_state } from '../share/function.js';
|
|
8
|
+
import { OperationStatusCode } from '../roborockCommunication/Zenum/operationStatusCode.js';
|
|
9
|
+
import { getRunningMode } from '../initialData/getSupportedRunModes.js';
|
|
10
|
+
import { hasDockingStationError } from '../model/DockingStationStatus.js';
|
|
11
|
+
import { triggerDssError } from './handleLocalMessage.js';
|
|
12
|
+
|
|
13
|
+
export async function updateFromHomeData(homeData: Home, platform: RoborockMatterbridgePlatform): Promise<void> {
|
|
14
|
+
if (platform.robots.size === 0) return;
|
|
15
|
+
const devices = homeData.devices.filter((d: Device) => platform.robots.has(d.duid));
|
|
16
|
+
|
|
17
|
+
for (const device of devices) {
|
|
18
|
+
const robot = platform.robots.get(device.duid);
|
|
19
|
+
if (robot === undefined) {
|
|
20
|
+
platform.log.error(`Error5: Robot with DUID ${device.duid} not found`);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const deviceData = robot.device.data;
|
|
25
|
+
if (!device || deviceData === undefined) {
|
|
26
|
+
platform.log.error('Device not found in home data');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
device.schema = homeData.products.find((prd) => prd.id === device.productId || prd.model === device.data.model)?.schema ?? [];
|
|
31
|
+
platform.log.debug('updateFromHomeData-homeData:', debugStringify(homeData));
|
|
32
|
+
platform.log.debug('updateFromHomeData-device:', debugStringify(device));
|
|
33
|
+
platform.log.debug('updateFromHomeData-schema:' + debugStringify(device.schema));
|
|
34
|
+
platform.log.debug('updateFromHomeData-battery:' + debugStringify(device.deviceStatus));
|
|
35
|
+
|
|
36
|
+
const batteryLevel = getVacuumProperty(device, 'battery');
|
|
37
|
+
if (batteryLevel) {
|
|
38
|
+
robot.updateAttribute(PowerSource.Cluster.id, 'batPercentRemaining', batteryLevel ? batteryLevel * 2 : 200, platform.log);
|
|
39
|
+
robot.updateAttribute(PowerSource.Cluster.id, 'batChargeLevel', getBatteryStatus(batteryLevel), platform.log);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const state = getVacuumProperty(device, 'state');
|
|
43
|
+
const matterState = state_to_matter_state(state);
|
|
44
|
+
if (!state || !matterState) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
platform.log.debug(`updateFromHomeData-RvcRunMode code: ${state} name: ${OperationStatusCode[state]}, matterState: ${RvcRunMode.ModeTag[matterState]}`);
|
|
48
|
+
|
|
49
|
+
if (matterState) {
|
|
50
|
+
robot.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(matterState), platform.log);
|
|
51
|
+
}
|
|
52
|
+
if (batteryLevel) {
|
|
53
|
+
robot.updateAttribute(PowerSource.Cluster.id, 'batChargeState', getBatteryState(state, batteryLevel), platform.log);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const operationalStateId = state_to_matter_operational_status(state);
|
|
57
|
+
|
|
58
|
+
if (operationalStateId) {
|
|
59
|
+
const dssHasError = hasDockingStationError(robot.dockStationStatus);
|
|
60
|
+
platform.log.debug(`dssHasError: ${dssHasError}, dockStationStatus: ${debugStringify(robot.dockStationStatus ?? {})}`);
|
|
61
|
+
if (!(dssHasError && triggerDssError(robot, platform))) {
|
|
62
|
+
platform.log.debug(`updateFromHomeData-OperationalState: ${RvcOperationalState.OperationalState[operationalStateId]}`);
|
|
63
|
+
robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', operationalStateId, platform.log);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|