matterbridge-roborock-vacuum-plugin-regions 1.1.1-jb.1

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 (261) hide show
  1. package/.github/workflows/build.yml +56 -0
  2. package/.github/workflows/coverage.yml +59 -0
  3. package/.github/workflows/publish.yml +37 -0
  4. package/.tarignore +5 -0
  5. package/CHANGELOG.md +62 -0
  6. package/LICENSE +202 -0
  7. package/README.md +135 -0
  8. package/README_CLEANMODE.md +29 -0
  9. package/README_DEV.md +75 -0
  10. package/README_REPORT_ISSUE.md +34 -0
  11. package/README_SUPPORTED.md +67 -0
  12. package/dist/behaviorFactory.js +26 -0
  13. package/dist/behaviors/BehaviorDeviceGeneric.js +22 -0
  14. package/dist/behaviors/roborock.vacuum/default/default.js +183 -0
  15. package/dist/behaviors/roborock.vacuum/default/initalData.js +143 -0
  16. package/dist/behaviors/roborock.vacuum/default/runtimes.js +21 -0
  17. package/dist/behaviors/roborock.vacuum/smart/initalData.js +18 -0
  18. package/dist/behaviors/roborock.vacuum/smart/runtimes.js +11 -0
  19. package/dist/behaviors/roborock.vacuum/smart/smart.js +119 -0
  20. package/dist/clientManager.js +17 -0
  21. package/dist/helper.js +76 -0
  22. package/dist/index.js +4 -0
  23. package/dist/initialData/getBatteryStatus.js +24 -0
  24. package/dist/initialData/getOperationalStates.js +82 -0
  25. package/dist/initialData/getSupportedAreas.js +120 -0
  26. package/dist/initialData/getSupportedCleanModes.js +17 -0
  27. package/dist/initialData/getSupportedRunModes.js +11 -0
  28. package/dist/initialData/getSupportedScenes.js +26 -0
  29. package/dist/initialData/index.js +6 -0
  30. package/dist/model/CloudMessageModel.js +1 -0
  31. package/dist/model/DockingStationStatus.js +26 -0
  32. package/dist/model/ExperimentalFeatureSetting.js +30 -0
  33. package/dist/model/RoomMap.js +19 -0
  34. package/dist/model/roomIndexMap.js +21 -0
  35. package/dist/notifyMessageTypes.js +9 -0
  36. package/dist/platform.js +312 -0
  37. package/dist/platformRunner.js +90 -0
  38. package/dist/roborockCommunication/RESTAPI/roborockAuthenticateApi.js +213 -0
  39. package/dist/roborockCommunication/RESTAPI/roborockIoTApi.js +95 -0
  40. package/dist/roborockCommunication/Zenum/additionalPropCode.js +4 -0
  41. package/dist/roborockCommunication/Zenum/authenticateResponseCode.js +7 -0
  42. package/dist/roborockCommunication/Zenum/dockType.js +4 -0
  43. package/dist/roborockCommunication/Zenum/operationStatusCode.js +44 -0
  44. package/dist/roborockCommunication/Zenum/vacuumAndDockErrorCode.js +68 -0
  45. package/dist/roborockCommunication/Zmodel/apiResponse.js +1 -0
  46. package/dist/roborockCommunication/Zmodel/authenticateFlowState.js +1 -0
  47. package/dist/roborockCommunication/Zmodel/authenticateResponse.js +1 -0
  48. package/dist/roborockCommunication/Zmodel/baseURL.js +1 -0
  49. package/dist/roborockCommunication/Zmodel/batteryMessage.js +1 -0
  50. package/dist/roborockCommunication/Zmodel/device.js +1 -0
  51. package/dist/roborockCommunication/Zmodel/deviceModel.js +28 -0
  52. package/dist/roborockCommunication/Zmodel/deviceSchema.js +1 -0
  53. package/dist/roborockCommunication/Zmodel/deviceStatus.js +22 -0
  54. package/dist/roborockCommunication/Zmodel/dockInfo.js +6 -0
  55. package/dist/roborockCommunication/Zmodel/home.js +1 -0
  56. package/dist/roborockCommunication/Zmodel/homeInfo.js +1 -0
  57. package/dist/roborockCommunication/Zmodel/map.js +1 -0
  58. package/dist/roborockCommunication/Zmodel/mapInfo.js +29 -0
  59. package/dist/roborockCommunication/Zmodel/messageResult.js +7 -0
  60. package/dist/roborockCommunication/Zmodel/multipleMap.js +1 -0
  61. package/dist/roborockCommunication/Zmodel/networkInfo.js +1 -0
  62. package/dist/roborockCommunication/Zmodel/product.js +1 -0
  63. package/dist/roborockCommunication/Zmodel/room.js +1 -0
  64. package/dist/roborockCommunication/Zmodel/roomInfo.js +22 -0
  65. package/dist/roborockCommunication/Zmodel/scene.js +16 -0
  66. package/dist/roborockCommunication/Zmodel/userData.js +1 -0
  67. package/dist/roborockCommunication/Zmodel/vacuumError.js +27 -0
  68. package/dist/roborockCommunication/broadcast/abstractClient.js +55 -0
  69. package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +174 -0
  70. package/dist/roborockCommunication/broadcast/client/LocalNetworkUDPClient.js +129 -0
  71. package/dist/roborockCommunication/broadcast/client/MQTTClient.js +139 -0
  72. package/dist/roborockCommunication/broadcast/client.js +1 -0
  73. package/dist/roborockCommunication/broadcast/clientRouter.js +82 -0
  74. package/dist/roborockCommunication/broadcast/listener/abstractConnectionListener.js +1 -0
  75. package/dist/roborockCommunication/broadcast/listener/abstractMessageHandler.js +1 -0
  76. package/dist/roborockCommunication/broadcast/listener/abstractMessageListener.js +1 -0
  77. package/dist/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.js +26 -0
  78. package/dist/roborockCommunication/broadcast/listener/implementation/chainedMessageListener.js +11 -0
  79. package/dist/roborockCommunication/broadcast/listener/implementation/connectionStateListener.js +43 -0
  80. package/dist/roborockCommunication/broadcast/listener/implementation/generalSyncMessageListener.js +28 -0
  81. package/dist/roborockCommunication/broadcast/listener/implementation/simpleMessageListener.js +27 -0
  82. package/dist/roborockCommunication/broadcast/listener/implementation/syncMessageListener.js +33 -0
  83. package/dist/roborockCommunication/broadcast/listener/index.js +1 -0
  84. package/dist/roborockCommunication/broadcast/messageProcessor.js +148 -0
  85. package/dist/roborockCommunication/broadcast/model/contentMessage.js +1 -0
  86. package/dist/roborockCommunication/broadcast/model/dps.js +1 -0
  87. package/dist/roborockCommunication/broadcast/model/headerMessage.js +1 -0
  88. package/dist/roborockCommunication/broadcast/model/messageContext.js +37 -0
  89. package/dist/roborockCommunication/broadcast/model/protocol.js +28 -0
  90. package/dist/roborockCommunication/broadcast/model/requestMessage.js +38 -0
  91. package/dist/roborockCommunication/broadcast/model/responseMessage.js +14 -0
  92. package/dist/roborockCommunication/helper/chunkBuffer.js +17 -0
  93. package/dist/roborockCommunication/helper/cryptoHelper.js +23 -0
  94. package/dist/roborockCommunication/helper/messageDeserializer.js +98 -0
  95. package/dist/roborockCommunication/helper/messageSerializer.js +84 -0
  96. package/dist/roborockCommunication/helper/nameDecoder.js +66 -0
  97. package/dist/roborockCommunication/helper/sequence.js +16 -0
  98. package/dist/roborockCommunication/index.js +13 -0
  99. package/dist/roborockService.js +494 -0
  100. package/dist/runtimes/handleCloudMessage.js +110 -0
  101. package/dist/runtimes/handleHomeDataMessage.js +57 -0
  102. package/dist/runtimes/handleLocalMessage.js +169 -0
  103. package/dist/rvc.js +51 -0
  104. package/dist/settings.js +1 -0
  105. package/dist/share/function.js +93 -0
  106. package/dist/share/runtimeHelper.js +17 -0
  107. package/dist/tests/testData/mockData.js +359 -0
  108. package/eslint.config.js +80 -0
  109. package/jest.config.js +22 -0
  110. package/jest.setup.js +2 -0
  111. package/logo.png +0 -0
  112. package/matterbridge-roborock-vacuum-plugin.config.json +46 -0
  113. package/matterbridge-roborock-vacuum-plugin.schema.json +293 -0
  114. package/misc/status.md +119 -0
  115. package/package.json +111 -0
  116. package/prettier.config.js +49 -0
  117. package/screenshot/IMG_1.PNG +0 -0
  118. package/screenshot/IMG_2.PNG +0 -0
  119. package/screenshot/IMG_3.PNG +0 -0
  120. package/screenshot/IMG_4.PNG +0 -0
  121. package/screenshot/IMG_5.PNG +0 -0
  122. package/screenshot/IMG_6.PNG +0 -0
  123. package/screenshot/IMG_7.PNG +0 -0
  124. package/src/behaviorFactory.ts +41 -0
  125. package/src/behaviors/BehaviorDeviceGeneric.ts +31 -0
  126. package/src/behaviors/roborock.vacuum/default/default.ts +238 -0
  127. package/src/behaviors/roborock.vacuum/default/initalData.ts +152 -0
  128. package/src/behaviors/roborock.vacuum/default/runtimes.ts +23 -0
  129. package/src/behaviors/roborock.vacuum/smart/initalData.ts +20 -0
  130. package/src/behaviors/roborock.vacuum/smart/runtimes.ts +15 -0
  131. package/src/behaviors/roborock.vacuum/smart/smart.ts +159 -0
  132. package/src/clientManager.ts +23 -0
  133. package/src/helper.ts +97 -0
  134. package/src/index.ts +16 -0
  135. package/src/initialData/getBatteryStatus.ts +26 -0
  136. package/src/initialData/getOperationalStates.ts +94 -0
  137. package/src/initialData/getSupportedAreas.ts +162 -0
  138. package/src/initialData/getSupportedCleanModes.ts +22 -0
  139. package/src/initialData/getSupportedRunModes.ts +14 -0
  140. package/src/initialData/getSupportedScenes.ts +32 -0
  141. package/src/initialData/index.ts +6 -0
  142. package/src/model/CloudMessageModel.ts +11 -0
  143. package/src/model/DockingStationStatus.ts +41 -0
  144. package/src/model/ExperimentalFeatureSetting.ts +77 -0
  145. package/src/model/RoomMap.ts +38 -0
  146. package/src/model/roomIndexMap.ts +26 -0
  147. package/src/notifyMessageTypes.ts +8 -0
  148. package/src/platform.ts +424 -0
  149. package/src/platformRunner.ts +103 -0
  150. package/src/roborockCommunication/RESTAPI/roborockAuthenticateApi.ts +302 -0
  151. package/src/roborockCommunication/RESTAPI/roborockIoTApi.ts +107 -0
  152. package/src/roborockCommunication/Zenum/additionalPropCode.ts +3 -0
  153. package/src/roborockCommunication/Zenum/authenticateResponseCode.ts +6 -0
  154. package/src/roborockCommunication/Zenum/dockType.ts +3 -0
  155. package/src/roborockCommunication/Zenum/operationStatusCode.ts +43 -0
  156. package/src/roborockCommunication/Zenum/vacuumAndDockErrorCode.ts +68 -0
  157. package/src/roborockCommunication/Zmodel/apiResponse.ts +3 -0
  158. package/src/roborockCommunication/Zmodel/authenticateFlowState.ts +6 -0
  159. package/src/roborockCommunication/Zmodel/authenticateResponse.ts +5 -0
  160. package/src/roborockCommunication/Zmodel/baseURL.ts +5 -0
  161. package/src/roborockCommunication/Zmodel/batteryMessage.ts +16 -0
  162. package/src/roborockCommunication/Zmodel/device.ts +50 -0
  163. package/src/roborockCommunication/Zmodel/deviceModel.ts +27 -0
  164. package/src/roborockCommunication/Zmodel/deviceSchema.ts +8 -0
  165. package/src/roborockCommunication/Zmodel/deviceStatus.ts +30 -0
  166. package/src/roborockCommunication/Zmodel/dockInfo.ts +9 -0
  167. package/src/roborockCommunication/Zmodel/home.ts +13 -0
  168. package/src/roborockCommunication/Zmodel/homeInfo.ts +5 -0
  169. package/src/roborockCommunication/Zmodel/map.ts +20 -0
  170. package/src/roborockCommunication/Zmodel/mapInfo.ts +54 -0
  171. package/src/roborockCommunication/Zmodel/messageResult.ts +75 -0
  172. package/src/roborockCommunication/Zmodel/multipleMap.ts +8 -0
  173. package/src/roborockCommunication/Zmodel/networkInfo.ts +7 -0
  174. package/src/roborockCommunication/Zmodel/product.ts +9 -0
  175. package/src/roborockCommunication/Zmodel/room.ts +4 -0
  176. package/src/roborockCommunication/Zmodel/roomInfo.ts +30 -0
  177. package/src/roborockCommunication/Zmodel/scene.ts +44 -0
  178. package/src/roborockCommunication/Zmodel/userData.ts +26 -0
  179. package/src/roborockCommunication/Zmodel/vacuumError.ts +35 -0
  180. package/src/roborockCommunication/broadcast/abstractClient.ts +80 -0
  181. package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +218 -0
  182. package/src/roborockCommunication/broadcast/client/LocalNetworkUDPClient.ts +157 -0
  183. package/src/roborockCommunication/broadcast/client/MQTTClient.ts +174 -0
  184. package/src/roborockCommunication/broadcast/client.ts +19 -0
  185. package/src/roborockCommunication/broadcast/clientRouter.ts +104 -0
  186. package/src/roborockCommunication/broadcast/listener/abstractConnectionListener.ts +6 -0
  187. package/src/roborockCommunication/broadcast/listener/abstractMessageHandler.ts +11 -0
  188. package/src/roborockCommunication/broadcast/listener/abstractMessageListener.ts +5 -0
  189. package/src/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.ts +33 -0
  190. package/src/roborockCommunication/broadcast/listener/implementation/chainedMessageListener.ts +16 -0
  191. package/src/roborockCommunication/broadcast/listener/implementation/connectionStateListener.ts +57 -0
  192. package/src/roborockCommunication/broadcast/listener/implementation/generalSyncMessageListener.ts +38 -0
  193. package/src/roborockCommunication/broadcast/listener/implementation/simpleMessageListener.ts +37 -0
  194. package/src/roborockCommunication/broadcast/listener/implementation/syncMessageListener.ts +50 -0
  195. package/src/roborockCommunication/broadcast/listener/index.ts +3 -0
  196. package/src/roborockCommunication/broadcast/messageProcessor.ts +184 -0
  197. package/src/roborockCommunication/broadcast/model/contentMessage.ts +5 -0
  198. package/src/roborockCommunication/broadcast/model/dps.ts +19 -0
  199. package/src/roborockCommunication/broadcast/model/headerMessage.ts +7 -0
  200. package/src/roborockCommunication/broadcast/model/messageContext.ts +53 -0
  201. package/src/roborockCommunication/broadcast/model/protocol.ts +28 -0
  202. package/src/roborockCommunication/broadcast/model/requestMessage.ts +51 -0
  203. package/src/roborockCommunication/broadcast/model/responseMessage.ts +19 -0
  204. package/src/roborockCommunication/helper/chunkBuffer.ts +18 -0
  205. package/src/roborockCommunication/helper/cryptoHelper.ts +30 -0
  206. package/src/roborockCommunication/helper/messageDeserializer.ts +119 -0
  207. package/src/roborockCommunication/helper/messageSerializer.ts +101 -0
  208. package/src/roborockCommunication/helper/nameDecoder.ts +78 -0
  209. package/src/roborockCommunication/helper/sequence.ts +18 -0
  210. package/src/roborockCommunication/index.ts +25 -0
  211. package/src/roborockService.ts +657 -0
  212. package/src/runtimes/handleCloudMessage.ts +134 -0
  213. package/src/runtimes/handleHomeDataMessage.ts +67 -0
  214. package/src/runtimes/handleLocalMessage.ts +209 -0
  215. package/src/rvc.ts +97 -0
  216. package/src/settings.ts +1 -0
  217. package/src/share/function.ts +103 -0
  218. package/src/share/runtimeHelper.ts +23 -0
  219. package/src/tests/behaviors/roborock.vacuum/default/default.test.ts +134 -0
  220. package/src/tests/behaviors/roborock.vacuum/smart/runtimes.test.ts +64 -0
  221. package/src/tests/behaviors/roborock.vacuum/smart/smart.test.ts +215 -0
  222. package/src/tests/helper.test.ts +162 -0
  223. package/src/tests/initialData/getSupportedAreas.test.ts +181 -0
  224. package/src/tests/model/DockingStationStatus.test.ts +39 -0
  225. package/src/tests/platformRunner.test.ts +188 -0
  226. package/src/tests/platformRunner2.test.ts +228 -0
  227. package/src/tests/platformRunner3.test.ts +46 -0
  228. package/src/tests/roborockCommunication/RESTAPI/roborockAuthenticateApi.test.ts +144 -0
  229. package/src/tests/roborockCommunication/RESTAPI/roborockIoTApi.test.ts +106 -0
  230. package/src/tests/roborockCommunication/broadcast/client/LocalNetworkClient.test.ts +189 -0
  231. package/src/tests/roborockCommunication/broadcast/client/MQTTClient.test.ts +208 -0
  232. package/src/tests/roborockCommunication/broadcast/clientRouter.test.ts +168 -0
  233. package/src/tests/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.test.ts +59 -0
  234. package/src/tests/roborockCommunication/broadcast/listener/implementation/chainedMessageListener.test.ts +46 -0
  235. package/src/tests/roborockCommunication/broadcast/listener/implementation/simpleMessageListener.test.ts +71 -0
  236. package/src/tests/roborockCommunication/broadcast/listener/implementation/syncMessageListener.test.ts +86 -0
  237. package/src/tests/roborockCommunication/broadcast/messageProcessor.test.ts +126 -0
  238. package/src/tests/roborockService.setSelectedAreas.test.ts +61 -0
  239. package/src/tests/roborockService.test.ts +517 -0
  240. package/src/tests/roborockService2.test.ts +69 -0
  241. package/src/tests/roborockService3.test.ts +133 -0
  242. package/src/tests/roborockService4.test.ts +76 -0
  243. package/src/tests/roborockService5.test.ts +79 -0
  244. package/src/tests/runtimes/handleCloudMessage.test.ts +200 -0
  245. package/src/tests/runtimes/handleHomeDataMessage.test.ts +54 -0
  246. package/src/tests/runtimes/handleLocalMessage.test.ts +227 -0
  247. package/src/tests/testData/mockData.ts +370 -0
  248. package/src/tests/testData/mockHomeData-a187.json +286 -0
  249. package/tsconfig.jest.json +21 -0
  250. package/tsconfig.json +37 -0
  251. package/tsconfig.production.json +19 -0
  252. package/tslint.json +9 -0
  253. package/web-for-testing/README.md +47 -0
  254. package/web-for-testing/nodemon.json +7 -0
  255. package/web-for-testing/package-lock.json +6600 -0
  256. package/web-for-testing/package.json +36 -0
  257. package/web-for-testing/src/app.ts +194 -0
  258. package/web-for-testing/tsconfig-ext.json +19 -0
  259. package/web-for-testing/tsconfig.json +23 -0
  260. package/web-for-testing/views/index.ejs +172 -0
  261. package/web-for-testing/watch.mjs +93 -0
@@ -0,0 +1,174 @@
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 { debugStringify } from 'matterbridge/logger';
6
+ import { AbstractClient } from '../abstractClient.js';
7
+ import { Sequence } from '../../helper/sequence.js';
8
+ import { ChunkBuffer } from '../../helper/chunkBuffer.js';
9
+ export class LocalNetworkClient extends AbstractClient {
10
+ clientName = 'LocalNetworkClient';
11
+ shouldReconnect = true;
12
+ socket = undefined;
13
+ buffer = new ChunkBuffer();
14
+ messageIdSeq;
15
+ pingInterval;
16
+ keepConnectionAliveInterval = undefined;
17
+ duid;
18
+ ip;
19
+ constructor(logger, context, duid, ip) {
20
+ super(logger, context);
21
+ this.duid = duid;
22
+ this.ip = ip;
23
+ this.messageIdSeq = new Sequence(100000, 999999);
24
+ this.initializeConnectionStateListener();
25
+ }
26
+ connect() {
27
+ if (this.socket) {
28
+ return;
29
+ }
30
+ this.socket = new Socket();
31
+ this.socket.on('close', this.onDisconnect.bind(this));
32
+ this.socket.on('end', this.onEnd.bind(this));
33
+ this.socket.on('error', this.onError.bind(this));
34
+ this.socket.on('connect', this.onConnect.bind(this));
35
+ this.socket.on('timeout', this.onTimeout.bind(this));
36
+ this.socket.on('data', this.onMessage.bind(this));
37
+ this.socket.connect(58867, this.ip);
38
+ this.keepConnectionAlive();
39
+ }
40
+ async disconnect() {
41
+ if (!this.socket) {
42
+ return;
43
+ }
44
+ this.isInDisconnectingStep = true;
45
+ if (this.pingInterval) {
46
+ clearInterval(this.pingInterval);
47
+ }
48
+ this.socket.destroy();
49
+ this.socket = undefined;
50
+ }
51
+ async send(duid, request) {
52
+ if (!this.socket || !this.connected) {
53
+ this.logger.error(`${duid}: socket is not online, , ${debugStringify(request)}`);
54
+ return;
55
+ }
56
+ const localRequest = request.toLocalRequest();
57
+ const message = this.serializer.serialize(duid, localRequest);
58
+ this.logger.debug(`sending message ${message.messageId}, protocol:${localRequest.protocol}, method:${localRequest.method}, secure:${request.secure} to ${duid}`);
59
+ this.socket.write(this.wrapWithLengthData(message.buffer));
60
+ }
61
+ async onConnect() {
62
+ this.logger.debug(` [LocalNetworkClient]: ${this.duid} connected to ${this.ip}`);
63
+ this.logger.debug(` [LocalNetworkClient]: ${this.duid} socket writable: ${this.socket?.writable}, readable: ${this.socket?.readable}`);
64
+ this.connected = true;
65
+ this.retryCount = 0;
66
+ await this.sendHelloMessage();
67
+ this.pingInterval = setInterval(this.sendPingRequest.bind(this), 5000);
68
+ await this.connectionListeners.onConnected(this.duid);
69
+ }
70
+ async onDisconnect(hadError) {
71
+ this.logger.info(` [LocalNetworkClient]: ${this.duid} socket disconnected. Had error: ${hadError}`);
72
+ this.connected = false;
73
+ if (this.socket) {
74
+ this.socket.destroy();
75
+ this.socket = undefined;
76
+ }
77
+ if (this.pingInterval) {
78
+ clearInterval(this.pingInterval);
79
+ }
80
+ await this.connectionListeners.onDisconnected(this.duid, 'Socket disconnected. Had no error.');
81
+ }
82
+ async onError(error) {
83
+ this.logger.error(` [LocalNetworkClient]: Socket error for ${this.duid}: ${error.message}`);
84
+ await this.connectionListeners.onError(this.duid, error.message);
85
+ }
86
+ async onTimeout() {
87
+ this.logger.error(` [LocalNetworkClient]: Socket for ${this.duid} timed out.`);
88
+ }
89
+ async onEnd() {
90
+ this.logger.debug(` [LocalNetworkClient]: ${this.duid} socket ended.`);
91
+ }
92
+ async onMessage(message) {
93
+ if (!this.socket) {
94
+ return;
95
+ }
96
+ if (!message || message.length == 0) {
97
+ this.logger.debug('LocalNetworkClient received empty message from socket.');
98
+ return;
99
+ }
100
+ try {
101
+ this.buffer.append(message);
102
+ const receivedBuffer = this.buffer.get();
103
+ if (!this.isMessageComplete(receivedBuffer)) {
104
+ return;
105
+ }
106
+ this.buffer.reset();
107
+ let offset = 0;
108
+ while (offset + 4 <= receivedBuffer.length) {
109
+ const segmentLength = receivedBuffer.readUInt32BE(offset);
110
+ if (segmentLength == 17) {
111
+ offset += 4 + segmentLength;
112
+ continue;
113
+ }
114
+ try {
115
+ const currentBuffer = receivedBuffer.subarray(offset + 4, offset + segmentLength + 4);
116
+ const response = this.deserializer.deserialize(this.duid, currentBuffer);
117
+ await this.messageListeners.onMessage(response);
118
+ }
119
+ catch (error) {
120
+ this.logger.error('LocalNetworkClient: unable to process message with error: ' + error);
121
+ }
122
+ offset += 4 + segmentLength;
123
+ }
124
+ }
125
+ catch (error) {
126
+ this.logger.error('LocalNetworkClient: read socket buffer error: ' + error);
127
+ }
128
+ }
129
+ isMessageComplete(buffer) {
130
+ let totalLength = 0;
131
+ let offset = 0;
132
+ while (offset + 4 <= buffer.length) {
133
+ const segmentLength = buffer.readUInt32BE(offset);
134
+ totalLength += 4 + segmentLength;
135
+ offset += 4 + segmentLength;
136
+ if (offset > buffer.length) {
137
+ return false;
138
+ }
139
+ }
140
+ return totalLength <= buffer.length;
141
+ }
142
+ wrapWithLengthData(buffer) {
143
+ const lengthBuffer = Buffer.alloc(4);
144
+ lengthBuffer.writeUInt32BE(buffer.length, 0);
145
+ return Buffer.concat([lengthBuffer, buffer]);
146
+ }
147
+ async sendHelloMessage() {
148
+ const request = new RequestMessage({
149
+ protocol: Protocol.hello_request,
150
+ messageId: this.messageIdSeq.next(),
151
+ nonce: this.context.nonce,
152
+ });
153
+ await this.send(this.duid, request);
154
+ }
155
+ async sendPingRequest() {
156
+ const request = new RequestMessage({
157
+ protocol: Protocol.ping_request,
158
+ messageId: this.messageIdSeq.next(),
159
+ });
160
+ await this.send(this.duid, request);
161
+ }
162
+ keepConnectionAlive() {
163
+ if (this.keepConnectionAliveInterval) {
164
+ clearTimeout(this.keepConnectionAliveInterval);
165
+ this.keepConnectionAliveInterval.unref();
166
+ }
167
+ this.keepConnectionAliveInterval = setInterval(() => {
168
+ if (this.socket === undefined || !this.connected || !this.socket.writable || this.socket.readable) {
169
+ this.logger.debug(` [LocalNetworkClient]: ${this.duid} socket is not writable or readable, reconnecting...`);
170
+ this.connect();
171
+ }
172
+ }, 60 * 60 * 1000);
173
+ }
174
+ }
@@ -0,0 +1,129 @@
1
+ import * as dgram from 'node:dgram';
2
+ import { Parser } from 'binary-parser/dist/binary_parser.js';
3
+ import crypto from 'node:crypto';
4
+ import CRC32 from 'crc-32';
5
+ import { AbstractClient } from '../abstractClient.js';
6
+ export class LocalNetworkUDPClient extends AbstractClient {
7
+ clientName = 'LocalNetworkUDPClient';
8
+ shouldReconnect = false;
9
+ PORT = 58866;
10
+ server = undefined;
11
+ V10Parser;
12
+ L01Parser;
13
+ constructor(logger, context) {
14
+ super(logger, context);
15
+ this.V10Parser = new Parser()
16
+ .endianness('big')
17
+ .string('version', { length: 3 })
18
+ .uint32('seq')
19
+ .uint16('protocol')
20
+ .uint16('payloadLen')
21
+ .buffer('payload', { length: 'payloadLen' })
22
+ .uint32('crc32');
23
+ this.L01Parser = new Parser()
24
+ .endianness('big')
25
+ .string('version', { length: 3 })
26
+ .string('field1', { length: 4 })
27
+ .string('field2', { length: 2 })
28
+ .uint16('payloadLen')
29
+ .buffer('payload', { length: 'payloadLen' })
30
+ .uint32('crc32');
31
+ this.logger = logger;
32
+ }
33
+ connect() {
34
+ try {
35
+ this.server = dgram.createSocket('udp4');
36
+ this.server.bind(this.PORT);
37
+ this.server.on('message', this.onMessage.bind(this));
38
+ this.server.on('error', this.onError.bind(this));
39
+ }
40
+ catch (err) {
41
+ this.logger.error(`Failed to create UDP socket: ${err}`);
42
+ this.server = undefined;
43
+ }
44
+ }
45
+ disconnect() {
46
+ if (this.server) {
47
+ return new Promise((resolve) => {
48
+ this.server?.close(() => {
49
+ this.server = undefined;
50
+ resolve();
51
+ });
52
+ });
53
+ }
54
+ return Promise.resolve();
55
+ }
56
+ send(duid, request) {
57
+ this.logger.debug(`Sending request to ${duid}: ${JSON.stringify(request)}`);
58
+ return Promise.resolve();
59
+ }
60
+ async onError(result) {
61
+ this.logger.error(`UDP socket error: ${result}`);
62
+ if (this.server) {
63
+ this.server.close();
64
+ this.server = undefined;
65
+ }
66
+ }
67
+ async onMessage(buffer) {
68
+ const message = await this.deserializeMessage(buffer);
69
+ this.logger.debug('Received message: ' + JSON.stringify(message));
70
+ }
71
+ async deserializeMessage(buffer) {
72
+ const version = buffer.toString('latin1', 0, 3);
73
+ if (version !== '1.0' && version !== 'L01' && version !== 'A01') {
74
+ throw new Error('unknown protocol version ' + version);
75
+ }
76
+ let data;
77
+ switch (version) {
78
+ case '1.0':
79
+ data = await this.deserializeV10Message(buffer);
80
+ return JSON.parse(data);
81
+ case 'L01':
82
+ data = await this.deserializeL01Message(buffer);
83
+ return JSON.parse(data);
84
+ case 'A01':
85
+ return undefined;
86
+ default:
87
+ throw new Error('unknown protocol version ' + version);
88
+ }
89
+ }
90
+ async deserializeV10Message(message) {
91
+ const data = this.V10Parser.parse(message);
92
+ const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0;
93
+ const expectedCrc32 = data.crc32;
94
+ if (crc32 != expectedCrc32) {
95
+ throw new Error('wrong CRC32 ' + crc32 + ', expected ' + expectedCrc32);
96
+ }
97
+ const decipher = crypto.createDecipheriv('aes-128-ecb', Buffer.from('qWKYcdQWrbm9hPqe', 'utf8'), null);
98
+ decipher.setAutoPadding(false);
99
+ let decrypted = decipher.update(data.payload, 'binary', 'utf8');
100
+ decrypted += decipher.final('utf8');
101
+ const paddingLength = decrypted.charCodeAt(decrypted.length - 1);
102
+ return decrypted.slice(0, -paddingLength);
103
+ }
104
+ async deserializeL01Message(message) {
105
+ const data = this.L01Parser.parse(message);
106
+ const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0;
107
+ const expectedCrc32 = data.crc32;
108
+ if (crc32 != expectedCrc32) {
109
+ throw new Error('wrong CRC32 ' + crc32 + ', expected ' + expectedCrc32);
110
+ }
111
+ const payload = data.payload;
112
+ const key = crypto.createHash('sha256').update(Buffer.from('qWKYcdQWrbm9hPqe', 'utf8')).digest();
113
+ const digestInput = message.subarray(0, 9);
114
+ const digest = crypto.createHash('sha256').update(digestInput).digest();
115
+ const iv = digest.subarray(0, 12);
116
+ const tag = payload.subarray(payload.length - 16);
117
+ const ciphertext = payload.subarray(0, payload.length - 16);
118
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
119
+ decipher.setAuthTag(tag);
120
+ try {
121
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
122
+ return decrypted.toString('utf8');
123
+ }
124
+ catch (e) {
125
+ const message = e && typeof e === 'object' && 'message' in e ? e.message : String(e);
126
+ throw new Error('failed to decrypt: ' + message + ' / iv: ' + iv.toString('hex') + ' / tag: ' + tag.toString('hex') + ' / encrypted: ' + ciphertext.toString('hex'));
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,139 @@
1
+ import mqtt from 'mqtt';
2
+ import * as CryptoUtils from '../../helper/cryptoHelper.js';
3
+ import { AbstractClient } from '../abstractClient.js';
4
+ import { debugStringify } from 'matterbridge/logger';
5
+ export class MQTTClient extends AbstractClient {
6
+ clientName = 'MQTTClient';
7
+ shouldReconnect = false;
8
+ rriot;
9
+ mqttUsername;
10
+ mqttPassword;
11
+ mqttClient = undefined;
12
+ keepConnectionAliveInterval = undefined;
13
+ constructor(logger, context, userdata) {
14
+ super(logger, context);
15
+ this.rriot = userdata.rriot;
16
+ this.mqttUsername = CryptoUtils.md5hex(userdata.rriot.u + ':' + userdata.rriot.k).substring(2, 10);
17
+ this.mqttPassword = CryptoUtils.md5hex(userdata.rriot.s + ':' + userdata.rriot.k).substring(16);
18
+ this.initializeConnectionStateListener();
19
+ }
20
+ connect() {
21
+ if (this.mqttClient) {
22
+ return;
23
+ }
24
+ this.mqttClient = mqtt.connect(this.rriot.r.m, {
25
+ clientId: this.mqttUsername,
26
+ username: this.mqttUsername,
27
+ password: this.mqttPassword,
28
+ keepalive: 30,
29
+ log: () => {
30
+ },
31
+ });
32
+ this.mqttClient.on('connect', this.onConnect.bind(this));
33
+ this.mqttClient.on('error', this.onError.bind(this));
34
+ this.mqttClient.on('reconnect', this.onReconnect.bind(this));
35
+ this.mqttClient.on('close', this.onClose.bind(this));
36
+ this.mqttClient.on('disconnect', this.onDisconnect.bind(this));
37
+ this.mqttClient.on('offline', this.onOffline.bind(this));
38
+ this.mqttClient.on('message', this.onMessage.bind(this));
39
+ this.keepConnectionAlive();
40
+ }
41
+ async disconnect() {
42
+ if (!this.mqttClient || !this.connected) {
43
+ return;
44
+ }
45
+ try {
46
+ this.isInDisconnectingStep = true;
47
+ this.mqttClient.end();
48
+ }
49
+ catch (error) {
50
+ this.logger.error('MQTT client failed to disconnect with error: ' + error);
51
+ }
52
+ }
53
+ async send(duid, request) {
54
+ if (!this.mqttClient || !this.connected) {
55
+ this.logger.error(`${duid}: mqtt is not available, ${debugStringify(request)}`);
56
+ return;
57
+ }
58
+ const mqttRequest = request.toMqttRequest();
59
+ const message = this.serializer.serialize(duid, mqttRequest);
60
+ this.logger.debug(`MQTTClient sending message to ${duid}: ${debugStringify(mqttRequest)}`);
61
+ this.mqttClient.publish(`rr/m/i/${this.rriot.u}/${this.mqttUsername}/${duid}`, message.buffer, { qos: 1 });
62
+ this.logger.debug(`MQTTClient published message to topic: rr/m/i/${this.rriot.u}/${this.mqttUsername}/${duid}`);
63
+ }
64
+ keepConnectionAlive() {
65
+ if (this.keepConnectionAliveInterval) {
66
+ clearTimeout(this.keepConnectionAliveInterval);
67
+ this.keepConnectionAliveInterval.unref();
68
+ }
69
+ this.keepConnectionAliveInterval = setInterval(() => {
70
+ if (this.mqttClient) {
71
+ this.mqttClient.end();
72
+ this.mqttClient.reconnect();
73
+ }
74
+ else {
75
+ this.connect();
76
+ }
77
+ }, 30 * 60 * 1000);
78
+ }
79
+ async onConnect(result) {
80
+ if (!result) {
81
+ return;
82
+ }
83
+ this.connected = true;
84
+ await this.connectionListeners.onConnected('mqtt-' + this.mqttUsername);
85
+ this.subscribeToQueue();
86
+ }
87
+ subscribeToQueue() {
88
+ if (!this.mqttClient || !this.connected) {
89
+ return;
90
+ }
91
+ this.mqttClient.subscribe('rr/m/o/' + this.rriot.u + '/' + this.mqttUsername + '/#', this.onSubscribe.bind(this));
92
+ }
93
+ async onSubscribe(err, granted) {
94
+ if (!err) {
95
+ this.logger.info('onSubscribe: ' + JSON.stringify(granted));
96
+ return;
97
+ }
98
+ this.logger.error('failed to subscribe: ' + err);
99
+ this.connected = false;
100
+ await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername, 'Failed to subscribe to the queue: ' + err.toString());
101
+ }
102
+ async onDisconnect() {
103
+ this.connected = false;
104
+ await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername, 'Disconnected from MQTT broker');
105
+ }
106
+ async onError(result) {
107
+ this.logger.error('MQTT connection error: ' + result);
108
+ this.connected = false;
109
+ await this.connectionListeners.onError('mqtt-' + this.mqttUsername, result.toString());
110
+ }
111
+ async onClose() {
112
+ if (this.connected) {
113
+ await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername, 'MQTT connection closed');
114
+ }
115
+ this.connected = false;
116
+ }
117
+ async onOffline() {
118
+ this.connected = false;
119
+ await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername, 'MQTT connection offline');
120
+ }
121
+ onReconnect() {
122
+ this.subscribeToQueue();
123
+ this.connectionListeners.onReconnect('mqtt-' + this.mqttUsername, 'Reconnected to MQTT broker');
124
+ }
125
+ async onMessage(topic, message) {
126
+ if (!message) {
127
+ this.logger.notice('MQTTClient received empty message from topic: ' + topic);
128
+ return;
129
+ }
130
+ try {
131
+ const duid = topic.split('/').slice(-1)[0];
132
+ const response = this.deserializer.deserialize(duid, message);
133
+ await this.messageListeners.onMessage(response);
134
+ }
135
+ catch (error) {
136
+ this.logger.error('MQTTClient: unable to process message with error: ' + topic + ': ' + error);
137
+ }
138
+ }
139
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,82 @@
1
+ import { ChainedConnectionListener } from './listener/implementation/chainedConnectionListener.js';
2
+ import { ChainedMessageListener } from './listener/implementation/chainedMessageListener.js';
3
+ import { MessageContext } from './model/messageContext.js';
4
+ import { LocalNetworkClient } from './client/LocalNetworkClient.js';
5
+ import { MQTTClient } from './client/MQTTClient.js';
6
+ export class ClientRouter {
7
+ connectionListeners = new ChainedConnectionListener();
8
+ messageListeners = new ChainedMessageListener();
9
+ context;
10
+ localClients = new Map();
11
+ logger;
12
+ mqttClient;
13
+ constructor(logger, userdata) {
14
+ this.context = new MessageContext(userdata);
15
+ this.logger = logger;
16
+ this.mqttClient = new MQTTClient(logger, this.context, userdata);
17
+ this.mqttClient.registerConnectionListener(this.connectionListeners);
18
+ this.mqttClient.registerMessageListener(this.messageListeners);
19
+ }
20
+ registerDevice(duid, localKey, pv, nonce) {
21
+ this.context.registerDevice(duid, localKey, pv, nonce);
22
+ }
23
+ updateNonce(duid, nonce) {
24
+ this.context.updateNonce(duid, nonce);
25
+ }
26
+ registerClient(duid, ip) {
27
+ const localClient = new LocalNetworkClient(this.logger, this.context, duid, ip);
28
+ localClient.registerConnectionListener(this.connectionListeners);
29
+ localClient.registerMessageListener(this.messageListeners);
30
+ this.localClients.set(duid, localClient);
31
+ return localClient;
32
+ }
33
+ unregisterClient(duid) {
34
+ this.localClients.delete(duid);
35
+ }
36
+ registerConnectionListener(listener) {
37
+ this.connectionListeners.register(listener);
38
+ }
39
+ registerMessageListener(listener) {
40
+ this.messageListeners.register(listener);
41
+ }
42
+ isConnected() {
43
+ return this.mqttClient.isConnected();
44
+ }
45
+ connect() {
46
+ this.mqttClient.connect();
47
+ this.localClients.forEach((client) => {
48
+ client.connect();
49
+ });
50
+ }
51
+ async disconnect() {
52
+ await this.mqttClient.disconnect();
53
+ this.localClients.forEach((client) => {
54
+ client.disconnect();
55
+ });
56
+ }
57
+ async send(duid, request) {
58
+ if (request.secure) {
59
+ await this.mqttClient.send(duid, request);
60
+ }
61
+ else {
62
+ await this.getClient(duid).send(duid, request);
63
+ }
64
+ }
65
+ async get(duid, request) {
66
+ if (request.secure) {
67
+ return await this.mqttClient.get(duid, request);
68
+ }
69
+ else {
70
+ return await this.getClient(duid).get(duid, request);
71
+ }
72
+ }
73
+ getClient(duid) {
74
+ const localClient = this.localClients.get(duid);
75
+ if (localClient === undefined || !localClient.isConnected()) {
76
+ return this.mqttClient;
77
+ }
78
+ else {
79
+ return localClient;
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,26 @@
1
+ export class ChainedConnectionListener {
2
+ listeners = [];
3
+ register(listener) {
4
+ this.listeners.push(listener);
5
+ }
6
+ async onConnected(duid) {
7
+ for (const listener of this.listeners) {
8
+ await listener.onConnected(duid);
9
+ }
10
+ }
11
+ async onDisconnected(duid, message) {
12
+ for (const listener of this.listeners) {
13
+ await listener.onDisconnected(duid, message);
14
+ }
15
+ }
16
+ async onError(duid, message) {
17
+ for (const listener of this.listeners) {
18
+ await listener.onError(duid, message);
19
+ }
20
+ }
21
+ async onReconnect(duid, message) {
22
+ for (const listener of this.listeners) {
23
+ await listener.onReconnect(duid, message);
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,11 @@
1
+ export class ChainedMessageListener {
2
+ listeners = [];
3
+ register(listener) {
4
+ this.listeners.push(listener);
5
+ }
6
+ async onMessage(message) {
7
+ for (const listener of this.listeners) {
8
+ await listener.onMessage(message);
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,43 @@
1
+ export class ConnectionStateListener {
2
+ logger;
3
+ client;
4
+ clientName;
5
+ shouldReconnect;
6
+ constructor(logger, client, clientName, shouldReconnect = false) {
7
+ this.logger = logger;
8
+ this.client = client;
9
+ this.clientName = clientName;
10
+ this.shouldReconnect = shouldReconnect;
11
+ }
12
+ async onConnected(duid) {
13
+ this.logger.notice(`Device ${duid} connected to ${this.clientName}`);
14
+ }
15
+ async onReconnect(duid, message) {
16
+ this.logger.info(`Device ${duid} reconnected to ${this.clientName} with message: ${message}`);
17
+ }
18
+ async onDisconnected(duid, message) {
19
+ this.logger.error(`Device ${duid} disconnected from ${this.clientName} with message: ${message}`);
20
+ if (!this.shouldReconnect) {
21
+ this.logger.notice(`Device ${duid} disconnected from ${this.clientName}, but re-registration is disabled.`);
22
+ return;
23
+ }
24
+ if (this.client.retryCount > 10) {
25
+ this.logger.error(`Device with DUID ${duid} has exceeded retry limit, not re-registering.`);
26
+ return;
27
+ }
28
+ this.client.retryCount++;
29
+ const isInDisconnectingStep = this.client.isInDisconnectingStep;
30
+ if (isInDisconnectingStep) {
31
+ this.logger.info(`Device with DUID ${duid} is in disconnecting step, skipping re-registration.`);
32
+ return;
33
+ }
34
+ setTimeout(() => {
35
+ this.logger.info(`Re-registering device with DUID ${duid} to ${this.clientName}`);
36
+ this.client.connect();
37
+ }, 10000);
38
+ this.client.isInDisconnectingStep = false;
39
+ }
40
+ async onError(duid, message) {
41
+ this.logger.error(`Error on device with DUID ${duid}: ${message}`);
42
+ }
43
+ }
@@ -0,0 +1,28 @@
1
+ import { Protocol } from '../../model/protocol.js';
2
+ export class GeneralSyncMessageListener {
3
+ duid;
4
+ handler;
5
+ timer;
6
+ constructor(duid) {
7
+ this.duid = duid;
8
+ }
9
+ waitFor() {
10
+ return new Promise((resolve, reject) => {
11
+ this.handler = resolve;
12
+ this.timer = setTimeout(() => {
13
+ reject('no ping response received for ' + this.duid + ' within ' + 30 + 'second');
14
+ }, 30 * 1000);
15
+ });
16
+ }
17
+ async onMessage(message) {
18
+ if (message.contain(Protocol.hello_response)) {
19
+ if (this.handler) {
20
+ this.handler(message);
21
+ }
22
+ if (this.timer) {
23
+ clearTimeout(this.timer);
24
+ this.timer.unref();
25
+ }
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,27 @@
1
+ import { Protocol } from '../../model/protocol.js';
2
+ export class SimpleMessageListener {
3
+ handler;
4
+ registerListener(handler) {
5
+ this.handler = handler;
6
+ }
7
+ async onMessage(message) {
8
+ if (!this.handler || message.contain(Protocol.rpc_response) || message.contain(Protocol.map_response)) {
9
+ return;
10
+ }
11
+ if (message.contain(Protocol.status_update) && this.handler.onStatusChanged) {
12
+ await this.handler.onStatusChanged();
13
+ }
14
+ if (message.contain(Protocol.error) && this.handler.onError) {
15
+ const value = message.get(Protocol.error);
16
+ await this.handler.onError(Number(value));
17
+ }
18
+ if (message.contain(Protocol.battery) && this.handler.onBatteryUpdate) {
19
+ const value = message.get(Protocol.battery);
20
+ await this.handler.onBatteryUpdate(Number(value));
21
+ }
22
+ if (message.contain(Protocol.additional_props) && this.handler.onAdditionalProps) {
23
+ const value = message.get(Protocol.additional_props);
24
+ await this.handler.onAdditionalProps(Number(value));
25
+ }
26
+ }
27
+ }