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,157 @@
1
+ import * as dgram from 'node:dgram';
2
+ import { Socket } from 'node:dgram';
3
+ import { Parser } from 'binary-parser/dist/binary_parser.js';
4
+ import crypto from 'node:crypto';
5
+ import CRC32 from 'crc-32';
6
+ import { AnsiLogger } from 'matterbridge/logger';
7
+ import { AbstractClient } from '../abstractClient.js';
8
+ import { RequestMessage, ResponseMessage } from '../../index.js';
9
+ import { MessageContext } from '../model/messageContext.js';
10
+
11
+ /* eslint-disable @typescript-eslint/no-explicit-any */
12
+ export class LocalNetworkUDPClient extends AbstractClient {
13
+ protected override clientName = 'LocalNetworkUDPClient';
14
+ protected override shouldReconnect = false;
15
+
16
+ private readonly PORT = 58866;
17
+ private server: Socket | undefined = undefined;
18
+
19
+ private readonly V10Parser: Parser;
20
+ private readonly L01Parser: Parser;
21
+
22
+ constructor(logger: AnsiLogger, context: MessageContext) {
23
+ super(logger, context);
24
+ this.V10Parser = new Parser()
25
+ .endianness('big')
26
+ .string('version', { length: 3 })
27
+ .uint32('seq')
28
+ .uint16('protocol')
29
+ .uint16('payloadLen')
30
+ .buffer('payload', { length: 'payloadLen' })
31
+ .uint32('crc32');
32
+
33
+ this.L01Parser = new Parser()
34
+ .endianness('big')
35
+ .string('version', { length: 3 })
36
+ .string('field1', { length: 4 })
37
+ .string('field2', { length: 2 })
38
+ .uint16('payloadLen')
39
+ .buffer('payload', { length: 'payloadLen' })
40
+ .uint32('crc32');
41
+
42
+ this.logger = logger;
43
+ }
44
+
45
+ public connect(): void {
46
+ try {
47
+ this.server = dgram.createSocket('udp4');
48
+ this.server.bind(this.PORT);
49
+
50
+ this.server.on('message', this.onMessage.bind(this));
51
+ this.server.on('error', this.onError.bind(this));
52
+ } catch (err) {
53
+ this.logger.error(`Failed to create UDP socket: ${err}`);
54
+ this.server = undefined;
55
+ }
56
+ }
57
+
58
+ public disconnect(): Promise<void> {
59
+ if (this.server) {
60
+ return new Promise<void>((resolve) => {
61
+ this.server?.close(() => {
62
+ this.server = undefined;
63
+ resolve();
64
+ });
65
+ });
66
+ }
67
+
68
+ return Promise.resolve();
69
+ }
70
+
71
+ public override send(duid: string, request: RequestMessage): Promise<void> {
72
+ this.logger.debug(`Sending request to ${duid}: ${JSON.stringify(request)}`);
73
+ return Promise.resolve();
74
+ }
75
+
76
+ private async onError(result: any) {
77
+ this.logger.error(`UDP socket error: ${result}`);
78
+
79
+ if (this.server) {
80
+ this.server.close();
81
+ this.server = undefined;
82
+ }
83
+ }
84
+
85
+ private async onMessage(buffer: Buffer) {
86
+ const message = await this.deserializeMessage(buffer);
87
+ this.logger.debug('Received message: ' + JSON.stringify(message));
88
+ }
89
+
90
+ private async deserializeMessage(buffer: Buffer): Promise<ResponseMessage | undefined> {
91
+ const version = buffer.toString('latin1', 0, 3);
92
+
93
+ if (version !== '1.0' && version !== 'L01' && version !== 'A01') {
94
+ throw new Error('unknown protocol version ' + version);
95
+ }
96
+
97
+ let data;
98
+ switch (version) {
99
+ case '1.0':
100
+ data = await this.deserializeV10Message(buffer);
101
+ return JSON.parse(data);
102
+ case 'L01':
103
+ data = await this.deserializeL01Message(buffer);
104
+ return JSON.parse(data);
105
+ case 'A01':
106
+ // TODO: Implement A01 deserialization
107
+ return undefined; // Placeholder for A01 deserialization
108
+ default:
109
+ throw new Error('unknown protocol version ' + version);
110
+ }
111
+ }
112
+
113
+ private async deserializeV10Message(message: Buffer<ArrayBufferLike>): Promise<string> {
114
+ const data = this.V10Parser.parse(message);
115
+ const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0;
116
+ const expectedCrc32 = data.crc32;
117
+ if (crc32 != expectedCrc32) {
118
+ throw new Error('wrong CRC32 ' + crc32 + ', expected ' + expectedCrc32);
119
+ }
120
+
121
+ const decipher = crypto.createDecipheriv('aes-128-ecb', Buffer.from('qWKYcdQWrbm9hPqe', 'utf8'), null);
122
+ decipher.setAutoPadding(false);
123
+
124
+ let decrypted = decipher.update(data.payload, 'binary', 'utf8');
125
+ decrypted += decipher.final('utf8');
126
+
127
+ const paddingLength = decrypted.charCodeAt(decrypted.length - 1);
128
+ return decrypted.slice(0, -paddingLength);
129
+ }
130
+
131
+ private async deserializeL01Message(message: Buffer<ArrayBufferLike>): Promise<string> {
132
+ const data = this.L01Parser.parse(message);
133
+ const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0;
134
+ const expectedCrc32 = data.crc32;
135
+ if (crc32 != expectedCrc32) {
136
+ throw new Error('wrong CRC32 ' + crc32 + ', expected ' + expectedCrc32);
137
+ }
138
+
139
+ const payload = data.payload;
140
+ const key = crypto.createHash('sha256').update(Buffer.from('qWKYcdQWrbm9hPqe', 'utf8')).digest();
141
+ const digestInput = message.subarray(0, 9);
142
+ const digest = crypto.createHash('sha256').update(digestInput).digest();
143
+ const iv = digest.subarray(0, 12);
144
+ const tag = payload.subarray(payload.length - 16);
145
+ const ciphertext = payload.subarray(0, payload.length - 16);
146
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
147
+ decipher.setAuthTag(tag);
148
+
149
+ try {
150
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
151
+ return decrypted.toString('utf8');
152
+ } catch (e: unknown) {
153
+ const message = e && typeof e === 'object' && 'message' in e ? (e as any).message : String(e);
154
+ throw new Error('failed to decrypt: ' + message + ' / iv: ' + iv.toString('hex') + ' / tag: ' + tag.toString('hex') + ' / encrypted: ' + ciphertext.toString('hex'));
155
+ }
156
+ }
157
+ }
@@ -0,0 +1,174 @@
1
+ import mqtt, { ErrorWithReasonCode, IConnackPacket, ISubscriptionGrant, MqttClient as MqttLibClient } from 'mqtt';
2
+ import * as 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, debugStringify } from 'matterbridge/logger';
8
+
9
+ export class MQTTClient extends AbstractClient {
10
+ protected override clientName = 'MQTTClient';
11
+ protected override shouldReconnect = false;
12
+
13
+ private readonly rriot: Rriot;
14
+ private readonly mqttUsername: string;
15
+ private readonly mqttPassword: string;
16
+ private mqttClient: MqttLibClient | undefined = undefined;
17
+ private keepConnectionAliveInterval: NodeJS.Timeout | undefined = undefined;
18
+
19
+ public constructor(logger: AnsiLogger, context: MessageContext, userdata: UserData) {
20
+ super(logger, context);
21
+ this.rriot = userdata.rriot;
22
+
23
+ this.mqttUsername = CryptoUtils.md5hex(userdata.rriot.u + ':' + userdata.rriot.k).substring(2, 10);
24
+ this.mqttPassword = CryptoUtils.md5hex(userdata.rriot.s + ':' + userdata.rriot.k).substring(16);
25
+
26
+ this.initializeConnectionStateListener();
27
+ }
28
+
29
+ public connect(): void {
30
+ if (this.mqttClient) {
31
+ return; // Already connected
32
+ }
33
+
34
+ this.mqttClient = mqtt.connect(this.rriot.r.m, {
35
+ clientId: this.mqttUsername,
36
+ username: this.mqttUsername,
37
+ password: this.mqttPassword,
38
+ keepalive: 30,
39
+ log: () => {
40
+ // ...args: unknown[] this.logger.debug(`MQTTClient args: ${debugStringify(args)}`);
41
+ },
42
+ });
43
+
44
+ this.mqttClient.on('connect', this.onConnect.bind(this));
45
+ this.mqttClient.on('error', this.onError.bind(this));
46
+ this.mqttClient.on('reconnect', this.onReconnect.bind(this));
47
+ this.mqttClient.on('close', this.onClose.bind(this));
48
+ this.mqttClient.on('disconnect', this.onDisconnect.bind(this));
49
+ this.mqttClient.on('offline', this.onOffline.bind(this));
50
+ this.mqttClient.on('message', this.onMessage.bind(this));
51
+
52
+ this.keepConnectionAlive();
53
+ }
54
+
55
+ public async disconnect(): Promise<void> {
56
+ if (!this.mqttClient || !this.connected) {
57
+ return;
58
+ }
59
+ try {
60
+ this.isInDisconnectingStep = true;
61
+ this.mqttClient.end();
62
+ } catch (error) {
63
+ this.logger.error('MQTT client failed to disconnect with error: ' + error);
64
+ }
65
+ }
66
+
67
+ public async send(duid: string, request: RequestMessage): Promise<void> {
68
+ if (!this.mqttClient || !this.connected) {
69
+ this.logger.error(`${duid}: mqtt is not available, ${debugStringify(request)}`);
70
+ return;
71
+ }
72
+
73
+ const mqttRequest = request.toMqttRequest();
74
+ const message = this.serializer.serialize(duid, mqttRequest);
75
+ this.logger.debug(`MQTTClient sending message to ${duid}: ${debugStringify(mqttRequest)}`);
76
+ this.mqttClient.publish(`rr/m/i/${this.rriot.u}/${this.mqttUsername}/${duid}`, message.buffer, { qos: 1 });
77
+ this.logger.debug(`MQTTClient published message to topic: rr/m/i/${this.rriot.u}/${this.mqttUsername}/${duid}`);
78
+ }
79
+
80
+ private keepConnectionAlive(): void {
81
+ if (this.keepConnectionAliveInterval) {
82
+ clearTimeout(this.keepConnectionAliveInterval);
83
+ this.keepConnectionAliveInterval.unref();
84
+ }
85
+
86
+ this.keepConnectionAliveInterval = setInterval(
87
+ () => {
88
+ if (this.mqttClient) {
89
+ this.mqttClient.end();
90
+ this.mqttClient.reconnect();
91
+ } else {
92
+ this.connect();
93
+ }
94
+ },
95
+ 30 * 60 * 1000,
96
+ );
97
+ }
98
+
99
+ private async onConnect(result: IConnackPacket): Promise<void> {
100
+ if (!result) {
101
+ return;
102
+ }
103
+
104
+ this.connected = true;
105
+ await this.connectionListeners.onConnected('mqtt-' + this.mqttUsername);
106
+ this.subscribeToQueue();
107
+ }
108
+
109
+ private subscribeToQueue(): void {
110
+ if (!this.mqttClient || !this.connected) {
111
+ return;
112
+ }
113
+
114
+ this.mqttClient.subscribe('rr/m/o/' + this.rriot.u + '/' + this.mqttUsername + '/#', this.onSubscribe.bind(this));
115
+ }
116
+
117
+ private async onSubscribe(err: Error | null, granted: ISubscriptionGrant[] | undefined): Promise<void> {
118
+ if (!err) {
119
+ this.logger.info('onSubscribe: ' + JSON.stringify(granted));
120
+ return;
121
+ }
122
+
123
+ this.logger.error('failed to subscribe: ' + err);
124
+ this.connected = false;
125
+
126
+ await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername, 'Failed to subscribe to the queue: ' + err.toString());
127
+ }
128
+
129
+ private async onDisconnect(): Promise<void> {
130
+ this.connected = false;
131
+ await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername, 'Disconnected from MQTT broker');
132
+ }
133
+
134
+ private async onError(result: Error | ErrorWithReasonCode): Promise<void> {
135
+ this.logger.error('MQTT connection error: ' + result);
136
+ this.connected = false;
137
+
138
+ await this.connectionListeners.onError('mqtt-' + this.mqttUsername, result.toString());
139
+ }
140
+
141
+ private async onClose(): Promise<void> {
142
+ if (this.connected) {
143
+ await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername, 'MQTT connection closed');
144
+ }
145
+
146
+ this.connected = false;
147
+ }
148
+
149
+ private async onOffline(): Promise<void> {
150
+ this.connected = false;
151
+ await this.connectionListeners.onDisconnected('mqtt-' + this.mqttUsername, 'MQTT connection offline');
152
+ }
153
+
154
+ private onReconnect(): void {
155
+ this.subscribeToQueue();
156
+ this.connectionListeners.onReconnect('mqtt-' + this.mqttUsername, 'Reconnected to MQTT broker');
157
+ }
158
+
159
+ private async onMessage(topic: string, message: Buffer<ArrayBufferLike>): Promise<void> {
160
+ if (!message) {
161
+ // Ignore empty messages
162
+ this.logger.notice('MQTTClient received empty message from topic: ' + topic);
163
+ return;
164
+ }
165
+
166
+ try {
167
+ const duid = topic.split('/').slice(-1)[0];
168
+ const response = this.deserializer.deserialize(duid, message);
169
+ await this.messageListeners.onMessage(response);
170
+ } catch (error) {
171
+ this.logger.error('MQTTClient: unable to process message with error: ' + topic + ': ' + error);
172
+ }
173
+ }
174
+ }
@@ -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 | undefined>;
19
+ }
@@ -0,0 +1,104 @@
1
+ import { AnsiLogger } from 'matterbridge/logger';
2
+ import { AbstractConnectionListener } from './listener/abstractConnectionListener.js';
3
+ import { AbstractMessageListener } from './listener/abstractMessageListener.js';
4
+ import { RequestMessage } from './model/requestMessage.js';
5
+ import { UserData } from '../Zmodel/userData.js';
6
+ import { Client } from './client.js';
7
+ import { ChainedConnectionListener } from './listener/implementation/chainedConnectionListener.js';
8
+ import { ChainedMessageListener } from './listener/implementation/chainedMessageListener.js';
9
+ import { MessageContext } from './model/messageContext.js';
10
+ import { LocalNetworkClient } from './client/LocalNetworkClient.js';
11
+ import { MQTTClient } from './client/MQTTClient.js';
12
+
13
+ export class ClientRouter implements Client {
14
+ protected readonly connectionListeners = new ChainedConnectionListener();
15
+ protected readonly messageListeners = new ChainedMessageListener();
16
+
17
+ private readonly context: MessageContext;
18
+ private readonly localClients = new Map<string, LocalNetworkClient>();
19
+ private readonly logger: AnsiLogger;
20
+ private mqttClient: MQTTClient;
21
+
22
+ public constructor(logger: AnsiLogger, userdata: UserData) {
23
+ this.context = new MessageContext(userdata);
24
+ this.logger = logger;
25
+
26
+ this.mqttClient = new MQTTClient(logger, this.context, userdata);
27
+ this.mqttClient.registerConnectionListener(this.connectionListeners);
28
+ this.mqttClient.registerMessageListener(this.messageListeners);
29
+ }
30
+
31
+ public registerDevice(duid: string, localKey: string, pv: string, nonce: number | undefined): void {
32
+ this.context.registerDevice(duid, localKey, pv, nonce);
33
+ }
34
+
35
+ public updateNonce(duid: string, nonce: number): void {
36
+ this.context.updateNonce(duid, nonce);
37
+ }
38
+
39
+ public registerClient(duid: string, ip: string): Client {
40
+ const localClient = new LocalNetworkClient(this.logger, this.context, duid, ip);
41
+ localClient.registerConnectionListener(this.connectionListeners);
42
+ localClient.registerMessageListener(this.messageListeners);
43
+
44
+ this.localClients.set(duid, localClient);
45
+ return localClient;
46
+ }
47
+
48
+ public unregisterClient(duid: string): void {
49
+ this.localClients.delete(duid);
50
+ }
51
+
52
+ public registerConnectionListener(listener: AbstractConnectionListener): void {
53
+ this.connectionListeners.register(listener);
54
+ }
55
+
56
+ public registerMessageListener(listener: AbstractMessageListener): void {
57
+ this.messageListeners.register(listener);
58
+ }
59
+
60
+ public isConnected(): boolean {
61
+ return this.mqttClient.isConnected();
62
+ }
63
+
64
+ public connect(): void {
65
+ this.mqttClient.connect();
66
+
67
+ this.localClients.forEach((client) => {
68
+ client.connect();
69
+ });
70
+ }
71
+
72
+ async disconnect(): Promise<void> {
73
+ await this.mqttClient.disconnect();
74
+
75
+ this.localClients.forEach((client) => {
76
+ client.disconnect();
77
+ });
78
+ }
79
+
80
+ async send(duid: string, request: RequestMessage): Promise<void> {
81
+ if (request.secure) {
82
+ await this.mqttClient.send(duid, request);
83
+ } else {
84
+ await this.getClient(duid).send(duid, request);
85
+ }
86
+ }
87
+
88
+ async get<T>(duid: string, request: RequestMessage): Promise<T | undefined> {
89
+ if (request.secure) {
90
+ return await this.mqttClient.get(duid, request);
91
+ } else {
92
+ return await this.getClient(duid).get(duid, request);
93
+ }
94
+ }
95
+
96
+ private getClient(duid: string): Client {
97
+ const localClient = this.localClients.get(duid);
98
+ if (localClient === undefined || !localClient.isConnected()) {
99
+ return this.mqttClient;
100
+ } else {
101
+ return localClient;
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,6 @@
1
+ export interface AbstractConnectionListener {
2
+ onConnected(duid: string): Promise<void>;
3
+ onDisconnected(duid: string, message: string): Promise<void>;
4
+ onError(duid: string, message: string): Promise<void>;
5
+ onReconnect(duid: string, message: string): Promise<void>;
6
+ }
@@ -0,0 +1,11 @@
1
+ import { VacuumErrorCode } from '../../Zenum/vacuumAndDockErrorCode.js';
2
+
3
+ /*
4
+ Skeleton only, implementation is no needed
5
+ */
6
+ export interface AbstractMessageHandler {
7
+ onError(error: VacuumErrorCode): Promise<void>;
8
+ onBatteryUpdate(percentage: number): Promise<void>;
9
+ onStatusChanged(): Promise<void>;
10
+ onAdditionalProps(value: number): Promise<void>;
11
+ }
@@ -0,0 +1,5 @@
1
+ import { ResponseMessage } from '../model/responseMessage.js';
2
+
3
+ export interface AbstractMessageListener {
4
+ onMessage(message: ResponseMessage): Promise<void>;
5
+ }
@@ -0,0 +1,33 @@
1
+ import { AbstractConnectionListener } from '../abstractConnectionListener.js';
2
+
3
+ export class ChainedConnectionListener implements AbstractConnectionListener {
4
+ private listeners: AbstractConnectionListener[] = [];
5
+
6
+ public register(listener: AbstractConnectionListener): void {
7
+ this.listeners.push(listener);
8
+ }
9
+
10
+ public async onConnected(duid: string): Promise<void> {
11
+ for (const listener of this.listeners) {
12
+ await listener.onConnected(duid);
13
+ }
14
+ }
15
+
16
+ public async onDisconnected(duid: string, message: string): Promise<void> {
17
+ for (const listener of this.listeners) {
18
+ await listener.onDisconnected(duid, message);
19
+ }
20
+ }
21
+
22
+ public async onError(duid: string, message: string): Promise<void> {
23
+ for (const listener of this.listeners) {
24
+ await listener.onError(duid, message);
25
+ }
26
+ }
27
+
28
+ public async onReconnect(duid: string, message: string): Promise<void> {
29
+ for (const listener of this.listeners) {
30
+ await listener.onReconnect(duid, message);
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,16 @@
1
+ import { ResponseMessage } from '../../model/responseMessage.js';
2
+ import { AbstractMessageListener } from '../abstractMessageListener.js';
3
+
4
+ export class ChainedMessageListener implements AbstractMessageListener {
5
+ private listeners: AbstractMessageListener[] = [];
6
+
7
+ public register(listener: AbstractMessageListener): void {
8
+ this.listeners.push(listener);
9
+ }
10
+
11
+ public async onMessage(message: ResponseMessage): Promise<void> {
12
+ for (const listener of this.listeners) {
13
+ await listener.onMessage(message);
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,57 @@
1
+ import { AnsiLogger } from 'matterbridge/logger';
2
+ import { AbstractConnectionListener } from '../abstractConnectionListener.js';
3
+ import { AbstractClient } from '../../abstractClient.js';
4
+
5
+ export class ConnectionStateListener implements AbstractConnectionListener {
6
+ protected logger: AnsiLogger;
7
+ protected client: AbstractClient;
8
+ protected clientName: string;
9
+ protected shouldReconnect: boolean;
10
+
11
+ constructor(logger: AnsiLogger, client: AbstractClient, clientName: string, shouldReconnect = false) {
12
+ this.logger = logger;
13
+ this.client = client;
14
+ this.clientName = clientName;
15
+ this.shouldReconnect = shouldReconnect;
16
+ }
17
+
18
+ public async onConnected(duid: string): Promise<void> {
19
+ this.logger.notice(`Device ${duid} connected to ${this.clientName}`);
20
+ }
21
+
22
+ public async onReconnect(duid: string, message: string): Promise<void> {
23
+ this.logger.info(`Device ${duid} reconnected to ${this.clientName} with message: ${message}`);
24
+ }
25
+
26
+ public async onDisconnected(duid: string, message: string): Promise<void> {
27
+ this.logger.error(`Device ${duid} disconnected from ${this.clientName} with message: ${message}`);
28
+ if (!this.shouldReconnect) {
29
+ this.logger.notice(`Device ${duid} disconnected from ${this.clientName}, but re-registration is disabled.`);
30
+ return;
31
+ }
32
+
33
+ if (this.client.retryCount > 10) {
34
+ this.logger.error(`Device with DUID ${duid} has exceeded retry limit, not re-registering.`);
35
+ return;
36
+ }
37
+
38
+ this.client.retryCount++;
39
+
40
+ const isInDisconnectingStep = this.client.isInDisconnectingStep;
41
+ if (isInDisconnectingStep) {
42
+ this.logger.info(`Device with DUID ${duid} is in disconnecting step, skipping re-registration.`);
43
+ return;
44
+ }
45
+
46
+ setTimeout(() => {
47
+ this.logger.info(`Re-registering device with DUID ${duid} to ${this.clientName}`);
48
+ this.client.connect();
49
+ }, 10000);
50
+
51
+ this.client.isInDisconnectingStep = false;
52
+ }
53
+
54
+ public async onError(duid: string, message: string): Promise<void> {
55
+ this.logger.error(`Error on device with DUID ${duid}: ${message}`);
56
+ }
57
+ }
@@ -0,0 +1,38 @@
1
+ import { Protocol } from '../../model/protocol.js';
2
+ import { ResponseMessage } from '../../model/responseMessage.js';
3
+ import { AbstractMessageListener } from '../abstractMessageListener.js';
4
+
5
+ export class GeneralSyncMessageListener implements AbstractMessageListener {
6
+ private readonly duid: string;
7
+
8
+ private handler?: (data: ResponseMessage) => void;
9
+ private timer?: NodeJS.Timeout;
10
+
11
+ constructor(duid: string) {
12
+ this.duid = duid;
13
+ }
14
+
15
+ public waitFor(): Promise<ResponseMessage> {
16
+ return new Promise<ResponseMessage>((resolve, reject) => {
17
+ this.handler = resolve;
18
+ this.timer = setTimeout(() => {
19
+ reject('no ping response received for ' + this.duid + ' within ' + 30 + 'second');
20
+ }, 30 * 1000);
21
+ });
22
+ }
23
+
24
+ public async onMessage(message: ResponseMessage): Promise<void> {
25
+ if (message.contain(Protocol.hello_response)) {
26
+ // trigger our waiters that we have received the response.
27
+ if (this.handler) {
28
+ this.handler(message);
29
+ }
30
+
31
+ // cleanup the timer
32
+ if (this.timer) {
33
+ clearTimeout(this.timer);
34
+ this.timer.unref();
35
+ }
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,37 @@
1
+ import { ResponseMessage } from '../../model/responseMessage.js';
2
+ import { AbstractMessageListener } from '../index.js';
3
+ import { Protocol } from '../../model/protocol.js';
4
+ import { AbstractMessageHandler } from '../abstractMessageHandler.js';
5
+
6
+ export class SimpleMessageListener implements AbstractMessageListener {
7
+ private handler: AbstractMessageHandler | undefined;
8
+
9
+ public registerListener(handler: AbstractMessageHandler): void {
10
+ this.handler = handler;
11
+ }
12
+
13
+ public async onMessage(message: ResponseMessage): Promise<void> {
14
+ if (!this.handler || message.contain(Protocol.rpc_response) || message.contain(Protocol.map_response)) {
15
+ return;
16
+ }
17
+
18
+ if (message.contain(Protocol.status_update) && this.handler.onStatusChanged) {
19
+ await this.handler.onStatusChanged();
20
+ }
21
+
22
+ if (message.contain(Protocol.error) && this.handler.onError) {
23
+ const value = message.get(Protocol.error) as string;
24
+ await this.handler.onError(Number(value));
25
+ }
26
+
27
+ if (message.contain(Protocol.battery) && this.handler.onBatteryUpdate) {
28
+ const value = message.get(Protocol.battery) as string;
29
+ await this.handler.onBatteryUpdate(Number(value));
30
+ }
31
+
32
+ if (message.contain(Protocol.additional_props) && this.handler.onAdditionalProps) {
33
+ const value = message.get(Protocol.additional_props) as string;
34
+ await this.handler.onAdditionalProps(Number(value));
35
+ }
36
+ }
37
+ }