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.
Files changed (51) hide show
  1. package/dist/platform.js +6 -4
  2. package/dist/roborockCommunication/RESTAPI/roborockIoTApi.js +22 -0
  3. package/dist/roborockCommunication/Zmodel/map.js +1 -0
  4. package/dist/roborockCommunication/Zmodel/mapInfo.js +26 -0
  5. package/dist/roborockCommunication/Zmodel/multipleMap.js +1 -0
  6. package/dist/roborockCommunication/broadcast/abstractClient.js +10 -2
  7. package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +22 -2
  8. package/dist/roborockCommunication/broadcast/client/MQTTClient.js +4 -0
  9. package/dist/roborockCommunication/broadcast/clientRouter.js +2 -2
  10. package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js +9 -2
  11. package/dist/roborockCommunication/broadcast/listener/implementation/syncMessageListener.js +3 -2
  12. package/dist/roborockCommunication/broadcast/messageProcessor.js +6 -3
  13. package/dist/roborockCommunication/helper/messageDeserializer.js +9 -3
  14. package/dist/roborockCommunication/helper/messageSerializer.js +6 -0
  15. package/dist/roborockCommunication/index.js +1 -0
  16. package/dist/roborockService.js +84 -13
  17. package/eslint.config.js +1 -1
  18. package/matterbridge-roborock-vacuum-plugin.config.json +1 -1
  19. package/matterbridge-roborock-vacuum-plugin.schema.json +1 -1
  20. package/package.json +1 -1
  21. package/src/platform.ts +7 -5
  22. package/src/roborockCommunication/RESTAPI/roborockIoTApi.ts +25 -1
  23. package/src/roborockCommunication/Zmodel/device.ts +1 -0
  24. package/src/roborockCommunication/Zmodel/map.ts +14 -0
  25. package/src/roborockCommunication/Zmodel/mapInfo.ts +34 -0
  26. package/src/roborockCommunication/Zmodel/multipleMap.ts +8 -0
  27. package/src/roborockCommunication/Zmodel/userData.ts +0 -3
  28. package/src/roborockCommunication/broadcast/abstractClient.ts +13 -4
  29. package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +26 -2
  30. package/src/roborockCommunication/broadcast/client/MQTTClient.ts +4 -0
  31. package/src/roborockCommunication/broadcast/client.ts +1 -1
  32. package/src/roborockCommunication/broadcast/clientRouter.ts +3 -3
  33. package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts +10 -2
  34. package/src/roborockCommunication/broadcast/listener/implementation/syncMessageListener.ts +4 -3
  35. package/src/roborockCommunication/broadcast/messageProcessor.ts +9 -5
  36. package/src/roborockCommunication/helper/messageDeserializer.ts +9 -3
  37. package/src/roborockCommunication/helper/messageSerializer.ts +6 -0
  38. package/src/roborockCommunication/index.ts +2 -0
  39. package/src/roborockService.ts +96 -17
  40. package/src/tests/roborockCommunication/broadcast/listener/implementation/syncMessageListener.test.ts +5 -4
  41. package/src/tests/roborockService.test.ts +3 -11
  42. package/web-for-testing/README.md +47 -0
  43. package/web-for-testing/nodemon.json +7 -0
  44. package/web-for-testing/package-lock.json +6598 -0
  45. package/web-for-testing/package.json +36 -0
  46. package/web-for-testing/src/accountStore.ts +8 -0
  47. package/web-for-testing/src/app.ts +194 -0
  48. package/web-for-testing/tsconfig-ext.json +19 -0
  49. package/web-for-testing/tsconfig.json +23 -0
  50. package/web-for-testing/views/index.ejs +172 -0
  51. package/web-for-testing/watch.mjs +93 -0
@@ -36,6 +36,7 @@ export interface Device {
36
36
  model: string;
37
37
  category: string;
38
38
  batteryLevel: number;
39
+ // schema: DeviceSchema[];
39
40
  };
40
41
 
41
42
  scenes: Scene[];
@@ -0,0 +1,14 @@
1
+ export interface Map {
2
+ mapFlag: number;
3
+ add_time: number;
4
+ length: number;
5
+ name: string;
6
+ rooms: RoomInformation[];
7
+ }
8
+
9
+ export interface RoomInformation {
10
+ id: number;
11
+ tag: number;
12
+ iot_name_id: string;
13
+ iot_name: string;
14
+ }
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ import { Map } from './map.js';
2
+
3
+ export interface MultipleMap {
4
+ max_multi_map: number;
5
+ max_bak_map: number;
6
+ multi_map_count: number;
7
+ map_info: Map[];
8
+ }
@@ -7,9 +7,6 @@ export interface UserData {
7
7
  country: string;
8
8
  nickname: string;
9
9
  rriot: Rriot;
10
-
11
- // For caching purposes
12
- baseUrl: string;
13
10
  }
14
11
 
15
12
  export interface Rriot {
@@ -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
- public constructor(logger: AnsiLogger, context: MessageContext, duid: string, ip: string) {
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.onDisconnect.bind(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 {
@@ -15,5 +15,5 @@ export interface Client {
15
15
 
16
16
  send(duid: string, request: RequestMessage): Promise<void>;
17
17
 
18
- get<T>(duid: string, request: RequestMessage): Promise<T>;
18
+ get<T>(duid: string, request: RequestMessage): Promise<T | undefined>;
19
19
  }
@@ -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 {
@@ -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.logger.notice(`Device ${duid} disconnected from ${this.clientName}`);
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
- this.logger?.debug('Device status: ', debugStringify(response));
47
- return new DeviceStatus(response);
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 (version !== '1.0' && version !== 'A01') {
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.deserializeProtocolRpcResponse(duid, data);
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 deserializeProtocolRpcResponse(duid: string, data: Message): ResponseMessage {
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';
@@ -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, method: string): Promise<unknown> {
193
- this.logger.debug('RoborockService - customSend-message', method);
194
- return this.getMessageProcessor(duid)?.getCustomMessage(duid, new RequestMessage({ method }));
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 async activateDeviceNotify(device: Device): Promise<void> {
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[][]> | undefined {
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
  }