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,424 @@
1
+ import { PlatformMatterbridge, MatterbridgeDynamicPlatform, PlatformConfig } from 'matterbridge';
2
+ import * as axios from 'axios';
3
+ import crypto from 'node:crypto';
4
+ import { AnsiLogger, debugStringify, LogLevel } from 'matterbridge/logger';
5
+ import RoborockService from './roborockService.js';
6
+ import { PLUGIN_NAME } from './settings.js';
7
+ import ClientManager from './clientManager.js';
8
+ import { getRoomMapFromDevice, isSupportedDevice } from './helper.js';
9
+ import { PlatformRunner } from './platformRunner.js';
10
+ import { RoborockVacuumCleaner } from './rvc.js';
11
+ import { configurateBehavior } from './behaviorFactory.js';
12
+ import { NotifyMessageTypes } from './notifyMessageTypes.js';
13
+ import { Device, RoborockAuthenticateApi, RoborockIoTApi, UserData, AuthenticateFlowState } from './roborockCommunication/index.js';
14
+ import { getSupportedAreas, getSupportedScenes } from './initialData/index.js';
15
+ import { AuthenticationPayload, CleanModeSettings, createDefaultExperimentalFeatureSetting, ExperimentalFeatureSetting } from './model/ExperimentalFeatureSetting.js';
16
+ import { ServiceArea } from 'matterbridge/matter/clusters';
17
+ import NodePersist from 'node-persist';
18
+ import Path from 'node:path';
19
+ import { Room } from './roborockCommunication/Zmodel/room.js';
20
+
21
+ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
22
+ robots: Map<string, RoborockVacuumCleaner> = new Map<string, RoborockVacuumCleaner>();
23
+ rvcInterval: NodeJS.Timeout | undefined;
24
+ roborockService: RoborockService | undefined;
25
+ clientManager: ClientManager;
26
+ platformRunner: PlatformRunner | undefined;
27
+ devices: Map<string, Device> = new Map<string, Device>();
28
+ cleanModeSettings: CleanModeSettings | undefined;
29
+ enableExperimentalFeature: ExperimentalFeatureSetting | undefined;
30
+ persist: NodePersist.LocalStorage;
31
+ rrHomeId: number | undefined;
32
+
33
+ constructor(matterbridge: PlatformMatterbridge, log: AnsiLogger, config: PlatformConfig) {
34
+ super(matterbridge, log, config);
35
+
36
+ // Verify that Matterbridge is the correct version
37
+ if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.3.6')) {
38
+ throw new Error(
39
+ `This plugin requires Matterbridge version >= "3.3.6". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend.`,
40
+ );
41
+ }
42
+ this.log.info('Initializing platform:', this.config.name);
43
+ if (config.whiteList === undefined) config.whiteList = [];
44
+ if (config.blackList === undefined) config.blackList = [];
45
+ if (config.enableExperimental === undefined) config.enableExperimental = createDefaultExperimentalFeatureSetting() as ExperimentalFeatureSetting;
46
+
47
+ // Create storage for this plugin (initialised in onStart)
48
+ const persistDir = Path.join(this.matterbridge.matterbridgePluginDirectory, PLUGIN_NAME, 'persist');
49
+ this.persist = NodePersist.create({ dir: persistDir });
50
+
51
+ this.clientManager = new ClientManager(this.log);
52
+ this.devices = new Map<string, Device>();
53
+ }
54
+
55
+ override async onStart(reason?: string) {
56
+ this.log.notice('onStart called with reason:', reason ?? 'none');
57
+
58
+ // Wait for the platform to start
59
+ await this.ready;
60
+ await this.clearSelect();
61
+
62
+ await this.persist.init();
63
+
64
+ // Verify that the config is correct
65
+ if (this.config.username === undefined) {
66
+ this.log.error('"username" (email address) is required in the config');
67
+ return;
68
+ }
69
+
70
+ const axiosInstance = axios.default ?? axios;
71
+
72
+ this.enableExperimentalFeature = this.config.enableExperimental as ExperimentalFeatureSetting;
73
+ // Disable multiple map for more investigation
74
+ this.enableExperimentalFeature.advancedFeature.enableMultipleMap = false;
75
+
76
+ if (this.enableExperimentalFeature?.enableExperimentalFeature && this.enableExperimentalFeature?.cleanModeSettings?.enableCleanModeMapping) {
77
+ this.cleanModeSettings = this.enableExperimentalFeature.cleanModeSettings as CleanModeSettings;
78
+ this.log.notice(`Experimental Feature has been enable`);
79
+ this.log.notice(`cleanModeSettings ${debugStringify(this.cleanModeSettings)}`);
80
+ }
81
+
82
+ this.platformRunner = new PlatformRunner(this);
83
+
84
+ // Load or generate deviceId for consistent authentication
85
+ let deviceId = (await this.persist.getItem('deviceId')) as string | undefined;
86
+ if (!deviceId) {
87
+ deviceId = crypto.randomUUID();
88
+ await this.persist.setItem('deviceId', deviceId);
89
+ this.log.debug('Generated new deviceId:', deviceId);
90
+ } else {
91
+ this.log.debug('Using cached deviceId:', deviceId);
92
+ }
93
+
94
+ const configRegion = (this.config.region as string | undefined) ?? undefined;
95
+ if (configRegion) {
96
+ this.log.notice(`Region configured: ${configRegion}`);
97
+ }
98
+
99
+ this.roborockService = new RoborockService(
100
+ () => new RoborockAuthenticateApi(this.log, axiosInstance, deviceId, configRegion),
101
+ (logger, ud) => new RoborockIoTApi(ud, logger),
102
+ (this.config.refreshInterval as number) ?? 60,
103
+ this.clientManager,
104
+ this.log,
105
+ );
106
+
107
+ const username = this.config.username as string;
108
+
109
+ this.log.debug(`config: ${debugStringify(this.config)}`);
110
+
111
+ const authenticationPayload = this.config.authentication as AuthenticationPayload;
112
+ const password = authenticationPayload.password ?? '';
113
+ const verificationCode = authenticationPayload.verificationCode ?? '';
114
+ const authenticationMethod = authenticationPayload.authenticationMethod as 'VerificationCode' | 'Password';
115
+
116
+ this.log.debug(`Authentication method: ${authenticationMethod}`);
117
+ this.log.debug(`Username: ${username}`);
118
+ this.log.debug(`Password provided: ${password !== ''}`);
119
+ this.log.debug(`Verification code provided: ${verificationCode !== ''}`);
120
+
121
+ // Authenticate using 2FA flow
122
+ let userData: UserData | undefined;
123
+ try {
124
+ if (authenticationMethod === 'VerificationCode') {
125
+ this.log.debug('Using verification code from config for authentication');
126
+ userData = await this.authenticate2FA(username, verificationCode);
127
+ } else {
128
+ userData = await this.authenticateWithPassword(username, password);
129
+ }
130
+ } catch (error) {
131
+ this.log.error(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`);
132
+ return;
133
+ }
134
+
135
+ if (!userData) {
136
+ // Code was requested, waiting for user to enter it in config
137
+ return;
138
+ }
139
+
140
+ this.log.debug('Initializing - userData:', debugStringify(userData));
141
+ const devices = await this.roborockService.listDevices(username);
142
+ this.log.notice('Initializing - devices: ', debugStringify(devices));
143
+
144
+ let vacuums: Device[] = [];
145
+ if ((this.config.whiteList as string[]).length > 0) {
146
+ const whiteList = (this.config.whiteList ?? []) as string[];
147
+ for (const item of whiteList) {
148
+ const duid = item.split('-')[1].trim();
149
+ const vacuum = devices.find((d) => d.duid === duid);
150
+ if (vacuum) {
151
+ vacuums.push(vacuum);
152
+ }
153
+ }
154
+ } else {
155
+ vacuums = devices.filter((d) => isSupportedDevice(d.data.model));
156
+ }
157
+
158
+ if (vacuums.length === 0) {
159
+ this.log.error('Initializing: No device found');
160
+ return;
161
+ }
162
+
163
+ if (!this.enableExperimentalFeature?.enableExperimentalFeature || !this.enableExperimentalFeature?.advancedFeature?.enableServerMode) {
164
+ vacuums = [vacuums[0]]; // If server mode is not enabled, only use the first vacuum
165
+ }
166
+ // else {
167
+ // const cloned = JSON.parse(JSON.stringify(vacuums[0])) as Device;
168
+ // cloned.name = `${cloned.name} Clone`;
169
+ // cloned.serialNumber = `${cloned.serialNumber}-clone`;
170
+
171
+ // vacuums = [...vacuums, cloned]; // If server mode is enabled, add the first vacuum again to ensure it is always included
172
+ // }
173
+
174
+ // this.log.error('Initializing - vacuums: ', debugStringify(vacuums));
175
+
176
+ for (const vacuum of vacuums) {
177
+ await this.roborockService.initializeMessageClient(username, vacuum, userData);
178
+ this.devices.set(vacuum.serialNumber, vacuum);
179
+ }
180
+
181
+ await this.onConfigurateDevice();
182
+ this.log.notice('onStart finished');
183
+ }
184
+
185
+ override async onConfigure() {
186
+ await super.onConfigure();
187
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
188
+ const self = this;
189
+ this.rvcInterval = setInterval(
190
+ async () => {
191
+ self.platformRunner?.requestHomeData();
192
+ },
193
+ ((this.config.refreshInterval as number) ?? 60) * 1000 + 100,
194
+ );
195
+ }
196
+
197
+ async onConfigurateDevice(): Promise<void> {
198
+ this.log.info('onConfigurateDevice start');
199
+ if (this.platformRunner === undefined || this.roborockService === undefined) {
200
+ this.log.error('Initializing: PlatformRunner or RoborockService is undefined');
201
+ return;
202
+ }
203
+
204
+ const username = this.config.username as string;
205
+ if (this.devices.size === 0 || !username) {
206
+ this.log.error('Initializing: No supported devices found');
207
+ return;
208
+ }
209
+
210
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
211
+ const self = this;
212
+
213
+ const configurateSuccess = new Map<string, boolean>();
214
+
215
+ for (const vacuum of this.devices.values()) {
216
+ const success = await this.configurateDevice(vacuum);
217
+ configurateSuccess.set(vacuum.duid, success);
218
+ if (success) {
219
+ this.rrHomeId = vacuum.rrHomeId;
220
+ }
221
+ }
222
+
223
+ this.roborockService.setDeviceNotify(async function (messageSource: NotifyMessageTypes, homeData: unknown) {
224
+ await self.platformRunner?.updateRobot(messageSource, homeData);
225
+ });
226
+
227
+ for (const [duid, robot] of this.robots.entries()) {
228
+ if (!configurateSuccess.get(duid)) {
229
+ continue;
230
+ }
231
+ this.roborockService.activateDeviceNotify(robot.device);
232
+ }
233
+
234
+ await this.platformRunner?.requestHomeData();
235
+
236
+ this.log.info('onConfigurateDevice finished');
237
+ }
238
+
239
+ // Running in loop to configurate devices
240
+ private async configurateDevice(vacuum: Device): Promise<boolean> {
241
+ const username = this.config.username as string;
242
+
243
+ if (this.platformRunner === undefined || this.roborockService === undefined) {
244
+ this.log.error('Initializing: PlatformRunner or RoborockService is undefined');
245
+ return false;
246
+ }
247
+
248
+ const connectedToLocalNetwork = await this.roborockService.initializeMessageClientForLocal(vacuum);
249
+
250
+ if (!connectedToLocalNetwork) {
251
+ this.log.error(`Failed to connect to local network for device: ${vacuum.name} (${vacuum.duid})`);
252
+ return false;
253
+ }
254
+
255
+ if (vacuum.rooms === undefined || vacuum.rooms.length === 0) {
256
+ this.log.notice(`Fetching map information for device: ${vacuum.name} (${vacuum.duid}) to get rooms`);
257
+ const map_info = await this.roborockService.getMapInformation(vacuum.duid);
258
+ const rooms = map_info?.allRooms ?? [];
259
+ vacuum.rooms = rooms.map((room) => ({ id: room.globalId, name: room.displayName }) as Room);
260
+ }
261
+
262
+ const roomMap = await getRoomMapFromDevice(vacuum, this);
263
+
264
+ this.log.debug('Initializing - roomMap: ', debugStringify(roomMap));
265
+
266
+ const behaviorHandler = configurateBehavior(
267
+ vacuum.data.model,
268
+ vacuum.duid,
269
+ this.roborockService,
270
+ this.cleanModeSettings,
271
+ this.enableExperimentalFeature?.advancedFeature?.forceRunAtDefault ?? false,
272
+ this.log,
273
+ );
274
+
275
+ const enableMultipleMap = this.enableExperimentalFeature?.enableExperimentalFeature && this.enableExperimentalFeature.advancedFeature?.enableMultipleMap;
276
+
277
+ const { supportedAreas, roomIndexMap } = getSupportedAreas(vacuum.rooms, roomMap, enableMultipleMap, this.log);
278
+ this.roborockService.setSupportedAreas(vacuum.duid, supportedAreas);
279
+ this.roborockService.setSupportedAreaIndexMap(vacuum.duid, roomIndexMap);
280
+
281
+ let routineAsRoom: ServiceArea.Area[] = [];
282
+ if (this.enableExperimentalFeature?.enableExperimentalFeature && this.enableExperimentalFeature.advancedFeature?.showRoutinesAsRoom) {
283
+ routineAsRoom = getSupportedScenes(vacuum.scenes ?? [], this.log);
284
+ this.roborockService.setSupportedScenes(vacuum.duid, routineAsRoom);
285
+ }
286
+
287
+ const robot = new RoborockVacuumCleaner(username, vacuum, roomMap, routineAsRoom, this.enableExperimentalFeature, this.log);
288
+ robot.configurateHandler(behaviorHandler);
289
+
290
+ this.log.info('vacuum:', debugStringify(vacuum));
291
+
292
+ this.setSelectDevice(robot.serialNumber ?? '', robot.deviceName ?? '', undefined, 'hub');
293
+ if (this.validateDevice(robot.deviceName ?? '')) {
294
+ await this.registerDevice(robot);
295
+ }
296
+
297
+ this.robots.set(robot.serialNumber ?? '', robot);
298
+
299
+ return true;
300
+ }
301
+
302
+ override async onShutdown(reason?: string) {
303
+ await super.onShutdown(reason);
304
+ this.log.notice('onShutdown called with reason:', reason ?? 'none');
305
+ if (this.rvcInterval) clearInterval(this.rvcInterval);
306
+ if (this.roborockService) this.roborockService.stopService();
307
+ if (this.config.unregisterOnShutdown === true) await this.unregisterAllDevices(500);
308
+ }
309
+
310
+ override async onChangeLoggerLevel(logLevel: LogLevel): Promise<void> {
311
+ this.log.notice(`Change ${PLUGIN_NAME} log level: ${logLevel} (was ${this.log.logLevel})`);
312
+ this.log.logLevel = logLevel;
313
+ return Promise.resolve();
314
+ }
315
+
316
+ private async authenticateWithPassword(username: string, password: string): Promise<UserData> {
317
+ if (!this.roborockService) {
318
+ throw new Error('RoborockService is not initialized');
319
+ }
320
+
321
+ this.log.notice('Attempting login with password...');
322
+
323
+ const userData = await this.roborockService.loginWithPassword(
324
+ username,
325
+ password,
326
+ async () => {
327
+ if (this.enableExperimentalFeature?.enableExperimentalFeature && this.enableExperimentalFeature.advancedFeature?.alwaysExecuteAuthentication) {
328
+ this.log.debug('Always execute authentication on startup');
329
+ return undefined;
330
+ }
331
+
332
+ const savedUserData = (await this.persist.getItem('userData')) as UserData | undefined;
333
+ if (savedUserData) {
334
+ this.log.debug('Loading saved userData:', debugStringify(savedUserData));
335
+ return savedUserData;
336
+ }
337
+ return undefined;
338
+ },
339
+ async (userData: UserData) => {
340
+ await this.persist.setItem('userData', userData);
341
+ },
342
+ );
343
+ this.log.notice('Authentication successful!');
344
+ return userData;
345
+ }
346
+
347
+ /**
348
+ * Authenticate using 2FA verification code flow
349
+ * @param username - The user's email address
350
+ * @param verificationCode - The verification code from config (if provided)
351
+ * @returns UserData on successful authentication, undefined if waiting for code
352
+ */
353
+ private async authenticate2FA(username: string, verificationCode: string | undefined): Promise<UserData | undefined> {
354
+ if (!this.roborockService) {
355
+ throw new Error('RoborockService is not initialized');
356
+ }
357
+
358
+ if (!this.enableExperimentalFeature?.advancedFeature?.alwaysExecuteAuthentication) {
359
+ const savedUserData = (await this.persist.getItem('userData')) as UserData | undefined;
360
+ if (savedUserData) {
361
+ this.log.debug('Found saved userData, attempting to use cached token');
362
+ try {
363
+ const userData = await this.roborockService.loginWithCachedToken(username, savedUserData);
364
+ this.log.notice('Successfully authenticated with cached token');
365
+ return userData;
366
+ } catch (error) {
367
+ this.log.warn(`Cached token invalid or expired: ${error instanceof Error ? error.message : String(error)}`);
368
+ await this.persist.removeItem('userData');
369
+ // Continue to request new code
370
+ }
371
+ }
372
+ }
373
+
374
+ if (!verificationCode || verificationCode.trim() === '') {
375
+ const authState = (await this.persist.getItem('authenticateFlowState')) as AuthenticateFlowState | undefined;
376
+ const now = Date.now();
377
+ const RATE_LIMIT_MS = 60000; // 1 minute between code requests
378
+
379
+ if (authState?.codeRequestedAt && now - authState.codeRequestedAt < RATE_LIMIT_MS) {
380
+ const waitSeconds = Math.ceil((RATE_LIMIT_MS - (now - authState.codeRequestedAt)) / 1000);
381
+ this.log.warn(`Please wait ${waitSeconds} seconds before requesting another code.`);
382
+ this.log.notice('============================================');
383
+ this.log.notice('ACTION REQUIRED: Enter verification code');
384
+ this.log.notice(`A verification code was previously sent to: ${username}`);
385
+ this.log.notice('Enter the 6-digit code in the plugin configuration');
386
+ this.log.notice('under the "verificationCode" field, then restart the plugin.');
387
+ this.log.notice('============================================');
388
+ return undefined;
389
+ }
390
+
391
+ try {
392
+ this.log.notice(`Requesting verification code for: ${username}`);
393
+ await this.roborockService.requestVerificationCode(username);
394
+
395
+ await this.persist.setItem('authenticateFlowState', {
396
+ email: username,
397
+ codeRequestedAt: now,
398
+ } as AuthenticateFlowState);
399
+
400
+ this.log.notice('============================================');
401
+ this.log.notice('ACTION REQUIRED: Enter verification code');
402
+ this.log.notice(`A verification code has been sent to: ${username}`);
403
+ this.log.notice('Enter the 6-digit code in the plugin configuration');
404
+ this.log.notice('under the "verificationCode" field, then restart the plugin.');
405
+ this.log.notice('============================================');
406
+ } catch (error) {
407
+ this.log.error(`Failed to request verification code: ${error instanceof Error ? error.message : String(error)}`);
408
+ throw error;
409
+ }
410
+
411
+ return undefined;
412
+ }
413
+
414
+ this.log.notice('Attempting login with verification code...');
415
+
416
+ const userData = await this.roborockService.loginWithVerificationCode(username, verificationCode.trim(), async (data: UserData) => {
417
+ await this.persist.setItem('userData', data);
418
+ await this.persist.removeItem('authenticateFlowState');
419
+ });
420
+
421
+ this.log.notice('Authentication successful!');
422
+ return userData;
423
+ }
424
+ }
@@ -0,0 +1,103 @@
1
+ import { PowerSource, RvcOperationalState } from 'matterbridge/matter/clusters';
2
+ import { CloudMessageModel } from './model/CloudMessageModel.js';
3
+ import { RoborockMatterbridgePlatform } from './platform.js';
4
+ import { getBatteryStatus, getOperationalErrorState } from './initialData/index.js';
5
+ import { NotifyMessageTypes } from './notifyMessageTypes.js';
6
+ import { CloudMessageResult } from './roborockCommunication/Zmodel/messageResult.js';
7
+ import { BatteryMessage, DeviceErrorMessage, DeviceStatusNotify, Home } from './roborockCommunication/index.js';
8
+ import { debugStringify } from 'matterbridge/logger';
9
+ import { handleLocalMessage } from './runtimes/handleLocalMessage.js';
10
+ import { handleCloudMessage } from './runtimes/handleCloudMessage.js';
11
+ import { updateFromHomeData } from './runtimes/handleHomeDataMessage.js';
12
+
13
+ export class PlatformRunner {
14
+ platform: RoborockMatterbridgePlatform;
15
+ constructor(platform: RoborockMatterbridgePlatform) {
16
+ this.platform = platform;
17
+ }
18
+
19
+ public async updateRobot(messageSource: NotifyMessageTypes, homeData: unknown): Promise<void> {
20
+ if (messageSource === NotifyMessageTypes.HomeData) {
21
+ updateFromHomeData(homeData as Home, this.platform);
22
+ } else {
23
+ await this.updateFromMQTTMessage(messageSource, homeData);
24
+ }
25
+ }
26
+
27
+ public async requestHomeData(): Promise<void> {
28
+ const platform = this.platform;
29
+ if (platform.robots.size === 0 || !platform.rrHomeId) return;
30
+ if (platform.roborockService === undefined) return;
31
+
32
+ const homeData = await platform.roborockService.getHomeDataForUpdating(platform.rrHomeId);
33
+ await this.updateRobot(NotifyMessageTypes.HomeData, homeData);
34
+ }
35
+
36
+ public async updateFromMQTTMessage(messageSource: NotifyMessageTypes, messageData: unknown, duid = '', tracked = false): Promise<void> {
37
+ const platform = this.platform;
38
+ duid = duid || (messageData as DeviceStatusNotify)?.duid || '';
39
+
40
+ const robot = platform.robots.get(duid);
41
+ if (robot === undefined) {
42
+ platform.log.error(`Error1: Robot with DUID ${duid} not found`);
43
+ return;
44
+ }
45
+
46
+ const deviceData = robot.device.data;
47
+ if (deviceData === undefined) {
48
+ platform.log.error('Device data is undefined');
49
+ return;
50
+ }
51
+
52
+ if (!tracked) {
53
+ platform.log.debug(`Receive: ${messageSource} updateFromMQTTMessage: ${debugStringify(messageData as DeviceStatusNotify)}`);
54
+ }
55
+
56
+ if (!robot.serialNumber) {
57
+ platform.log.error('Robot serial number is undefined');
58
+ return;
59
+ }
60
+
61
+ switch (messageSource) {
62
+ case NotifyMessageTypes.ErrorOccurred: {
63
+ const message = messageData as DeviceErrorMessage;
64
+ const operationalStateId = getOperationalErrorState(message.errorCode);
65
+ if (operationalStateId) {
66
+ platform.log.error(`Error occurred: ${message.errorCode}`);
67
+ robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', operationalStateId, platform.log);
68
+ }
69
+ break;
70
+ }
71
+ case NotifyMessageTypes.BatteryUpdate: {
72
+ const message = messageData as BatteryMessage;
73
+ const batteryLevel = message.percentage;
74
+ if (batteryLevel) {
75
+ robot.updateAttribute(PowerSource.Cluster.id, 'batPercentRemaining', batteryLevel * 2, platform.log);
76
+ robot.updateAttribute(PowerSource.Cluster.id, 'batChargeLevel', getBatteryStatus(batteryLevel), platform.log);
77
+ }
78
+ break;
79
+ }
80
+
81
+ case NotifyMessageTypes.LocalMessage: {
82
+ const data = messageData as CloudMessageResult;
83
+ const robot = platform.robots.get(duid);
84
+ if (robot && data) {
85
+ await handleLocalMessage(data, platform, duid);
86
+ return;
87
+ }
88
+ platform.log.error(`Error2: Robot with DUID ${duid} not found`);
89
+ break;
90
+ }
91
+
92
+ case NotifyMessageTypes.CloudMessage: {
93
+ const data = messageData as CloudMessageModel;
94
+ if (!data) return;
95
+ await handleCloudMessage(data, platform, this, duid);
96
+ break;
97
+ }
98
+
99
+ default:
100
+ break;
101
+ }
102
+ }
103
+ }