matterbridge-roborock-vacuum-plugin 1.0.5

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 (170) hide show
  1. package/.github/workflows/publish.yml +34 -0
  2. package/.tarignore +5 -0
  3. package/LICENSE +202 -0
  4. package/README.md +34 -0
  5. package/README_DEV.md +69 -0
  6. package/bmc-button.svg +22 -0
  7. package/dist/appliances.js +6 -0
  8. package/dist/behaviorFactory.js +18 -0
  9. package/dist/behaviors/BehaviorDeviceGeneric.js +31 -0
  10. package/dist/behaviors/roborock.vacuum/QREVO_EDGE_5V1/a187.js +94 -0
  11. package/dist/behaviors/roborock.vacuum/QREVO_EDGE_5V1/initalData.js +81 -0
  12. package/dist/behaviors/roborock.vacuum/QREVO_EDGE_5V1/runtimes.js +39 -0
  13. package/dist/behaviors/roborock.vacuum/default/default.js +58 -0
  14. package/dist/behaviors/roborock.vacuum/default/initalData.js +66 -0
  15. package/dist/clientManager.js +17 -0
  16. package/dist/extensions/AxiosStaticExtensions.js +22 -0
  17. package/dist/extensions/index.js +1 -0
  18. package/dist/helper.js +16 -0
  19. package/dist/index.js +4 -0
  20. package/dist/initialData/getBatteryStatus.js +24 -0
  21. package/dist/initialData/getOperationalStates.js +22 -0
  22. package/dist/initialData/getSupportedAreas.js +69 -0
  23. package/dist/initialData/getSupportedCleanModes.js +11 -0
  24. package/dist/initialData/getSupportedRunModes.js +18 -0
  25. package/dist/initialData/index.js +5 -0
  26. package/dist/model/CloudMessageModel.js +1 -0
  27. package/dist/model/DockingStationStatus.js +24 -0
  28. package/dist/model/RoomMap.js +18 -0
  29. package/dist/notifyMessageTypes.js +9 -0
  30. package/dist/platform.js +146 -0
  31. package/dist/platformRunner.js +249 -0
  32. package/dist/roborockCommunication/RESTAPI/roborockAuthenticateApi.js +73 -0
  33. package/dist/roborockCommunication/RESTAPI/roborockIoTApi.js +61 -0
  34. package/dist/roborockCommunication/Zenum/dockType.js +4 -0
  35. package/dist/roborockCommunication/Zenum/operationStatusCode.js +44 -0
  36. package/dist/roborockCommunication/Zenum/vacuumAndDockErrorCode.js +68 -0
  37. package/dist/roborockCommunication/Zmodel/apiResponse.js +1 -0
  38. package/dist/roborockCommunication/Zmodel/authenticateResponse.js +1 -0
  39. package/dist/roborockCommunication/Zmodel/baseURL.js +1 -0
  40. package/dist/roborockCommunication/Zmodel/device.js +1 -0
  41. package/dist/roborockCommunication/Zmodel/deviceModel.js +28 -0
  42. package/dist/roborockCommunication/Zmodel/deviceSchema.js +1 -0
  43. package/dist/roborockCommunication/Zmodel/deviceStatus.js +22 -0
  44. package/dist/roborockCommunication/Zmodel/dockInfo.js +6 -0
  45. package/dist/roborockCommunication/Zmodel/home.js +1 -0
  46. package/dist/roborockCommunication/Zmodel/homeInfo.js +1 -0
  47. package/dist/roborockCommunication/Zmodel/messageResult.js +7 -0
  48. package/dist/roborockCommunication/Zmodel/networkInfo.js +1 -0
  49. package/dist/roborockCommunication/Zmodel/product.js +1 -0
  50. package/dist/roborockCommunication/Zmodel/room.js +1 -0
  51. package/dist/roborockCommunication/Zmodel/roomInfo.js +22 -0
  52. package/dist/roborockCommunication/Zmodel/userData.js +1 -0
  53. package/dist/roborockCommunication/Zmodel/vacuumError.js +27 -0
  54. package/dist/roborockCommunication/broadcast/abstractClient.js +41 -0
  55. package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +148 -0
  56. package/dist/roborockCommunication/broadcast/client/MQTTClient.js +101 -0
  57. package/dist/roborockCommunication/broadcast/client.js +1 -0
  58. package/dist/roborockCommunication/broadcast/clientRouter.js +79 -0
  59. package/dist/roborockCommunication/broadcast/listener/abstractConnectionListener.js +1 -0
  60. package/dist/roborockCommunication/broadcast/listener/abstractMessageHandler.js +1 -0
  61. package/dist/roborockCommunication/broadcast/listener/abstractMessageListener.js +1 -0
  62. package/dist/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.js +21 -0
  63. package/dist/roborockCommunication/broadcast/listener/implementation/chainedMessageListener.js +11 -0
  64. package/dist/roborockCommunication/broadcast/listener/implementation/simpleMessageListener.js +27 -0
  65. package/dist/roborockCommunication/broadcast/listener/implementation/syncMessageListener.js +35 -0
  66. package/dist/roborockCommunication/broadcast/listener/index.js +1 -0
  67. package/dist/roborockCommunication/broadcast/messageProcessor.js +79 -0
  68. package/dist/roborockCommunication/broadcast/model/dps.js +1 -0
  69. package/dist/roborockCommunication/broadcast/model/messageContext.js +26 -0
  70. package/dist/roborockCommunication/broadcast/model/protocol.js +19 -0
  71. package/dist/roborockCommunication/broadcast/model/requestMessage.js +33 -0
  72. package/dist/roborockCommunication/broadcast/model/responseMessage.js +14 -0
  73. package/dist/roborockCommunication/helper/chunkBuffer.js +17 -0
  74. package/dist/roborockCommunication/helper/cryptoHelper.js +27 -0
  75. package/dist/roborockCommunication/helper/messageDeserializer.js +74 -0
  76. package/dist/roborockCommunication/helper/messageSerializer.js +70 -0
  77. package/dist/roborockCommunication/helper/nameDecoder.js +66 -0
  78. package/dist/roborockCommunication/helper/sequence.js +16 -0
  79. package/dist/roborockCommunication/index.js +9 -0
  80. package/dist/roborockService.js +300 -0
  81. package/dist/rvc.js +39 -0
  82. package/dist/settings.js +1 -0
  83. package/dist/share/function.js +96 -0
  84. package/dist/share/runtimeHelper.js +29 -0
  85. package/eslint.config.js +76 -0
  86. package/matterbridge-roborock-vacuum-plugin.config.json +11 -0
  87. package/matterbridge-roborock-vacuum-plugin.schema.json +57 -0
  88. package/package.json +43 -0
  89. package/prettier.config.js +49 -0
  90. package/src/appliances.ts +15 -0
  91. package/src/behaviorFactory.ts +24 -0
  92. package/src/behaviors/BehaviorDeviceGeneric.ts +39 -0
  93. package/src/behaviors/roborock.vacuum/QREVO_EDGE_5V1/a187.ts +117 -0
  94. package/src/behaviors/roborock.vacuum/QREVO_EDGE_5V1/initalData.ts +85 -0
  95. package/src/behaviors/roborock.vacuum/QREVO_EDGE_5V1/runtimes.ts +29 -0
  96. package/src/behaviors/roborock.vacuum/default/default.ts +78 -0
  97. package/src/behaviors/roborock.vacuum/default/initalData.ts +69 -0
  98. package/src/clientManager.ts +23 -0
  99. package/src/extensions/AxiosStaticExtensions.ts +35 -0
  100. package/src/extensions/index.ts +1 -0
  101. package/src/helper.ts +22 -0
  102. package/src/index.ts +16 -0
  103. package/src/initialData/getBatteryStatus.ts +26 -0
  104. package/src/initialData/getOperationalStates.ts +24 -0
  105. package/src/initialData/getSupportedAreas.ts +95 -0
  106. package/src/initialData/getSupportedCleanModes.ts +13 -0
  107. package/src/initialData/getSupportedRunModes.ts +21 -0
  108. package/src/initialData/index.ts +5 -0
  109. package/src/model/CloudMessageModel.ts +13 -0
  110. package/src/model/DockingStationStatus.ts +37 -0
  111. package/src/model/RoomMap.ts +44 -0
  112. package/src/notifyMessageTypes.ts +8 -0
  113. package/src/platform.ts +192 -0
  114. package/src/platformRunner.ts +292 -0
  115. package/src/roborockCommunication/RESTAPI/roborockAuthenticateApi.ts +98 -0
  116. package/src/roborockCommunication/RESTAPI/roborockIoTApi.ts +70 -0
  117. package/src/roborockCommunication/Zenum/dockType.ts +3 -0
  118. package/src/roborockCommunication/Zenum/operationStatusCode.ts +43 -0
  119. package/src/roborockCommunication/Zenum/vacuumAndDockErrorCode.ts +68 -0
  120. package/src/roborockCommunication/Zmodel/apiResponse.ts +3 -0
  121. package/src/roborockCommunication/Zmodel/authenticateResponse.ts +5 -0
  122. package/src/roborockCommunication/Zmodel/baseURL.ts +5 -0
  123. package/src/roborockCommunication/Zmodel/device.ts +39 -0
  124. package/src/roborockCommunication/Zmodel/deviceModel.ts +27 -0
  125. package/src/roborockCommunication/Zmodel/deviceSchema.ts +8 -0
  126. package/src/roborockCommunication/Zmodel/deviceStatus.ts +30 -0
  127. package/src/roborockCommunication/Zmodel/dockInfo.ts +9 -0
  128. package/src/roborockCommunication/Zmodel/home.ts +13 -0
  129. package/src/roborockCommunication/Zmodel/homeInfo.ts +5 -0
  130. package/src/roborockCommunication/Zmodel/messageResult.ts +72 -0
  131. package/src/roborockCommunication/Zmodel/networkInfo.ts +7 -0
  132. package/src/roborockCommunication/Zmodel/product.ts +9 -0
  133. package/src/roborockCommunication/Zmodel/room.ts +4 -0
  134. package/src/roborockCommunication/Zmodel/roomInfo.ts +25 -0
  135. package/src/roborockCommunication/Zmodel/userData.ts +23 -0
  136. package/src/roborockCommunication/Zmodel/vacuumError.ts +35 -0
  137. package/src/roborockCommunication/broadcast/abstractClient.ts +61 -0
  138. package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +177 -0
  139. package/src/roborockCommunication/broadcast/client/MQTTClient.ts +129 -0
  140. package/src/roborockCommunication/broadcast/client.ts +19 -0
  141. package/src/roborockCommunication/broadcast/clientRouter.ts +100 -0
  142. package/src/roborockCommunication/broadcast/listener/abstractConnectionListener.ts +5 -0
  143. package/src/roborockCommunication/broadcast/listener/abstractMessageHandler.ts +11 -0
  144. package/src/roborockCommunication/broadcast/listener/abstractMessageListener.ts +5 -0
  145. package/src/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.ts +26 -0
  146. package/src/roborockCommunication/broadcast/listener/implementation/chainedMessageListener.ts +16 -0
  147. package/src/roborockCommunication/broadcast/listener/implementation/simpleMessageListener.ts +37 -0
  148. package/src/roborockCommunication/broadcast/listener/implementation/syncMessageListener.ts +48 -0
  149. package/src/roborockCommunication/broadcast/listener/index.ts +3 -0
  150. package/src/roborockCommunication/broadcast/messageProcessor.ts +110 -0
  151. package/src/roborockCommunication/broadcast/model/dps.ts +17 -0
  152. package/src/roborockCommunication/broadcast/model/messageContext.ts +34 -0
  153. package/src/roborockCommunication/broadcast/model/protocol.ts +19 -0
  154. package/src/roborockCommunication/broadcast/model/requestMessage.ts +44 -0
  155. package/src/roborockCommunication/broadcast/model/responseMessage.ts +19 -0
  156. package/src/roborockCommunication/helper/chunkBuffer.ts +18 -0
  157. package/src/roborockCommunication/helper/cryptoHelper.ts +34 -0
  158. package/src/roborockCommunication/helper/messageDeserializer.ts +99 -0
  159. package/src/roborockCommunication/helper/messageSerializer.ts +82 -0
  160. package/src/roborockCommunication/helper/nameDecoder.ts +78 -0
  161. package/src/roborockCommunication/helper/sequence.ts +18 -0
  162. package/src/roborockCommunication/index.ts +15 -0
  163. package/src/roborockService.ts +379 -0
  164. package/src/rvc.ts +66 -0
  165. package/src/settings.ts +1 -0
  166. package/src/share/function.ts +106 -0
  167. package/src/share/runtimeHelper.ts +35 -0
  168. package/tsconfig.json +37 -0
  169. package/tsconfig.production.json +19 -0
  170. package/tslint.json +9 -0
@@ -0,0 +1,30 @@
1
+ import { VacuumErrorCode, DockErrorCode } from '../Zenum/vacuumAndDockErrorCode.js';
2
+ import { DockInfo } from './dockInfo.js';
3
+ import { CloudMessageResult } from './messageResult.js';
4
+ import { VacuumError } from './vacuumError.js';
5
+
6
+ export class DeviceStatus {
7
+ errorStatus: VacuumError;
8
+ private readonly message: CloudMessageResult;
9
+
10
+ constructor(message: CloudMessageResult) {
11
+ this.message = message;
12
+ this.errorStatus = new VacuumError(message.error_code, message.dock_error_status);
13
+ }
14
+
15
+ getBattery(): number {
16
+ return this.message.battery;
17
+ }
18
+
19
+ getVacuumErrorCode(): VacuumErrorCode {
20
+ return this.message.error_code;
21
+ }
22
+
23
+ getDockInfo(): DockInfo {
24
+ return new DockInfo(this.message.dock_type);
25
+ }
26
+
27
+ getDockErrorCode(): DockErrorCode {
28
+ return this.message.dock_error_status;
29
+ }
30
+ }
@@ -0,0 +1,9 @@
1
+ import { DockType } from '../Zenum/dockType.js';
2
+
3
+ export class DockInfo {
4
+ private readonly dockType: DockType;
5
+
6
+ constructor(dockType: DockType) {
7
+ this.dockType = dockType;
8
+ }
9
+ }
@@ -0,0 +1,13 @@
1
+ import { Device } from './device.js';
2
+ import { Product } from './product.js';
3
+ import { Room } from './room.js';
4
+
5
+ export interface Home {
6
+ id: number;
7
+ name: string;
8
+
9
+ products: Product[];
10
+ devices: Device[];
11
+ receivedDevices: Device[];
12
+ rooms: Room[];
13
+ }
@@ -0,0 +1,5 @@
1
+ export interface HomeInfo {
2
+ id: number;
3
+ name: string;
4
+ rrHomeId: number;
5
+ }
@@ -0,0 +1,72 @@
1
+ export interface CloudMessageResult {
2
+ msg_ver: number;
3
+ msg_seq: number;
4
+ state: number;
5
+ battery: number;
6
+ clean_time: number;
7
+ clean_area: number;
8
+ error_code: number;
9
+ map_present: number;
10
+ in_cleaning: number;
11
+ in_returning: number;
12
+ in_fresh_state: number;
13
+ lab_status: number;
14
+ water_box_status: number;
15
+ back_type: number;
16
+ wash_phase: number;
17
+ wash_ready: number;
18
+ wash_status: number;
19
+ fan_power: number;
20
+ dnd_enabled: number;
21
+ map_status: number;
22
+ is_locating: number;
23
+ lock_status: number;
24
+ water_box_mode: number;
25
+ distance_off: number;
26
+ water_box_carriage_status: number;
27
+ mop_forbidden_enable: number;
28
+ camera_status: number;
29
+ is_exploring: number;
30
+ adbumper_status: number[];
31
+ water_shortage_status: number;
32
+ dock_type: number;
33
+ dust_collection_status: number;
34
+ auto_dust_collection: number;
35
+ avoid_count: number;
36
+ mop_mode: number;
37
+ debug_mode: number;
38
+ in_warmup: number;
39
+ collision_avoid_status: number;
40
+ switch_map_mode: number;
41
+ dock_error_status: number;
42
+ charge_status: number;
43
+ unsave_map_reason: number;
44
+ unsave_map_flag: number;
45
+ dry_status: number;
46
+ rdt: number;
47
+ clean_percent: number;
48
+ extra_time: number;
49
+ rss: number;
50
+ dss: number;
51
+ common_status: number;
52
+ last_clean_t: number;
53
+ replenish_mode: number;
54
+ repeat: number;
55
+ kct: number;
56
+ subdivision_sets: number;
57
+ cleaning_info: {
58
+ target_segment_id: number;
59
+ segment_id: number;
60
+ fan_power: number;
61
+ water_box_status: number;
62
+ mop_mode: number;
63
+ };
64
+ exit_dock: number;
65
+ }
66
+
67
+ export enum CarpetCleanMode {
68
+ Avoid = 0,
69
+ Ignore = 2,
70
+ Cross = 3,
71
+ DynamicLift = 200, //TODO
72
+ }
@@ -0,0 +1,7 @@
1
+ export interface NetworkInfo {
2
+ ssid: string;
3
+ ip: string;
4
+ mac: string;
5
+ bssid: string;
6
+ rssi: number;
7
+ }
@@ -0,0 +1,9 @@
1
+ import { DeviceSchema } from './deviceSchema.js';
2
+
3
+ export interface Product {
4
+ id: string;
5
+ name: string;
6
+ model: string;
7
+ category: string;
8
+ schema: DeviceSchema[];
9
+ }
@@ -0,0 +1,4 @@
1
+ export interface Room {
2
+ id: number; //this is global id
3
+ name: string;
4
+ }
@@ -0,0 +1,25 @@
1
+ import decodeComponent from '../helper/nameDecoder.js';
2
+ import { Room } from './room.js';
3
+
4
+ export class RoomInfo {
5
+ readonly rooms: { id: number; name: string | undefined }[] = [];
6
+
7
+ constructor(roomInfo: Room[], roomData: number[][]) {
8
+ this.rooms = roomData
9
+ .map((entry) => {
10
+ return { id: entry[0], globalId: entry[1] };
11
+ })
12
+ .map((entry) => {
13
+ return {
14
+ id: entry.id,
15
+ room: roomInfo.find((el) => el.id == entry.globalId),
16
+ };
17
+ })
18
+ .map((entry) => {
19
+ return {
20
+ id: entry.id,
21
+ name: decodeComponent(entry.room?.name)?.toLowerCase(),
22
+ };
23
+ });
24
+ }
25
+ }
@@ -0,0 +1,23 @@
1
+ export interface UserData {
2
+ uid: string;
3
+ token: string;
4
+ rruid: string;
5
+ region: string;
6
+ countrycode: string;
7
+ country: string;
8
+ nickname: string;
9
+ rriot: Rriot;
10
+ }
11
+
12
+ export interface Rriot {
13
+ u: string;
14
+ s: string;
15
+ h: string;
16
+ k: string;
17
+ r: {
18
+ r: string;
19
+ a: string;
20
+ m: string;
21
+ l: string;
22
+ };
23
+ }
@@ -0,0 +1,35 @@
1
+ import { DockErrorCode, VacuumErrorCode } from '../Zenum/vacuumAndDockErrorCode.js';
2
+
3
+ export class VacuumError {
4
+ private readonly vacuumErrorCode: VacuumErrorCode;
5
+ private readonly dockErrorCode: DockErrorCode;
6
+
7
+ constructor(errorCode: VacuumErrorCode, dockErrorCode: DockErrorCode) {
8
+ this.vacuumErrorCode = errorCode;
9
+ this.dockErrorCode = dockErrorCode;
10
+ }
11
+
12
+ hasError() {
13
+ return this.vacuumErrorCode != 0 || this.dockErrorCode != 0;
14
+ }
15
+
16
+ isStuck(): boolean {
17
+ return this.vacuumErrorCode === VacuumErrorCode.RobotTrapped;
18
+ }
19
+
20
+ isBatteryLow(): boolean {
21
+ return this.vacuumErrorCode === VacuumErrorCode.LowBattery;
22
+ }
23
+
24
+ isBinFull(): boolean {
25
+ return this.vacuumErrorCode === VacuumErrorCode.CleanAutoEmptyDock || this.dockErrorCode == DockErrorCode.DuctBlockage;
26
+ }
27
+
28
+ isCleanWaterEmpty(): boolean {
29
+ return this.vacuumErrorCode === VacuumErrorCode.ClearWaterTankEmpty || this.dockErrorCode == DockErrorCode.WaterEmpty;
30
+ }
31
+
32
+ isWasteWaterFull(): boolean {
33
+ return this.dockErrorCode == DockErrorCode.WasteWaterTankFull;
34
+ }
35
+ }
@@ -0,0 +1,61 @@
1
+ import { AnsiLogger } from 'matterbridge/logger';
2
+ import { MessageDeserializer } from '../helper/messageDeserializer.js';
3
+ import { MessageSerializer } from '../helper/messageSerializer.js';
4
+ import { AbstractConnectionListener } from './listener/abstractConnectionListener.js';
5
+ import { AbstractMessageListener } from './listener/abstractMessageListener.js';
6
+ import { RequestMessage } from './model/requestMessage.js';
7
+ import { Client } from './client.js';
8
+ import { MessageContext } from './model/messageContext.js';
9
+ import { ChainedConnectionListener } from './listener/implementation/chainedConnectionListener.js';
10
+ import { ChainedMessageListener } from './listener/implementation/chainedMessageListener.js';
11
+ import { SyncMessageListener } from './listener/implementation/syncMessageListener.js';
12
+
13
+ export abstract class AbstractClient implements Client {
14
+ protected readonly connectionListeners = new ChainedConnectionListener();
15
+ protected readonly messageListeners = new ChainedMessageListener();
16
+
17
+ protected connected: boolean = false;
18
+
19
+ private readonly context: MessageContext;
20
+ protected readonly serializer: MessageSerializer;
21
+ protected readonly deserializer: MessageDeserializer;
22
+ private readonly syncMessageListener: SyncMessageListener;
23
+ protected logger: AnsiLogger;
24
+
25
+ protected constructor(logger: AnsiLogger, context: MessageContext) {
26
+ this.context = context;
27
+ this.serializer = new MessageSerializer(this.context);
28
+ this.deserializer = new MessageDeserializer(this.context);
29
+
30
+ this.syncMessageListener = new SyncMessageListener(logger);
31
+ this.messageListeners.register(this.syncMessageListener);
32
+ this.logger = logger;
33
+ }
34
+
35
+ abstract connect(): void;
36
+ abstract disconnect(): Promise<void>;
37
+ abstract send(duid: string, request: RequestMessage): Promise<void>;
38
+
39
+ public async get<T>(duid: string, request: RequestMessage): Promise<T> {
40
+ return new Promise<any>((resolve, reject) => {
41
+ this.syncMessageListener.waitFor(request.messageId, resolve, reject);
42
+ this.send(duid, request);
43
+ });
44
+ }
45
+
46
+ public registerDevice(duid: string, localKey: string, pv: string): void {
47
+ this.context.registerDevice(duid, localKey, pv);
48
+ }
49
+
50
+ public registerConnectionListener(listener: AbstractConnectionListener): void {
51
+ this.connectionListeners.register(listener);
52
+ }
53
+
54
+ public registerMessageListener(listener: AbstractMessageListener): void {
55
+ this.messageListeners.register(listener);
56
+ }
57
+
58
+ public isConnected() {
59
+ return this.connected;
60
+ }
61
+ }
@@ -0,0 +1,177 @@
1
+ import { Socket } from 'node:net';
2
+ import { clearInterval } from 'node:timers';
3
+ import { Protocol } from '../model/protocol.js';
4
+ import { RequestMessage } from '../model/requestMessage.js';
5
+ import { AnsiLogger } from 'matterbridge/logger';
6
+ import { AbstractClient } from '../abstractClient.js';
7
+ import { MessageContext } from '../model/messageContext.js';
8
+ import { Sequence } from '../../helper/sequence.js';
9
+ import { ChunkBuffer } from '../../helper/chunkBuffer.js';
10
+
11
+ export class LocalNetworkClient extends AbstractClient {
12
+ private socket: Socket | undefined = undefined;
13
+ private buffer: ChunkBuffer = new ChunkBuffer();
14
+ private messageIdSeq: Sequence;
15
+ private pingInterval?: NodeJS.Timeout;
16
+ duid: string;
17
+ ip: string;
18
+
19
+ public constructor(logger: AnsiLogger, context: MessageContext, duid: string, ip: string) {
20
+ super(logger, context);
21
+ this.duid = duid;
22
+ this.ip = ip;
23
+ this.messageIdSeq = new Sequence(100000, 999999);
24
+ }
25
+
26
+ public connect(): void {
27
+ this.socket = new Socket();
28
+ this.socket.on('close', this.onDisconnect.bind(this));
29
+ this.socket.on('end', this.onDisconnect.bind(this));
30
+ this.socket.on('error', this.onError.bind(this));
31
+ this.socket.on('data', this.onMessage.bind(this));
32
+ this.socket.connect(58867, this.ip, this.onConnect.bind(this));
33
+ }
34
+
35
+ public async disconnect(): Promise<void> {
36
+ if (!this.socket) {
37
+ return;
38
+ }
39
+
40
+ if (this.pingInterval) {
41
+ clearInterval(this.pingInterval);
42
+ }
43
+ this.socket.destroy();
44
+ this.socket = undefined;
45
+ }
46
+
47
+ public async send(duid: string, request: RequestMessage): Promise<void> {
48
+ if (!this.socket || !this.connected) {
49
+ this.logger.error(`failed to send request to ${duid}, socket is not online, ${JSON.stringify(request)}`);
50
+ return;
51
+ }
52
+
53
+ const localRequest = request.toLocalRequest();
54
+ const message = this.serializer.serialize(duid, localRequest);
55
+
56
+ this.logger.debug(`sending message ${message.messageId}, protocol:${localRequest.protocol}, method:${localRequest.method}, secure:${request.secure} to ${duid}`);
57
+ this.socket.write(this.wrapWithLengthData(message.buffer));
58
+ }
59
+
60
+ private async onConnect(): Promise<void> {
61
+ this.connected = true;
62
+ const address = this.socket?.address();
63
+ this.logger.debug(`${this.duid} connected to ${this.ip}, address: ${JSON.stringify(address)}`);
64
+ await this.sendHelloMessage();
65
+ this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
66
+ await this.connectionListeners.onConnected();
67
+ }
68
+
69
+ private async onDisconnect(): Promise<void> {
70
+ this.logger.info('Socket has disconnected.');
71
+ this.connected = false;
72
+
73
+ if (this.socket) {
74
+ this.socket.destroy();
75
+ this.socket = undefined;
76
+ }
77
+ if (this.pingInterval) {
78
+ clearInterval(this.pingInterval);
79
+ }
80
+
81
+ await this.connectionListeners.onDisconnected();
82
+ }
83
+
84
+ private async onError(result: any): Promise<void> {
85
+ this.logger.error('Socket connection error: ' + result);
86
+ this.connected = false;
87
+
88
+ if (this.socket) {
89
+ this.socket.destroy();
90
+ this.socket = undefined;
91
+ }
92
+
93
+ await this.connectionListeners.onError(result.toString());
94
+ }
95
+
96
+ private async onMessage(message: Buffer): Promise<void> {
97
+ if (!this.socket) {
98
+ this.logger.error('unable to receive data if there is no socket available');
99
+ return;
100
+ }
101
+
102
+ if (!message || message.length == 0) {
103
+ this.logger.debug('LocalNetworkClient received empty message from socket.');
104
+ return;
105
+ }
106
+
107
+ try {
108
+ this.buffer.append(message);
109
+
110
+ const recvBuffer = this.buffer.get();
111
+ if (!this.isMessageComplete(recvBuffer)) {
112
+ return;
113
+ }
114
+ this.buffer.reset();
115
+
116
+ let offset = 0;
117
+ while (offset + 4 <= recvBuffer.length) {
118
+ const segmentLength = recvBuffer.readUInt32BE(offset);
119
+ if (segmentLength == 17) {
120
+ offset += 4 + segmentLength;
121
+ continue;
122
+ }
123
+
124
+ try {
125
+ const currentBuffer = recvBuffer.subarray(offset + 4, offset + segmentLength + 4);
126
+ const response = this.deserializer.deserialize(this.duid, currentBuffer);
127
+ await this.messageListeners.onMessage(response);
128
+ } catch (error) {
129
+ this.logger.error('unable to process message: ' + error);
130
+ }
131
+ offset += 4 + segmentLength;
132
+ }
133
+ } catch (error) {
134
+ this.logger.error('failed to read from a socket: ' + error);
135
+ }
136
+ }
137
+
138
+ private isMessageComplete(buffer: Buffer): boolean {
139
+ let totalLength = 0;
140
+ let offset = 0;
141
+
142
+ while (offset + 4 <= buffer.length) {
143
+ const segmentLength = buffer.readUInt32BE(offset);
144
+ totalLength += 4 + segmentLength;
145
+ offset += 4 + segmentLength;
146
+
147
+ if (offset > buffer.length) {
148
+ return false; // Data is not complete yet
149
+ }
150
+ }
151
+
152
+ return totalLength <= buffer.length;
153
+ }
154
+
155
+ private wrapWithLengthData(buffer: Buffer<ArrayBufferLike>): Buffer<ArrayBufferLike> {
156
+ const lengthBuffer = Buffer.alloc(4);
157
+ lengthBuffer.writeUInt32BE(buffer.length, 0);
158
+ return Buffer.concat([lengthBuffer, buffer]);
159
+ }
160
+
161
+ private async sendHelloMessage(): Promise<void> {
162
+ const request = new RequestMessage({
163
+ protocol: Protocol.hello_request,
164
+ messageId: this.messageIdSeq.next(),
165
+ });
166
+
167
+ await this.send(this.duid, request);
168
+ }
169
+
170
+ private async sendPingRequest(): Promise<void> {
171
+ const request = new RequestMessage({
172
+ protocol: Protocol.ping_request,
173
+ messageId: this.messageIdSeq.next(),
174
+ });
175
+ await this.send(this.duid, request);
176
+ }
177
+ }
@@ -0,0 +1,129 @@
1
+ import mqtt, { ErrorWithReasonCode, IConnackPacket, ISubscriptionGrant, MqttClient as MqttLibClient } from 'mqtt';
2
+ import { CryptoUtils } from '../../helper/cryptoHelper.js';
3
+ import { RequestMessage } from '../model/requestMessage.js';
4
+ import { AbstractClient } from '../abstractClient.js';
5
+ import { MessageContext } from '../model/messageContext.js';
6
+ import { Rriot, UserData } from '../../Zmodel/userData.js';
7
+ import { AnsiLogger } from 'matterbridge/logger';
8
+
9
+ export class MQTTClient extends AbstractClient {
10
+ private readonly rriot: Rriot;
11
+ private readonly mqttUsername: string;
12
+ private readonly mqttPassword: string;
13
+ private client: MqttLibClient | undefined = undefined;
14
+
15
+ public constructor(logger: AnsiLogger, context: MessageContext, userdata: UserData) {
16
+ super(logger, context);
17
+ this.rriot = userdata.rriot;
18
+
19
+ this.mqttUsername = CryptoUtils.md5hex(userdata.rriot.u + ':' + userdata.rriot.k).substring(2, 10);
20
+ this.mqttPassword = CryptoUtils.md5hex(userdata.rriot.s + ':' + userdata.rriot.k).substring(16);
21
+ }
22
+
23
+ public connect(): void {
24
+ if (this.client) {
25
+ return;
26
+ }
27
+
28
+ this.client = mqtt.connect(this.rriot.r.m, {
29
+ clientId: this.mqttUsername,
30
+ username: this.mqttUsername,
31
+ password: this.mqttPassword,
32
+ keepalive: 30,
33
+ log: (...args: any[]) => {
34
+ //this.logger.debug('MQTTClient args:: ' + args[0]);
35
+ },
36
+ });
37
+
38
+ this.client.on('connect', this.onConnect.bind(this));
39
+ this.client.on('error', this.onError.bind(this));
40
+ this.client.on('reconnect', this.onReconnect.bind(this));
41
+ this.client.on('close', this.onDisconnect.bind(this));
42
+ this.client.on('disconnect', this.onDisconnect.bind(this));
43
+ this.client.on('offline', this.onDisconnect.bind(this));
44
+
45
+ // message events
46
+ this.client.on('message', this.onMessage.bind(this));
47
+ }
48
+
49
+ public async disconnect(): Promise<void> {
50
+ if (!this.client || !this.connected) {
51
+ return;
52
+ }
53
+ try {
54
+ this.client.end();
55
+ } catch (error) {
56
+ this.logger.error('MQTT client failed to disconnect with error: ' + error);
57
+ }
58
+ }
59
+
60
+ public async send(duid: string, request: RequestMessage): Promise<void> {
61
+ if (!this.client || !this.connected) {
62
+ return;
63
+ }
64
+
65
+ const mqttRequest = request.toMqttRequest();
66
+ const message = this.serializer.serialize(duid, mqttRequest);
67
+ this.client.publish('rr/m/i/' + this.rriot.u + '/' + this.mqttUsername + '/' + duid, message.buffer, { qos: 1 });
68
+ }
69
+
70
+ private async onConnect(result: IConnackPacket) {
71
+ if (!result) {
72
+ return;
73
+ }
74
+
75
+ this.connected = true;
76
+ await this.connectionListeners.onConnected();
77
+
78
+ this.subscribeToQueue();
79
+ }
80
+
81
+ private subscribeToQueue() {
82
+ if (!this.client) {
83
+ return;
84
+ }
85
+ this.client.subscribe('rr/m/o/' + this.rriot.u + '/' + this.mqttUsername + '/#', this.onSubscribe.bind(this));
86
+ }
87
+
88
+ private async onSubscribe(err: Error | null, granted: ISubscriptionGrant[] | undefined) {
89
+ if (!err) {
90
+ return;
91
+ }
92
+
93
+ this.logger.error('failed to subscribe to the queue: ' + err);
94
+ this.connected = false;
95
+
96
+ await this.connectionListeners.onDisconnected();
97
+ }
98
+
99
+ private async onDisconnect() {
100
+ await this.connectionListeners.onDisconnected();
101
+ }
102
+
103
+ private async onError(result: Error | ErrorWithReasonCode) {
104
+ this.logger.error('MQTT connection error: ' + result);
105
+ this.connected = false;
106
+
107
+ await this.connectionListeners.onError(result.toString());
108
+ }
109
+
110
+ private onReconnect() {
111
+ this.subscribeToQueue();
112
+ }
113
+
114
+ private async onMessage(topic: string, message: Buffer<ArrayBufferLike>) {
115
+ if (!message) {
116
+ //Ignore empty messages
117
+ this.logger.notice('MQTTClient received empty message from topic: ' + topic);
118
+ return;
119
+ }
120
+
121
+ try {
122
+ const duid = topic.split('/').slice(-1)[0];
123
+ const response = this.deserializer.deserialize(duid, message);
124
+ await this.messageListeners.onMessage(response);
125
+ } catch (error) {
126
+ this.logger.error('unable to process message from queue ' + topic + ': ' + error);
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,19 @@
1
+ import { AbstractConnectionListener } from './listener/abstractConnectionListener.js';
2
+ import { AbstractMessageListener } from './listener/abstractMessageListener.js';
3
+ import { RequestMessage } from './model/requestMessage.js';
4
+
5
+ export interface Client {
6
+ registerConnectionListener(listener: AbstractConnectionListener): void;
7
+
8
+ registerMessageListener(listener: AbstractMessageListener): void;
9
+
10
+ isConnected(): boolean;
11
+
12
+ connect(): void;
13
+
14
+ disconnect(): Promise<void>;
15
+
16
+ send(duid: string, request: RequestMessage): Promise<void>;
17
+
18
+ get<T>(duid: string, request: RequestMessage): Promise<T>;
19
+ }