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,50 @@
1
+ import { AnsiLogger, debugStringify } from 'matterbridge/logger';
2
+ import { DpsPayload } from '../../model/dps.js';
3
+ import { Protocol } from '../../model/protocol.js';
4
+ import { ResponseMessage } from '../../model/responseMessage.js';
5
+ import { AbstractMessageListener } from '../index.js';
6
+ import { RequestMessage } from '../../model/requestMessage.js';
7
+
8
+ export class SyncMessageListener implements AbstractMessageListener {
9
+ private readonly pending = new Map<number, (response: ResponseMessage) => void>();
10
+ logger: AnsiLogger;
11
+
12
+ constructor(logger: AnsiLogger) {
13
+ this.logger = logger;
14
+ }
15
+
16
+ public waitFor(messageId: number, request: RequestMessage, resolve: (response: ResponseMessage) => void, reject: (error?: Error) => void): void {
17
+ this.pending.set(messageId, resolve);
18
+
19
+ setTimeout(() => {
20
+ this.pending.delete(messageId);
21
+ reject(new Error(`Message timeout for messageId: ${messageId}, request: ${debugStringify(request)}`));
22
+ }, 10000);
23
+ }
24
+
25
+ public async onMessage(message: ResponseMessage): Promise<void> {
26
+ if (message.contain(Protocol.rpc_response)) {
27
+ const dps = message.get(Protocol.rpc_response) as DpsPayload;
28
+ const messageId = dps.id;
29
+
30
+ const responseHandler = this.pending.get(messageId);
31
+ // const result = dps.result as Record<string, unknown>;
32
+ // if (result && result.length == 1 && result[0] == 'ok') {
33
+ // return;
34
+ // }
35
+
36
+ if (responseHandler) {
37
+ responseHandler(dps.result as ResponseMessage);
38
+ }
39
+ this.pending.delete(messageId);
40
+ return;
41
+ }
42
+
43
+ // map data
44
+ if (message.contain(Protocol.map_response)) {
45
+ const dps = message.get(Protocol.map_response) as DpsPayload;
46
+ this.pending.delete(dps.id);
47
+ return;
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,3 @@
1
+ export type { AbstractConnectionListener } from './abstractConnectionListener.js';
2
+ export type { AbstractMessageListener } from './abstractMessageListener.js';
3
+ export { SimpleMessageListener } from './implementation/simpleMessageListener.js';
@@ -0,0 +1,184 @@
1
+ import { AnsiLogger, debugStringify } from 'matterbridge/logger';
2
+ import { CloudMessageResult } from '../Zmodel/messageResult.js';
3
+ import { AbstractMessageHandler } from './listener/abstractMessageHandler.js';
4
+ import { SimpleMessageListener } from './listener/index.js';
5
+ import { RequestMessage } from './model/requestMessage.js';
6
+ import { DeviceStatus } from '../Zmodel/deviceStatus.js';
7
+ import { Client } from './client.js';
8
+ import { NetworkInfo } from '../Zmodel/networkInfo.js';
9
+ import { CleanModeSetting } from '../../behaviors/roborock.vacuum/default/default.js';
10
+
11
+ export class MessageProcessor {
12
+ private readonly client: Client;
13
+ private readonly messageListener: SimpleMessageListener;
14
+ logger: AnsiLogger | undefined;
15
+
16
+ constructor(client: Client) {
17
+ this.client = client;
18
+
19
+ this.messageListener = new SimpleMessageListener();
20
+ this.client.registerMessageListener(this.messageListener);
21
+ }
22
+
23
+ public injectLogger(logger: AnsiLogger): void {
24
+ this.logger = logger;
25
+ }
26
+
27
+ public registerListener(listener: AbstractMessageHandler): void {
28
+ this.messageListener.registerListener(listener);
29
+ }
30
+
31
+ public async getNetworkInfo(duid: string): Promise<NetworkInfo | undefined> {
32
+ const request = new RequestMessage({ method: 'get_network_info' });
33
+ return await this.client.get(duid, request);
34
+ }
35
+
36
+ public async getDeviceStatus(duid: string): Promise<DeviceStatus | undefined> {
37
+ const request = new RequestMessage({ method: 'get_status' });
38
+ const response = await this.client.get<CloudMessageResult[]>(duid, request);
39
+
40
+ if (response) {
41
+ this.logger?.debug('Device status: ', debugStringify(response));
42
+ return new DeviceStatus(response);
43
+ }
44
+
45
+ return undefined;
46
+ }
47
+
48
+ public async getDeviceStatusOverMQTT(duid: string): Promise<DeviceStatus | undefined> {
49
+ const request = new RequestMessage({ method: 'get_status', secure: true });
50
+ const response = await this.client.get<CloudMessageResult[]>(duid, request);
51
+
52
+ if (response) {
53
+ this.logger?.debug('MQTT - Device status: ', debugStringify(response));
54
+ return new DeviceStatus(response);
55
+ }
56
+
57
+ return undefined;
58
+ }
59
+
60
+ public async getRooms(duid: string): Promise<number[][] | undefined> {
61
+ const request = new RequestMessage({ method: 'get_room_mapping' });
62
+ return this.client.get<number[][] | undefined>(duid, request); // .then((response) => new RoomInfo(rooms, response ?? []));
63
+ }
64
+
65
+ public async gotoDock(duid: string): Promise<void> {
66
+ const request = new RequestMessage({ method: 'app_charge' });
67
+ return this.client.send(duid, request);
68
+ }
69
+
70
+ public async startClean(duid: string): Promise<void> {
71
+ const request = new RequestMessage({ method: 'app_start' });
72
+ return this.client.send(duid, request);
73
+ }
74
+
75
+ public async startRoomClean(duid: string, roomIds: number[], repeat: number): Promise<void> {
76
+ const request = new RequestMessage({
77
+ method: 'app_segment_clean',
78
+ params: [{ segments: roomIds, repeat: repeat }],
79
+ });
80
+ return this.client.send(duid, request);
81
+ }
82
+
83
+ public async pauseClean(duid: string): Promise<void> {
84
+ const request = new RequestMessage({ method: 'app_pause' });
85
+ return this.client.send(duid, request);
86
+ }
87
+
88
+ public async resumeClean(duid: string): Promise<void> {
89
+ const request = new RequestMessage({ method: 'app_resume' });
90
+ return this.client.send(duid, request);
91
+ }
92
+
93
+ public async stopClean(duid: string): Promise<void> {
94
+ const request = new RequestMessage({ method: 'app_stop' });
95
+ return this.client.send(duid, request);
96
+ }
97
+
98
+ public async sendCustomMessage(duid: string, def: RequestMessage): Promise<void> {
99
+ const request = new RequestMessage(def);
100
+ return this.client.send(duid, request);
101
+ }
102
+
103
+ public getCustomMessage(duid: string, def: RequestMessage): Promise<unknown> {
104
+ return this.client.get(duid, def);
105
+ }
106
+
107
+ public async findMyRobot(duid: string): Promise<unknown> {
108
+ const request = new RequestMessage({ method: 'find_me' });
109
+ return this.client.get(duid, request);
110
+ }
111
+
112
+ public async getCleanModeData(duid: string): Promise<CleanModeSetting> {
113
+ const currentMopMode = await this.getCustomMessage(duid, new RequestMessage({ method: 'get_mop_mode' }));
114
+ const suctionPowerRaw = await this.getCustomMessage(duid, new RequestMessage({ method: 'get_custom_mode' }));
115
+ const waterFlowRaw = await this.getCustomMessage(duid, new RequestMessage({ method: 'get_water_box_custom_mode' }));
116
+
117
+ let suctionPower: number;
118
+ let waterFlow: number;
119
+ let mopRoute: number;
120
+ let distance_off = 0;
121
+
122
+ if (Array.isArray(suctionPowerRaw)) {
123
+ suctionPower = suctionPowerRaw[0];
124
+ } else {
125
+ suctionPower = suctionPowerRaw as number;
126
+ }
127
+
128
+ if (Array.isArray(currentMopMode)) {
129
+ mopRoute = currentMopMode[0];
130
+ } else {
131
+ mopRoute = currentMopMode as number;
132
+ }
133
+
134
+ if (typeof waterFlowRaw === 'object' && waterFlowRaw !== null && 'water_box_mode' in waterFlowRaw) {
135
+ waterFlow = waterFlowRaw.water_box_mode as number;
136
+
137
+ if ('distance_off' in waterFlowRaw) {
138
+ distance_off = (waterFlowRaw.distance_off as number) ?? 0;
139
+ }
140
+ } else {
141
+ waterFlow = waterFlowRaw as number;
142
+ }
143
+
144
+ return {
145
+ suctionPower: suctionPower,
146
+ waterFlow: waterFlow,
147
+ distance_off: distance_off,
148
+ mopRoute: mopRoute,
149
+ } satisfies CleanModeSetting;
150
+ }
151
+
152
+ public async changeCleanMode(duid: string, suctionPower: number, waterFlow: number, mopRoute: number, distance_off: number): Promise<void> {
153
+ this.logger?.notice(`Change clean mode for ${duid} to suctionPower: ${suctionPower}, waterFlow: ${waterFlow}, mopRoute: ${mopRoute}, distance_off: ${distance_off}`);
154
+
155
+ const currentMopMode = await this.getCustomMessage(duid, new RequestMessage({ method: 'get_custom_mode' }));
156
+ const smartMopMode = 110;
157
+ const smartMopRoute = 306;
158
+ const customMopMode = 106;
159
+ const customMopRoute = 302;
160
+
161
+ if (currentMopMode == smartMopMode && mopRoute == smartMopRoute) return;
162
+ if (currentMopMode == customMopMode && mopRoute == customMopRoute) return;
163
+
164
+ // if change mode from smart plan, firstly change to custom
165
+ if (currentMopMode == smartMopMode) {
166
+ await this.client.send(duid, new RequestMessage({ method: 'set_mop_mode', params: [customMopRoute] }));
167
+ }
168
+
169
+ if (suctionPower && suctionPower != 0) {
170
+ await this.client.send(duid, new RequestMessage({ method: 'set_custom_mode', params: [suctionPower] }));
171
+ }
172
+
173
+ const CustomizeWithDistanceOff = 207;
174
+ if (waterFlow && waterFlow == CustomizeWithDistanceOff && distance_off && distance_off != 0) {
175
+ await this.client.send(duid, new RequestMessage({ method: 'set_water_box_custom_mode', params: { 'water_box_mode': waterFlow, 'distance_off': distance_off } }));
176
+ } else if (waterFlow && waterFlow != 0) {
177
+ await this.client.send(duid, new RequestMessage({ method: 'set_water_box_custom_mode', params: [waterFlow] }));
178
+ }
179
+
180
+ if (mopRoute && mopRoute != 0) {
181
+ await this.client.send(duid, new RequestMessage({ method: 'set_mop_mode', params: [mopRoute] }));
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,5 @@
1
+ export interface ContentMessage {
2
+ payloadLen?: number;
3
+ payload: Buffer<ArrayBufferLike>;
4
+ crc32: number;
5
+ }
@@ -0,0 +1,19 @@
1
+ interface Security {
2
+ endpoint: string;
3
+ nonce: string;
4
+ }
5
+
6
+ export interface DpsPayload {
7
+ id: number;
8
+ method?: string;
9
+ params?: unknown[] | Record<string, unknown> | undefined;
10
+ security?: Security;
11
+ result: unknown;
12
+ }
13
+
14
+ export type Dps = Record<number | string, string | DpsPayload>;
15
+
16
+ export interface Payload {
17
+ dps: Dps;
18
+ t: number;
19
+ }
@@ -0,0 +1,7 @@
1
+ export interface HeaderMessage {
2
+ version: string;
3
+ seq: number;
4
+ nonce: number;
5
+ timestamp: number;
6
+ protocol: number;
7
+ }
@@ -0,0 +1,53 @@
1
+ import { randomBytes, randomInt } from 'node:crypto';
2
+ import * as CryptoUtils from '../../helper/cryptoHelper.js';
3
+ import { UserData } from '../../Zmodel/userData.js';
4
+
5
+ interface DeviceInfo {
6
+ localKey: string;
7
+ protocolVersion: string;
8
+ nonce: number | undefined;
9
+ }
10
+
11
+ export class MessageContext {
12
+ private readonly endpoint: string;
13
+ private readonly devices = new Map<string, DeviceInfo>();
14
+ public readonly nonce: number;
15
+ public readonly serializeNonce: Buffer;
16
+
17
+ constructor(userdata: UserData) {
18
+ this.endpoint = CryptoUtils.md5bin(userdata.rriot.k).subarray(8, 14).toString('base64');
19
+ this.nonce = randomInt(1000, 1000000);
20
+ this.serializeNonce = randomBytes(16);
21
+ }
22
+
23
+ public registerDevice(duid: string, localKey: string, pv: string, nonce: number | undefined) {
24
+ this.devices.set(duid, { localKey: localKey, protocolVersion: pv, nonce });
25
+ }
26
+
27
+ public updateNonce(duid: string, nonce: number): void {
28
+ const device = this.devices.get(duid);
29
+ if (device) {
30
+ device.nonce = nonce;
31
+ }
32
+ }
33
+
34
+ public getSerializeNonceAsHex(): string {
35
+ return this.serializeNonce.toString('hex').toUpperCase();
36
+ }
37
+
38
+ public getLocalKey(duid: string): string | undefined {
39
+ return this.devices.get(duid)?.localKey;
40
+ }
41
+
42
+ public getProtocolVersion(duid: string): string | undefined {
43
+ return this.devices.get(duid)?.protocolVersion;
44
+ }
45
+
46
+ public getDeviceNonce(duid: string): number | undefined {
47
+ return this.devices.get(duid)?.nonce;
48
+ }
49
+
50
+ public getEndpoint(): string {
51
+ return this.endpoint;
52
+ }
53
+ }
@@ -0,0 +1,28 @@
1
+ export enum Protocol {
2
+ hello_request = 0,
3
+ hello_response = 1,
4
+ ping_request = 2,
5
+ ping_response = 3,
6
+ general_request = 4,
7
+ general_response = 5,
8
+ rpc_request = 101,
9
+ rpc_response = 102,
10
+ error = 120,
11
+ status_update = 121,
12
+ battery = 122,
13
+ suction_power = 123,
14
+ water_box_mode = 124,
15
+ main_brush_work_time = 125,
16
+ side_brush_work_time = 126,
17
+ filter_work_time = 127,
18
+ additional_props = 128,
19
+ task_complete = 130,
20
+ task_cancel_low_power = 131,
21
+ task_cancel_in_motion = 132,
22
+ charge_status = 133,
23
+ drying_status = 134,
24
+ back_type = 139, // WTF is this
25
+ map_response = 301,
26
+ some_thing_happened_when_socket_closed = 500,
27
+ }
28
+ // "deviceStatus":{"120":0,"121":8,"122":100,"123":110,"124":209,"125":99,"126":96,"127":97,"128":0,"133":1,"134":1,"135":0,"139":0}
@@ -0,0 +1,51 @@
1
+ import { Protocol } from './protocol.js';
2
+ import { randomInt } from 'node:crypto';
3
+
4
+ export interface ProtocolRequest {
5
+ messageId?: number;
6
+ protocol?: Protocol;
7
+ method?: string | undefined;
8
+ params?: unknown[] | Record<string, unknown> | undefined;
9
+ secure?: boolean;
10
+ nonce?: number;
11
+ timestamp?: number;
12
+ }
13
+
14
+ export class RequestMessage {
15
+ public readonly messageId: number;
16
+ public readonly protocol: Protocol;
17
+ public readonly method: string | undefined;
18
+ public readonly params: unknown[] | Record<string, unknown> | undefined;
19
+ public readonly secure: boolean;
20
+ public readonly timestamp: number;
21
+ public readonly nonce: number;
22
+
23
+ constructor(args: ProtocolRequest) {
24
+ this.messageId = args.messageId ?? randomInt(10000, 32767);
25
+ this.protocol = args.protocol ?? Protocol.rpc_request;
26
+ this.method = args.method;
27
+ this.params = args.params;
28
+ this.secure = args.secure ?? false;
29
+ this.nonce = args.nonce ?? randomInt(10000, 32767);
30
+ this.timestamp = args.timestamp ?? Math.floor(Date.now() / 1000);
31
+ }
32
+
33
+ toMqttRequest() {
34
+ return this;
35
+ }
36
+
37
+ toLocalRequest() {
38
+ if (this.protocol == Protocol.rpc_request) {
39
+ return new RequestMessage({
40
+ messageId: this.messageId,
41
+ protocol: Protocol.general_request,
42
+ method: this.method,
43
+ params: this.params,
44
+ secure: this.secure,
45
+ timestamp: this.timestamp,
46
+ });
47
+ } else {
48
+ return this;
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,19 @@
1
+ import { Dps, DpsPayload } from './dps.js';
2
+
3
+ export class ResponseMessage {
4
+ readonly duid: string;
5
+ readonly dps: Dps;
6
+
7
+ constructor(duid: string, dps: Dps) {
8
+ this.duid = duid;
9
+ this.dps = dps;
10
+ }
11
+
12
+ contain(index: number | string): boolean {
13
+ return this.dps[index.toString()] !== undefined;
14
+ }
15
+
16
+ get(index: number | string): string | DpsPayload {
17
+ return this.dps[index.toString()];
18
+ }
19
+ }
@@ -0,0 +1,18 @@
1
+ export class ChunkBuffer {
2
+ private buffer: Buffer = Buffer.alloc(0);
3
+
4
+ get(): Buffer {
5
+ return this.buffer;
6
+ }
7
+ reset(): void {
8
+ this.buffer = Buffer.alloc(0);
9
+ }
10
+
11
+ append(message: Buffer): void {
12
+ if (this.buffer.length == 0) {
13
+ this.buffer = message;
14
+ } else {
15
+ this.buffer = Buffer.concat([this.buffer, message]);
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,30 @@
1
+ import crypto, { CipherKey, BinaryLike } from 'node:crypto';
2
+
3
+ export function md5bin(str: BinaryLike): Buffer {
4
+ return crypto.createHash('md5').update(str).digest();
5
+ }
6
+
7
+ export function md5hex(str: BinaryLike): string {
8
+ return crypto.createHash('md5').update(str).digest('hex');
9
+ }
10
+
11
+ export function decryptECB(encrypted: string, aesKey: CipherKey) {
12
+ const decipher = crypto.createDecipheriv('aes-128-ecb', aesKey, null);
13
+ decipher.setAutoPadding(false);
14
+ let decrypted = decipher.update(encrypted, 'binary', 'utf8');
15
+ decrypted += decipher.final('utf8');
16
+ return removePadding(decrypted);
17
+ }
18
+
19
+ function removePadding(str: string) {
20
+ const paddingLength = str.charCodeAt(str.length - 1);
21
+ return str.slice(0, -paddingLength);
22
+ }
23
+
24
+ // MessageUtils as plain exports
25
+ export const SALT = 'TXdfu$jyZ#TZHsg4';
26
+
27
+ export function encodeTimestamp(timestamp: number) {
28
+ const hex = timestamp.toString(16).padStart(8, '0').split('');
29
+ return [5, 6, 3, 7, 1, 2, 0, 4].map((idx) => hex[idx]).join('');
30
+ }
@@ -0,0 +1,119 @@
1
+ import crypto from 'node:crypto';
2
+ import CRC32 from 'crc-32';
3
+ import { ResponseMessage } from '../broadcast/model/responseMessage.js';
4
+ import * as CryptoUtils from './cryptoHelper.js';
5
+ import { Protocol } from '../broadcast/model/protocol.js';
6
+ import { MessageContext } from '../broadcast/model/messageContext.js';
7
+ import { AnsiLogger } from 'matterbridge/logger';
8
+ import { Parser } from 'binary-parser/dist/binary_parser.js';
9
+ import { ContentMessage } from '../broadcast/model/contentMessage.js';
10
+ import { HeaderMessage } from '../broadcast/model/headerMessage.js';
11
+ import { DpsPayload } from '../broadcast/model/dps.js';
12
+
13
+ export class MessageDeserializer {
14
+ private readonly context: MessageContext;
15
+ private readonly headerMessageParser: Parser;
16
+ private readonly contentMessageParser: Parser;
17
+ private readonly logger: AnsiLogger;
18
+ private readonly supportedVersions: string[] = ['1.0', 'A01', 'B01'];
19
+
20
+ constructor(context: MessageContext, logger: AnsiLogger) {
21
+ this.context = context;
22
+ this.logger = logger;
23
+
24
+ this.headerMessageParser = new Parser()
25
+ .endianness('big')
26
+ .string('version', {
27
+ length: 3,
28
+ })
29
+ .uint32('seq')
30
+ .uint32('nonce')
31
+ .uint32('timestamp')
32
+ .uint16('protocol');
33
+
34
+ this.contentMessageParser = new Parser()
35
+ .endianness('big')
36
+ .uint16('payloadLen')
37
+ .buffer('payload', {
38
+ length: 'payloadLen',
39
+ })
40
+ .uint32('crc32');
41
+ }
42
+
43
+ public deserialize(duid: string, message: Buffer<ArrayBufferLike>): ResponseMessage {
44
+ // check message header
45
+ const header: HeaderMessage = this.headerMessageParser.parse(message);
46
+ if (!this.supportedVersions.includes(header.version)) {
47
+ throw new Error('unknown protocol version ' + header.version);
48
+ }
49
+
50
+ if (header.protocol === Protocol.hello_response || header.protocol === Protocol.ping_response) {
51
+ const dpsValue: DpsPayload = {
52
+ id: header.seq,
53
+ result: {
54
+ version: header.version,
55
+ nonce: header.nonce,
56
+ },
57
+ };
58
+ return new ResponseMessage(duid, { [header.protocol.toString()]: dpsValue });
59
+ }
60
+
61
+ // parse message content
62
+ const data: ContentMessage = this.contentMessageParser.parse(message.subarray(this.headerMessageParser.sizeOf()));
63
+
64
+ // check crc32
65
+ const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0;
66
+ const expectedCrc32 = message.readUInt32BE(message.length - 4);
67
+ if (crc32 != expectedCrc32) {
68
+ throw new Error(`Wrong CRC32 ${crc32}, expected ${expectedCrc32}`);
69
+ }
70
+
71
+ const localKey = this.context.getLocalKey(duid);
72
+ if (!localKey) {
73
+ this.logger.notice(`Unable to retrieve local key for ${duid}, it should be from other vacuums`);
74
+ return new ResponseMessage(duid, { dps: { id: 0, result: null } });
75
+ }
76
+
77
+ if (header.version == '1.0') {
78
+ const aesKey = CryptoUtils.md5bin(CryptoUtils.encodeTimestamp(header.timestamp) + localKey + CryptoUtils.SALT);
79
+ const decipher = crypto.createDecipheriv('aes-128-ecb', aesKey, null);
80
+ data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
81
+ } else if (header.version == 'A01') {
82
+ const iv = CryptoUtils.md5hex(header.nonce.toString(16).padStart(8, '0') + '726f626f726f636b2d67a6d6da').substring(8, 24);
83
+ const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
84
+ data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
85
+ } else if (header.version == 'B01') {
86
+ const iv = CryptoUtils.md5hex(header.nonce.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25);
87
+ const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv);
88
+ // unpad ??
89
+ data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]);
90
+ }
91
+
92
+ // map visualization not support
93
+ if (header.protocol == Protocol.map_response) {
94
+ return new ResponseMessage(duid, { dps: { id: 0, result: null } });
95
+ }
96
+
97
+ if (header.protocol == Protocol.rpc_response || header.protocol == Protocol.general_request) {
98
+ return this.deserializeRpcResponse(duid, data);
99
+ } else {
100
+ this.logger.error('unknown protocol: ' + header.protocol);
101
+ return new ResponseMessage(duid, { dps: { id: 0, result: null } });
102
+ }
103
+ }
104
+
105
+ private deserializeRpcResponse(duid: string, data: ContentMessage): ResponseMessage {
106
+ const payload = JSON.parse(data.payload.toString());
107
+ const dps = payload.dps;
108
+ this.parseJsonInDps(dps, Protocol.general_request);
109
+ this.parseJsonInDps(dps, Protocol.rpc_response);
110
+ return new ResponseMessage(duid, dps);
111
+ }
112
+
113
+ private parseJsonInDps(dps: Record<string, unknown>, index: Protocol) {
114
+ const indexString = index.toString();
115
+ if (dps[indexString] !== undefined) {
116
+ dps[indexString] = JSON.parse(dps[indexString] as string);
117
+ }
118
+ }
119
+ }