matterbridge-roborock-vacuum-plugin 1.1.0-rc09 → 1.1.0-rc11
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/platform.js +6 -4
- package/dist/roborockCommunication/RESTAPI/roborockIoTApi.js +22 -0
- package/dist/roborockCommunication/Zmodel/map.js +1 -0
- package/dist/roborockCommunication/Zmodel/mapInfo.js +26 -0
- package/dist/roborockCommunication/Zmodel/multipleMap.js +1 -0
- package/dist/roborockCommunication/broadcast/abstractClient.js +10 -2
- package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +22 -2
- package/dist/roborockCommunication/broadcast/client/MQTTClient.js +4 -0
- package/dist/roborockCommunication/broadcast/clientRouter.js +2 -2
- package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js +9 -2
- package/dist/roborockCommunication/broadcast/listener/implementation/syncMessageListener.js +3 -2
- package/dist/roborockCommunication/broadcast/messageProcessor.js +6 -3
- package/dist/roborockCommunication/helper/messageDeserializer.js +9 -3
- package/dist/roborockCommunication/helper/messageSerializer.js +6 -0
- package/dist/roborockCommunication/index.js +1 -0
- package/dist/roborockService.js +84 -13
- package/eslint.config.js +1 -1
- 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/platform.ts +7 -5
- package/src/roborockCommunication/RESTAPI/roborockIoTApi.ts +25 -1
- package/src/roborockCommunication/Zmodel/device.ts +1 -0
- package/src/roborockCommunication/Zmodel/map.ts +14 -0
- package/src/roborockCommunication/Zmodel/mapInfo.ts +34 -0
- package/src/roborockCommunication/Zmodel/multipleMap.ts +8 -0
- package/src/roborockCommunication/Zmodel/userData.ts +0 -3
- package/src/roborockCommunication/broadcast/abstractClient.ts +13 -4
- package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +26 -2
- package/src/roborockCommunication/broadcast/client/MQTTClient.ts +4 -0
- package/src/roborockCommunication/broadcast/client.ts +1 -1
- package/src/roborockCommunication/broadcast/clientRouter.ts +3 -3
- package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts +10 -2
- package/src/roborockCommunication/broadcast/listener/implementation/syncMessageListener.ts +4 -3
- package/src/roborockCommunication/broadcast/messageProcessor.ts +9 -5
- package/src/roborockCommunication/helper/messageDeserializer.ts +9 -3
- package/src/roborockCommunication/helper/messageSerializer.ts +6 -0
- package/src/roborockCommunication/index.ts +2 -0
- package/src/roborockService.ts +96 -17
- package/src/tests/roborockCommunication/broadcast/listener/implementation/syncMessageListener.test.ts +5 -4
- package/src/tests/roborockService.test.ts +3 -11
- package/web-for-testing/README.md +47 -0
- package/web-for-testing/nodemon.json +7 -0
- package/web-for-testing/package-lock.json +6598 -0
- package/web-for-testing/package.json +36 -0
- package/web-for-testing/src/accountStore.ts +8 -0
- package/web-for-testing/src/app.ts +194 -0
- package/web-for-testing/tsconfig-ext.json +19 -0
- package/web-for-testing/tsconfig.json +23 -0
- package/web-for-testing/views/index.ejs +172 -0
- package/web-for-testing/watch.mjs +93 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import decodeComponent from '../helper/nameDecoder.js';
|
|
2
|
+
import { RoomInformation } from './map.js';
|
|
3
|
+
import type { MultipleMap } from './multipleMap.js';
|
|
4
|
+
import { Room } from './room.js';
|
|
5
|
+
|
|
6
|
+
export class MapInfo {
|
|
7
|
+
readonly maps: { id: number; name: string | undefined; rooms: Room[] }[] = [];
|
|
8
|
+
|
|
9
|
+
constructor(multimap: MultipleMap) {
|
|
10
|
+
multimap.map_info.forEach((map) => {
|
|
11
|
+
this.maps.push({
|
|
12
|
+
id: map.mapFlag,
|
|
13
|
+
name: decodeComponent(map.name)?.toLowerCase(),
|
|
14
|
+
rooms:
|
|
15
|
+
map.rooms && map.rooms.length > 0
|
|
16
|
+
? map.rooms.map((room: RoomInformation) => {
|
|
17
|
+
return {
|
|
18
|
+
id: room.iot_name_id,
|
|
19
|
+
name: room.iot_name,
|
|
20
|
+
} as unknown as Room;
|
|
21
|
+
})
|
|
22
|
+
: [],
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getById(id: number): string | undefined {
|
|
28
|
+
return this.maps.find((m) => m.id === id)?.name;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getByName(name: string): number | undefined {
|
|
32
|
+
return this.maps.find((m) => m.name === name.toLowerCase())?.id;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -14,6 +14,7 @@ import { ConnectionStateListener } from './listener/implementation/connectionSta
|
|
|
14
14
|
|
|
15
15
|
export abstract class AbstractClient implements Client {
|
|
16
16
|
public isInDisconnectingStep = false;
|
|
17
|
+
public retryCount = 0;
|
|
17
18
|
|
|
18
19
|
protected readonly connectionListeners = new ChainedConnectionListener();
|
|
19
20
|
protected readonly messageListeners = new ChainedMessageListener();
|
|
@@ -24,6 +25,7 @@ export abstract class AbstractClient implements Client {
|
|
|
24
25
|
|
|
25
26
|
protected abstract clientName: string;
|
|
26
27
|
protected abstract shouldReconnect: boolean;
|
|
28
|
+
protected abstract changeToSecureConnection: (duid: string) => void;
|
|
27
29
|
|
|
28
30
|
private readonly context: MessageContext;
|
|
29
31
|
private readonly syncMessageListener: SyncMessageListener;
|
|
@@ -39,7 +41,7 @@ export abstract class AbstractClient implements Client {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
protected initializeConnectionStateListener() {
|
|
42
|
-
const connectionStateListener = new ConnectionStateListener(this.logger, this, this.clientName, this.shouldReconnect);
|
|
44
|
+
const connectionStateListener = new ConnectionStateListener(this.logger, this, this.clientName, this.changeToSecureConnection, this.shouldReconnect);
|
|
43
45
|
this.connectionListeners.register(connectionStateListener);
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -47,11 +49,18 @@ export abstract class AbstractClient implements Client {
|
|
|
47
49
|
abstract disconnect(): Promise<void>;
|
|
48
50
|
abstract send(duid: string, request: RequestMessage): Promise<void>;
|
|
49
51
|
|
|
50
|
-
public async get<T>(duid: string, request: RequestMessage): Promise<T> {
|
|
52
|
+
public async get<T>(duid: string, request: RequestMessage): Promise<T | undefined> {
|
|
51
53
|
return new Promise<T>((resolve, reject) => {
|
|
52
|
-
this.syncMessageListener.waitFor(request.messageId, (response: ResponseMessage) => resolve(response as unknown as T), reject);
|
|
54
|
+
this.syncMessageListener.waitFor(request.messageId, request, (response: ResponseMessage) => resolve(response as unknown as T), reject);
|
|
53
55
|
this.send(duid, request);
|
|
54
|
-
})
|
|
56
|
+
})
|
|
57
|
+
.then((result: T) => {
|
|
58
|
+
return result;
|
|
59
|
+
})
|
|
60
|
+
.catch((error: Error) => {
|
|
61
|
+
this.logger.error(error.message);
|
|
62
|
+
return undefined;
|
|
63
|
+
});
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
public registerDevice(duid: string, localKey: string, pv: string): void {
|
|
@@ -9,6 +9,7 @@ import { Sequence } from '../../helper/sequence.js';
|
|
|
9
9
|
import { ChunkBuffer } from '../../helper/chunkBuffer.js';
|
|
10
10
|
|
|
11
11
|
export class LocalNetworkClient extends AbstractClient {
|
|
12
|
+
protected override changeToSecureConnection: (duid: string) => void;
|
|
12
13
|
protected override clientName = 'LocalNetworkClient';
|
|
13
14
|
protected override shouldReconnect = true;
|
|
14
15
|
|
|
@@ -19,19 +20,26 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
19
20
|
duid: string;
|
|
20
21
|
ip: string;
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
constructor(logger: AnsiLogger, context: MessageContext, duid: string, ip: string, inject: (duid: string) => void) {
|
|
23
24
|
super(logger, context);
|
|
24
25
|
this.duid = duid;
|
|
25
26
|
this.ip = ip;
|
|
26
27
|
this.messageIdSeq = new Sequence(100000, 999999);
|
|
27
28
|
|
|
28
29
|
this.initializeConnectionStateListener();
|
|
30
|
+
this.changeToSecureConnection = inject;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
public connect(): void {
|
|
34
|
+
if (this.socket) {
|
|
35
|
+
this.socket.destroy();
|
|
36
|
+
this.socket = undefined;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
this.socket = new Socket();
|
|
33
41
|
this.socket.on('close', this.onDisconnect.bind(this));
|
|
34
|
-
this.socket.on('end', this.
|
|
42
|
+
this.socket.on('end', this.onEnd.bind(this));
|
|
35
43
|
this.socket.on('error', this.onError.bind(this));
|
|
36
44
|
this.socket.on('data', this.onMessage.bind(this));
|
|
37
45
|
this.socket.connect(58867, this.ip, this.onConnect.bind(this));
|
|
@@ -71,6 +79,22 @@ export class LocalNetworkClient extends AbstractClient {
|
|
|
71
79
|
await this.sendHelloMessage();
|
|
72
80
|
this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
|
|
73
81
|
await this.connectionListeners.onConnected(this.duid);
|
|
82
|
+
this.retryCount = 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async onEnd(): Promise<void> {
|
|
86
|
+
this.logger.notice('LocalNetworkClient: Socket has ended.');
|
|
87
|
+
this.connected = false;
|
|
88
|
+
|
|
89
|
+
if (this.socket) {
|
|
90
|
+
this.socket.destroy();
|
|
91
|
+
this.socket = undefined;
|
|
92
|
+
}
|
|
93
|
+
if (this.pingInterval) {
|
|
94
|
+
clearInterval(this.pingInterval);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await this.connectionListeners.onDisconnected(this.duid);
|
|
74
98
|
}
|
|
75
99
|
|
|
76
100
|
private async onDisconnect(): Promise<void> {
|
|
@@ -7,6 +7,7 @@ import { Rriot, UserData } from '../../Zmodel/userData.js';
|
|
|
7
7
|
import { AnsiLogger, debugStringify } from 'matterbridge/logger';
|
|
8
8
|
|
|
9
9
|
export class MQTTClient extends AbstractClient {
|
|
10
|
+
protected override changeToSecureConnection: (duid: string) => void;
|
|
10
11
|
protected override clientName = 'MQTTClient';
|
|
11
12
|
protected override shouldReconnect = false;
|
|
12
13
|
|
|
@@ -23,6 +24,9 @@ export class MQTTClient extends AbstractClient {
|
|
|
23
24
|
this.mqttPassword = CryptoUtils.md5hex(userdata.rriot.s + ':' + userdata.rriot.k).substring(16);
|
|
24
25
|
|
|
25
26
|
this.initializeConnectionStateListener();
|
|
27
|
+
this.changeToSecureConnection = (duid: string) => {
|
|
28
|
+
this.logger.info(`MqttClient for ${duid} has been disconnected`);
|
|
29
|
+
};
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
public connect(): void {
|
|
@@ -32,8 +32,8 @@ export class ClientRouter implements Client {
|
|
|
32
32
|
this.context.registerDevice(duid, localKey, pv);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
public registerClient(duid: string, ip: string): Client {
|
|
36
|
-
const localClient = new LocalNetworkClient(this.logger, this.context, duid, ip);
|
|
35
|
+
public registerClient(duid: string, ip: string, onDisconnect: (duid: string) => void): Client {
|
|
36
|
+
const localClient = new LocalNetworkClient(this.logger, this.context, duid, ip, onDisconnect);
|
|
37
37
|
localClient.registerConnectionListener(this.connectionListeners);
|
|
38
38
|
localClient.registerMessageListener(this.messageListeners);
|
|
39
39
|
|
|
@@ -81,7 +81,7 @@ export class ClientRouter implements Client {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
async get<T>(duid: string, request: RequestMessage): Promise<T> {
|
|
84
|
+
async get<T>(duid: string, request: RequestMessage): Promise<T | undefined> {
|
|
85
85
|
if (request.secure) {
|
|
86
86
|
return await this.mqttClient.get(duid, request);
|
|
87
87
|
} else {
|
package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts
CHANGED
|
@@ -7,12 +7,14 @@ export class ConnectionStateListener implements AbstractConnectionListener {
|
|
|
7
7
|
protected client: AbstractClient;
|
|
8
8
|
protected clientName: string;
|
|
9
9
|
protected shouldReconnect: boolean;
|
|
10
|
+
protected changeToSecureConnection: (duid: string) => void;
|
|
10
11
|
|
|
11
|
-
constructor(logger: AnsiLogger, client: AbstractClient, clientName: string, shouldReconnect = false) {
|
|
12
|
+
constructor(logger: AnsiLogger, client: AbstractClient, clientName: string, changeToSecureConnection: (duid: string) => void, shouldReconnect = false) {
|
|
12
13
|
this.logger = logger;
|
|
13
14
|
this.client = client;
|
|
14
15
|
this.clientName = clientName;
|
|
15
16
|
this.shouldReconnect = shouldReconnect;
|
|
17
|
+
this.changeToSecureConnection = changeToSecureConnection;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
public async onConnected(duid: string): Promise<void> {
|
|
@@ -25,7 +27,13 @@ export class ConnectionStateListener implements AbstractConnectionListener {
|
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
this.
|
|
30
|
+
if (this.client.retryCount > 10) {
|
|
31
|
+
this.logger.error(`Device with DUID ${duid} has exceeded retry limit, not re-registering.`);
|
|
32
|
+
this.changeToSecureConnection(duid);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.client.retryCount++;
|
|
29
37
|
|
|
30
38
|
const isInDisconnectingStep = this.client.isInDisconnectingStep;
|
|
31
39
|
if (isInDisconnectingStep) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { AnsiLogger } from 'matterbridge/logger';
|
|
1
|
+
import { AnsiLogger, debugStringify } from 'matterbridge/logger';
|
|
2
2
|
import { DpsPayload } from '../../model/dps.js';
|
|
3
3
|
import { Protocol } from '../../model/protocol.js';
|
|
4
4
|
import { ResponseMessage } from '../../model/responseMessage.js';
|
|
5
5
|
import { AbstractMessageListener } from '../index.js';
|
|
6
|
+
import { RequestMessage } from '../../model/requestMessage.js';
|
|
6
7
|
|
|
7
8
|
export class SyncMessageListener implements AbstractMessageListener {
|
|
8
9
|
private readonly pending = new Map<number, (response: ResponseMessage) => void>();
|
|
@@ -12,12 +13,12 @@ export class SyncMessageListener implements AbstractMessageListener {
|
|
|
12
13
|
this.logger = logger;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
public waitFor(messageId: number, resolve: (response: ResponseMessage) => void, reject: () => void): void {
|
|
16
|
+
public waitFor(messageId: number, request: RequestMessage, resolve: (response: ResponseMessage) => void, reject: (error?: Error) => void): void {
|
|
16
17
|
this.pending.set(messageId, resolve);
|
|
17
18
|
|
|
18
19
|
setTimeout(() => {
|
|
19
20
|
this.pending.delete(messageId);
|
|
20
|
-
reject();
|
|
21
|
+
reject(new Error(`Message timeout for messageId: ${messageId}, request: ${debugStringify(request)}`));
|
|
21
22
|
}, 10000);
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -29,7 +29,7 @@ export class MessageProcessor {
|
|
|
29
29
|
this.messageListener.registerListener(listener);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
public async getNetworkInfo(duid: string): Promise<NetworkInfo> {
|
|
32
|
+
public async getNetworkInfo(duid: string): Promise<NetworkInfo | undefined> {
|
|
33
33
|
const request = new RequestMessage({ method: 'get_network_info' });
|
|
34
34
|
return await this.client.get(duid, request);
|
|
35
35
|
}
|
|
@@ -39,17 +39,21 @@ export class MessageProcessor {
|
|
|
39
39
|
// return this.client.get<CloudMessageResult[]>(duid, request).then((response) => new DeviceStatus(response[0]));
|
|
40
40
|
// }
|
|
41
41
|
|
|
42
|
-
public async getDeviceStatus(duid: string): Promise<DeviceStatus> {
|
|
42
|
+
public async getDeviceStatus(duid: string): Promise<DeviceStatus | undefined> {
|
|
43
43
|
const request = new RequestMessage({ method: 'get_status' });
|
|
44
44
|
const response = await this.client.get<CloudMessageResult[]>(duid, request);
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
if (response) {
|
|
47
|
+
this.logger?.debug('Device status: ', debugStringify(response));
|
|
48
|
+
return new DeviceStatus(response);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return undefined;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
public async getRooms(duid: string, rooms: Room[]): Promise<RoomInfo> {
|
|
51
55
|
const request = new RequestMessage({ method: 'get_room_mapping' });
|
|
52
|
-
return this.client.get<number[][]>(duid, request).then((response) => new RoomInfo(rooms, response));
|
|
56
|
+
return this.client.get<number[][] | undefined>(duid, request).then((response) => new RoomInfo(rooms, response ?? []));
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
public async gotoDock(duid: string): Promise<void> {
|
|
@@ -23,6 +23,7 @@ export class MessageDeserializer {
|
|
|
23
23
|
private readonly context: MessageContext;
|
|
24
24
|
private readonly messageParser: Parser;
|
|
25
25
|
private readonly logger: AnsiLogger;
|
|
26
|
+
private readonly supportedVersions: string[] = ['1.0', 'A01', 'B01'];
|
|
26
27
|
|
|
27
28
|
constructor(context: MessageContext, logger: AnsiLogger) {
|
|
28
29
|
this.context = context;
|
|
@@ -46,7 +47,7 @@ export class MessageDeserializer {
|
|
|
46
47
|
|
|
47
48
|
public deserialize(duid: string, message: Buffer<ArrayBufferLike>): ResponseMessage {
|
|
48
49
|
const version = message.toString('latin1', 0, 3);
|
|
49
|
-
if (
|
|
50
|
+
if (!this.supportedVersions.includes(version)) {
|
|
50
51
|
throw new Error('unknown protocol version ' + version);
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -71,6 +72,11 @@ export class MessageDeserializer {
|
|
|
71
72
|
const iv = CryptoUtils.md5hex(data.random.toString(16).padStart(8, '0') + '726f626f726f636b2d67a6d6da').substring(8, 24);
|
|
72
73
|
const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
|
|
73
74
|
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
75
|
+
} else if (version == 'B01') {
|
|
76
|
+
const iv = CryptoUtils.md5hex(data.random.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25);
|
|
77
|
+
const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
|
|
78
|
+
// unpad ??
|
|
79
|
+
data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
// map visualization not support
|
|
@@ -79,14 +85,14 @@ export class MessageDeserializer {
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
if (data.protocol == Protocol.rpc_response || data.protocol == Protocol.general_request) {
|
|
82
|
-
return this.
|
|
88
|
+
return this.deserializeRpcResponse(duid, data);
|
|
83
89
|
} else {
|
|
84
90
|
this.logger.error('unknown protocol: ' + data.protocol);
|
|
85
91
|
return new ResponseMessage(duid, { dps: { id: 0, result: null } });
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
private
|
|
95
|
+
private deserializeRpcResponse(duid: string, data: Message): ResponseMessage {
|
|
90
96
|
const payload = JSON.parse(data.payload.toString());
|
|
91
97
|
const dps = payload.dps;
|
|
92
98
|
this.parseJsonInDps(dps, Protocol.general_request);
|
|
@@ -66,6 +66,12 @@ export class MessageSerializer {
|
|
|
66
66
|
const iv = CryptoUtils.md5hex(this.random.toString(16).padStart(8, '0') + '726f626f726f636b2d67a6d6da').substring(8, 24);
|
|
67
67
|
const cipher = crypto.createCipheriv('aes-128-cbc', encoder.encode(localKey), iv);
|
|
68
68
|
encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
69
|
+
} else if (version == 'B01') {
|
|
70
|
+
const encoder = new TextEncoder();
|
|
71
|
+
const iv = CryptoUtils.md5hex(this.random.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25);
|
|
72
|
+
const cipher = crypto.createCipheriv('aes-128-cbc', encoder.encode(localKey), iv);
|
|
73
|
+
// pad ??
|
|
74
|
+
encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
69
75
|
} else {
|
|
70
76
|
throw new Error('unable to build the message: unsupported protocol version: ' + version);
|
|
71
77
|
}
|
|
@@ -7,6 +7,7 @@ export { Protocol } from './broadcast/model/protocol.js';
|
|
|
7
7
|
export { ClientRouter } from './broadcast/clientRouter.js';
|
|
8
8
|
export { DeviceStatus } from './Zmodel/deviceStatus.js';
|
|
9
9
|
export { ResponseMessage } from './broadcast/model/responseMessage.js';
|
|
10
|
+
export { MapInfo } from './Zmodel/mapInfo.js';
|
|
10
11
|
|
|
11
12
|
export { Scene } from './Zmodel/scene.js';
|
|
12
13
|
|
|
@@ -18,3 +19,4 @@ export type { Home } from './Zmodel/home.js';
|
|
|
18
19
|
export type { Client } from './broadcast/client.js';
|
|
19
20
|
export type { SceneParam } from './Zmodel/scene.js';
|
|
20
21
|
export type { BatteryMessage, DeviceErrorMessage, DeviceStatusNotify } from './Zmodel/batteryMessage.js';
|
|
22
|
+
export type { MultipleMap } from './Zmodel/multipleMap.js';
|
package/src/roborockService.ts
CHANGED
|
@@ -19,8 +19,9 @@ import {
|
|
|
19
19
|
ResponseMessage,
|
|
20
20
|
Scene,
|
|
21
21
|
SceneParam,
|
|
22
|
+
MapInfo,
|
|
22
23
|
} from './roborockCommunication/index.js';
|
|
23
|
-
import type { AbstractMessageHandler, AbstractMessageListener, BatteryMessage, DeviceErrorMessage, DeviceStatusNotify } from './roborockCommunication/index.js';
|
|
24
|
+
import type { AbstractMessageHandler, AbstractMessageListener, BatteryMessage, DeviceErrorMessage, DeviceStatusNotify, MultipleMap } from './roborockCommunication/index.js';
|
|
24
25
|
import { ServiceArea } from 'matterbridge/matter/clusters';
|
|
25
26
|
import { LocalNetworkClient } from './roborockCommunication/broadcast/client/LocalNetworkClient.js';
|
|
26
27
|
export type Factory<A, T> = (logger: AnsiLogger, arg: A) => T;
|
|
@@ -38,6 +39,7 @@ export default class RoborockService {
|
|
|
38
39
|
messageProcessorMap = new Map<string, MessageProcessor>();
|
|
39
40
|
ipMap = new Map<string, string>();
|
|
40
41
|
localClientMap = new Map<string, Client>();
|
|
42
|
+
mqttAlwaysOnDevices = new Map<string, boolean>();
|
|
41
43
|
clientManager: ClientManager;
|
|
42
44
|
refreshInterval: number;
|
|
43
45
|
requestDeviceStatusInterval: NodeJS.Timeout | undefined;
|
|
@@ -47,6 +49,8 @@ export default class RoborockService {
|
|
|
47
49
|
private supportedRoutines = new Map<string, ServiceArea.Area[]>();
|
|
48
50
|
private selectedAreas = new Map<string, number[]>();
|
|
49
51
|
|
|
52
|
+
private readonly vacuumNeedAPIV3 = ['roborock.vacuum.ss07'];
|
|
53
|
+
|
|
50
54
|
constructor(
|
|
51
55
|
authenticateApiSupplier: Factory<void, RoborockAuthenticateApi> = (logger) => new RoborockAuthenticateApi(logger),
|
|
52
56
|
iotApiSupplier: Factory<UserData, RoborockIoTApi> = (logger, ud) => new RoborockIoTApi(ud, logger),
|
|
@@ -115,6 +119,20 @@ export default class RoborockService {
|
|
|
115
119
|
return data;
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
public async getRoomIdFromMap(duid: string): Promise<number | undefined> {
|
|
123
|
+
const data = (await this.customGet(duid, new RequestMessage({ method: 'get_map_v1' }))) as { vacuumRoom?: number };
|
|
124
|
+
return data?.vacuumRoom;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public async getMapInformation(duid: string): Promise<MapInfo | undefined> {
|
|
128
|
+
this.logger.debug('RoborockService - getMapInformation', duid);
|
|
129
|
+
assert(this.messageClient !== undefined);
|
|
130
|
+
return this.messageClient.get<MultipleMap[] | undefined>(duid, new RequestMessage({ method: 'get_multi_maps_list' })).then((response) => {
|
|
131
|
+
this.logger.debug('RoborockService - getMapInformation response', debugStringify(response ?? []));
|
|
132
|
+
return response ? new MapInfo(response[0]) : undefined;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
118
136
|
public async changeCleanMode(
|
|
119
137
|
duid: string,
|
|
120
138
|
{ suctionPower, waterFlow, distance_off, mopRoute }: { suctionPower: number; waterFlow: number; distance_off: number; mopRoute: number },
|
|
@@ -189,20 +207,26 @@ export default class RoborockService {
|
|
|
189
207
|
await this.getMessageProcessor(duid)?.findMyRobot(duid);
|
|
190
208
|
}
|
|
191
209
|
|
|
192
|
-
public async customGet(duid: string,
|
|
193
|
-
this.logger.debug('RoborockService - customSend-message', method);
|
|
194
|
-
return this.getMessageProcessor(duid)?.getCustomMessage(duid,
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
public async customGetInSecure(duid: string, method: string): Promise<unknown> {
|
|
198
|
-
this.logger.debug('RoborockService - customGetInSecure-message', method);
|
|
199
|
-
return this.getMessageProcessor(duid)?.getCustomMessage(duid, new RequestMessage({ method, secure: true }));
|
|
210
|
+
public async customGet(duid: string, request: RequestMessage): Promise<unknown> {
|
|
211
|
+
this.logger.debug('RoborockService - customSend-message', request.method, request.params, request.secure);
|
|
212
|
+
return this.getMessageProcessor(duid)?.getCustomMessage(duid, request);
|
|
200
213
|
}
|
|
201
214
|
|
|
202
215
|
public async customSend(duid: string, request: RequestMessage): Promise<void> {
|
|
203
216
|
return this.getMessageProcessor(duid)?.sendCustomMessage(duid, request);
|
|
204
217
|
}
|
|
205
218
|
|
|
219
|
+
public async getCustomAPI(url: string): Promise<unknown> {
|
|
220
|
+
this.logger.debug('RoborockService - getCustomAPI', url);
|
|
221
|
+
assert(this.iotApi !== undefined);
|
|
222
|
+
try {
|
|
223
|
+
return await this.iotApi.getCustom(url);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
this.logger.error(`Failed to get custom API with url ${url}: ${error ? debugStringify(error) : 'undefined'}`);
|
|
226
|
+
return { result: undefined, error: `Failed to get custom API with url ${url}` };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
206
230
|
public stopService(): void {
|
|
207
231
|
if (this.messageClient) {
|
|
208
232
|
this.messageClient.disconnect();
|
|
@@ -236,15 +260,15 @@ export default class RoborockService {
|
|
|
236
260
|
this.deviceNotify = callback;
|
|
237
261
|
}
|
|
238
262
|
|
|
239
|
-
public
|
|
263
|
+
public activateDeviceNotify(device: Device): void {
|
|
240
264
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
241
265
|
const self = this;
|
|
242
266
|
this.logger.debug('Requesting device info for device', device.duid);
|
|
243
267
|
const messageProcessor = this.getMessageProcessor(device.duid);
|
|
244
268
|
this.requestDeviceStatusInterval = setInterval(async () => {
|
|
245
269
|
if (messageProcessor) {
|
|
246
|
-
await messageProcessor.getDeviceStatus(device.duid).then((response: DeviceStatus) => {
|
|
247
|
-
if (self.deviceNotify) {
|
|
270
|
+
await messageProcessor.getDeviceStatus(device.duid).then((response: DeviceStatus | undefined) => {
|
|
271
|
+
if (self.deviceNotify && response) {
|
|
248
272
|
const message = { duid: device.duid, ...response.errorStatus, ...response.message } as DeviceStatusNotify;
|
|
249
273
|
self.logger.debug('Device status update', debugStringify(message));
|
|
250
274
|
self.deviceNotify(NotifyMessageTypes.LocalMessage, message);
|
|
@@ -274,8 +298,31 @@ export default class RoborockService {
|
|
|
274
298
|
|
|
275
299
|
const products = new Map<string, string>();
|
|
276
300
|
homeData.products.forEach((p) => products.set(p.id, p.model));
|
|
301
|
+
|
|
302
|
+
if (homeData.products.some((p) => this.vacuumNeedAPIV3.includes(p.model))) {
|
|
303
|
+
this.logger.debug('Using v3 API for home data retrieval');
|
|
304
|
+
const homeDataV3 = await this.iotApi.getHomev3(homeDetails.rrHomeId);
|
|
305
|
+
if (!homeDataV3) {
|
|
306
|
+
throw new Error('Failed to retrieve the home data from v3 API');
|
|
307
|
+
}
|
|
308
|
+
homeData.devices = [...homeData.devices, ...homeDataV3.devices.filter((d) => !homeData.devices.some((x) => x.duid === d.duid))];
|
|
309
|
+
homeData.receivedDevices = [...homeData.receivedDevices, ...homeDataV3.receivedDevices.filter((d) => !homeData.receivedDevices.some((x) => x.duid === d.duid))];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Try to get rooms from v2 API if rooms are empty
|
|
313
|
+
if (homeData.rooms.length === 0) {
|
|
314
|
+
const homeDataV2 = await this.iotApi.getHomev2(homeDetails.rrHomeId);
|
|
315
|
+
if (homeDataV2 && homeDataV2.rooms && homeDataV2.rooms.length > 0) {
|
|
316
|
+
homeData.rooms = homeDataV2.rooms;
|
|
317
|
+
} else {
|
|
318
|
+
const homeDataV3 = await this.iotApi.getHomev3(homeDetails.rrHomeId);
|
|
319
|
+
if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
|
|
320
|
+
homeData.rooms = homeDataV3.rooms;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
277
325
|
const devices: Device[] = [...homeData.devices, ...homeData.receivedDevices];
|
|
278
|
-
// homeData.devices.length > 0 ? homeData.devices : homeData.receivedDevices;
|
|
279
326
|
|
|
280
327
|
const result = devices.map((device) => {
|
|
281
328
|
return {
|
|
@@ -293,6 +340,7 @@ export default class RoborockService {
|
|
|
293
340
|
model: homeData.products.find((p) => p.id === device.productId)?.model,
|
|
294
341
|
category: homeData.products.find((p) => p.id === device.productId)?.category,
|
|
295
342
|
batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100,
|
|
343
|
+
schema: homeData.products.find((p) => p.id === device.productId)?.schema,
|
|
296
344
|
},
|
|
297
345
|
|
|
298
346
|
store: {
|
|
@@ -321,6 +369,18 @@ export default class RoborockService {
|
|
|
321
369
|
homeData.products.forEach((p) => products.set(p.id, p.model));
|
|
322
370
|
const devices: Device[] = homeData.devices.length > 0 ? homeData.devices : homeData.receivedDevices;
|
|
323
371
|
|
|
372
|
+
if (homeData.rooms.length === 0) {
|
|
373
|
+
const homeDataV3 = await this.iotApi.getHomev3(homeid);
|
|
374
|
+
if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
|
|
375
|
+
homeData.rooms = homeDataV3.rooms;
|
|
376
|
+
} else {
|
|
377
|
+
const homeDataV1 = await this.iotApi.getHome(homeid);
|
|
378
|
+
if (homeDataV1 && homeDataV1.rooms && homeDataV1.rooms.length > 0) {
|
|
379
|
+
homeData.rooms = homeDataV1.rooms;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
324
384
|
const dvs = devices.map((device) => {
|
|
325
385
|
return {
|
|
326
386
|
...device,
|
|
@@ -334,6 +394,7 @@ export default class RoborockService {
|
|
|
334
394
|
model: homeData.products.find((p) => p.id === device.productId)?.model,
|
|
335
395
|
category: homeData.products.find((p) => p.id === device.productId)?.category,
|
|
336
396
|
batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100,
|
|
397
|
+
schema: homeData.products.find((p) => p.id === device.productId)?.schema,
|
|
337
398
|
},
|
|
338
399
|
|
|
339
400
|
store: {
|
|
@@ -361,13 +422,13 @@ export default class RoborockService {
|
|
|
361
422
|
return this.iotApi.startScene(sceneId);
|
|
362
423
|
}
|
|
363
424
|
|
|
364
|
-
public getRoomMappings(duid: string): Promise<number[][]
|
|
425
|
+
public getRoomMappings(duid: string): Promise<number[][] | undefined> {
|
|
365
426
|
if (!this.messageClient) {
|
|
366
427
|
this.logger.warn('messageClient not initialized. Waititing for next execution');
|
|
367
|
-
return undefined;
|
|
428
|
+
return Promise.resolve(undefined);
|
|
368
429
|
}
|
|
369
430
|
|
|
370
|
-
return this.messageClient.get(duid, new RequestMessage({ method: 'get_room_mapping' }));
|
|
431
|
+
return this.messageClient.get(duid, new RequestMessage({ method: 'get_room_mapping', secure: this.isRequestSecure(duid) }));
|
|
371
432
|
}
|
|
372
433
|
|
|
373
434
|
public async initializeMessageClient(username: string, device: Device, userdata: UserData): Promise<void> {
|
|
@@ -411,6 +472,7 @@ export default class RoborockService {
|
|
|
411
472
|
this.logger.error('messageClient not initialized');
|
|
412
473
|
return false;
|
|
413
474
|
}
|
|
475
|
+
|
|
414
476
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
415
477
|
const self = this;
|
|
416
478
|
|
|
@@ -439,6 +501,15 @@ export default class RoborockService {
|
|
|
439
501
|
|
|
440
502
|
this.messageProcessorMap.set(device.duid, messageProcessor);
|
|
441
503
|
|
|
504
|
+
this.logger.debug('Checking if device supports local connection', device.pv, device.data.model, device.duid);
|
|
505
|
+
if (device.pv === 'B01') {
|
|
506
|
+
this.logger.warn('Device does not support local connection', device.duid);
|
|
507
|
+
this.mqttAlwaysOnDevices.set(device.duid, true);
|
|
508
|
+
return true;
|
|
509
|
+
} else {
|
|
510
|
+
this.mqttAlwaysOnDevices.set(device.duid, false);
|
|
511
|
+
}
|
|
512
|
+
|
|
442
513
|
this.logger.debug('Local device', device.duid);
|
|
443
514
|
let localIp = this.ipMap.get(device.duid);
|
|
444
515
|
try {
|
|
@@ -457,7 +528,7 @@ export default class RoborockService {
|
|
|
457
528
|
|
|
458
529
|
if (localIp) {
|
|
459
530
|
this.logger.debug('initializing the local connection for this client towards ' + localIp);
|
|
460
|
-
const localClient = this.messageClient.registerClient(device.duid, localIp) as LocalNetworkClient;
|
|
531
|
+
const localClient = this.messageClient.registerClient(device.duid, localIp, this.onLocalClientDisconnect) as LocalNetworkClient;
|
|
461
532
|
localClient.connect();
|
|
462
533
|
|
|
463
534
|
let count = 0;
|
|
@@ -483,6 +554,10 @@ export default class RoborockService {
|
|
|
483
554
|
return true;
|
|
484
555
|
}
|
|
485
556
|
|
|
557
|
+
private onLocalClientDisconnect(duid: string): void {
|
|
558
|
+
this.mqttAlwaysOnDevices.set(duid, true);
|
|
559
|
+
}
|
|
560
|
+
|
|
486
561
|
private sleep(ms: number): Promise<void> {
|
|
487
562
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
488
563
|
}
|
|
@@ -492,4 +567,8 @@ export default class RoborockService {
|
|
|
492
567
|
this.iotApi = this.iotApiFactory(this.logger, userdata);
|
|
493
568
|
return userdata;
|
|
494
569
|
}
|
|
570
|
+
|
|
571
|
+
private isRequestSecure(duid: string): boolean {
|
|
572
|
+
return this.mqttAlwaysOnDevices.get(duid) ?? false;
|
|
573
|
+
}
|
|
495
574
|
}
|