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,312 @@
1
+ import { MatterbridgeDynamicPlatform } from 'matterbridge';
2
+ import * as axios from 'axios';
3
+ import crypto from 'node:crypto';
4
+ import { debugStringify } 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 { RoborockAuthenticateApi, RoborockIoTApi } from './roborockCommunication/index.js';
13
+ import { getSupportedAreas, getSupportedScenes } from './initialData/index.js';
14
+ import { createDefaultExperimentalFeatureSetting } from './model/ExperimentalFeatureSetting.js';
15
+ import NodePersist from 'node-persist';
16
+ import Path from 'node:path';
17
+ export class RoborockMatterbridgePlatform extends MatterbridgeDynamicPlatform {
18
+ robots = new Map();
19
+ rvcInterval;
20
+ roborockService;
21
+ clientManager;
22
+ platformRunner;
23
+ devices = new Map();
24
+ cleanModeSettings;
25
+ enableExperimentalFeature;
26
+ persist;
27
+ rrHomeId;
28
+ constructor(matterbridge, log, config) {
29
+ super(matterbridge, log, config);
30
+ if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.3.6')) {
31
+ throw new Error(`This plugin requires Matterbridge version >= "3.3.6". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend.`);
32
+ }
33
+ this.log.info('Initializing platform:', this.config.name);
34
+ if (config.whiteList === undefined)
35
+ config.whiteList = [];
36
+ if (config.blackList === undefined)
37
+ config.blackList = [];
38
+ if (config.enableExperimental === undefined)
39
+ config.enableExperimental = createDefaultExperimentalFeatureSetting();
40
+ const persistDir = Path.join(this.matterbridge.matterbridgePluginDirectory, PLUGIN_NAME, 'persist');
41
+ this.persist = NodePersist.create({ dir: persistDir });
42
+ this.clientManager = new ClientManager(this.log);
43
+ this.devices = new Map();
44
+ }
45
+ async onStart(reason) {
46
+ this.log.notice('onStart called with reason:', reason ?? 'none');
47
+ await this.ready;
48
+ await this.clearSelect();
49
+ await this.persist.init();
50
+ if (this.config.username === undefined) {
51
+ this.log.error('"username" (email address) is required in the config');
52
+ return;
53
+ }
54
+ const axiosInstance = axios.default ?? axios;
55
+ this.enableExperimentalFeature = this.config.enableExperimental;
56
+ this.enableExperimentalFeature.advancedFeature.enableMultipleMap = false;
57
+ if (this.enableExperimentalFeature?.enableExperimentalFeature && this.enableExperimentalFeature?.cleanModeSettings?.enableCleanModeMapping) {
58
+ this.cleanModeSettings = this.enableExperimentalFeature.cleanModeSettings;
59
+ this.log.notice(`Experimental Feature has been enable`);
60
+ this.log.notice(`cleanModeSettings ${debugStringify(this.cleanModeSettings)}`);
61
+ }
62
+ this.platformRunner = new PlatformRunner(this);
63
+ let deviceId = (await this.persist.getItem('deviceId'));
64
+ if (!deviceId) {
65
+ deviceId = crypto.randomUUID();
66
+ await this.persist.setItem('deviceId', deviceId);
67
+ this.log.debug('Generated new deviceId:', deviceId);
68
+ }
69
+ else {
70
+ this.log.debug('Using cached deviceId:', deviceId);
71
+ }
72
+ const configRegion = this.config.region ?? undefined;
73
+ if (configRegion) {
74
+ this.log.notice(`Region configured: ${configRegion}`);
75
+ }
76
+ this.roborockService = new RoborockService(() => new RoborockAuthenticateApi(this.log, axiosInstance, deviceId, configRegion), (logger, ud) => new RoborockIoTApi(ud, logger), this.config.refreshInterval ?? 60, this.clientManager, this.log);
77
+ const username = this.config.username;
78
+ this.log.debug(`config: ${debugStringify(this.config)}`);
79
+ const authenticationPayload = this.config.authentication;
80
+ const password = authenticationPayload.password ?? '';
81
+ const verificationCode = authenticationPayload.verificationCode ?? '';
82
+ const authenticationMethod = authenticationPayload.authenticationMethod;
83
+ this.log.debug(`Authentication method: ${authenticationMethod}`);
84
+ this.log.debug(`Username: ${username}`);
85
+ this.log.debug(`Password provided: ${password !== ''}`);
86
+ this.log.debug(`Verification code provided: ${verificationCode !== ''}`);
87
+ let userData;
88
+ try {
89
+ if (authenticationMethod === 'VerificationCode') {
90
+ this.log.debug('Using verification code from config for authentication');
91
+ userData = await this.authenticate2FA(username, verificationCode);
92
+ }
93
+ else {
94
+ userData = await this.authenticateWithPassword(username, password);
95
+ }
96
+ }
97
+ catch (error) {
98
+ this.log.error(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`);
99
+ return;
100
+ }
101
+ if (!userData) {
102
+ return;
103
+ }
104
+ this.log.debug('Initializing - userData:', debugStringify(userData));
105
+ const devices = await this.roborockService.listDevices(username);
106
+ this.log.notice('Initializing - devices: ', debugStringify(devices));
107
+ let vacuums = [];
108
+ if (this.config.whiteList.length > 0) {
109
+ const whiteList = (this.config.whiteList ?? []);
110
+ for (const item of whiteList) {
111
+ const duid = item.split('-')[1].trim();
112
+ const vacuum = devices.find((d) => d.duid === duid);
113
+ if (vacuum) {
114
+ vacuums.push(vacuum);
115
+ }
116
+ }
117
+ }
118
+ else {
119
+ vacuums = devices.filter((d) => isSupportedDevice(d.data.model));
120
+ }
121
+ if (vacuums.length === 0) {
122
+ this.log.error('Initializing: No device found');
123
+ return;
124
+ }
125
+ if (!this.enableExperimentalFeature?.enableExperimentalFeature || !this.enableExperimentalFeature?.advancedFeature?.enableServerMode) {
126
+ vacuums = [vacuums[0]];
127
+ }
128
+ for (const vacuum of vacuums) {
129
+ await this.roborockService.initializeMessageClient(username, vacuum, userData);
130
+ this.devices.set(vacuum.serialNumber, vacuum);
131
+ }
132
+ await this.onConfigurateDevice();
133
+ this.log.notice('onStart finished');
134
+ }
135
+ async onConfigure() {
136
+ await super.onConfigure();
137
+ const self = this;
138
+ this.rvcInterval = setInterval(async () => {
139
+ self.platformRunner?.requestHomeData();
140
+ }, (this.config.refreshInterval ?? 60) * 1000 + 100);
141
+ }
142
+ async onConfigurateDevice() {
143
+ this.log.info('onConfigurateDevice start');
144
+ if (this.platformRunner === undefined || this.roborockService === undefined) {
145
+ this.log.error('Initializing: PlatformRunner or RoborockService is undefined');
146
+ return;
147
+ }
148
+ const username = this.config.username;
149
+ if (this.devices.size === 0 || !username) {
150
+ this.log.error('Initializing: No supported devices found');
151
+ return;
152
+ }
153
+ const self = this;
154
+ const configurateSuccess = new Map();
155
+ for (const vacuum of this.devices.values()) {
156
+ const success = await this.configurateDevice(vacuum);
157
+ configurateSuccess.set(vacuum.duid, success);
158
+ if (success) {
159
+ this.rrHomeId = vacuum.rrHomeId;
160
+ }
161
+ }
162
+ this.roborockService.setDeviceNotify(async function (messageSource, homeData) {
163
+ await self.platformRunner?.updateRobot(messageSource, homeData);
164
+ });
165
+ for (const [duid, robot] of this.robots.entries()) {
166
+ if (!configurateSuccess.get(duid)) {
167
+ continue;
168
+ }
169
+ this.roborockService.activateDeviceNotify(robot.device);
170
+ }
171
+ await this.platformRunner?.requestHomeData();
172
+ this.log.info('onConfigurateDevice finished');
173
+ }
174
+ async configurateDevice(vacuum) {
175
+ const username = this.config.username;
176
+ if (this.platformRunner === undefined || this.roborockService === undefined) {
177
+ this.log.error('Initializing: PlatformRunner or RoborockService is undefined');
178
+ return false;
179
+ }
180
+ const connectedToLocalNetwork = await this.roborockService.initializeMessageClientForLocal(vacuum);
181
+ if (!connectedToLocalNetwork) {
182
+ this.log.error(`Failed to connect to local network for device: ${vacuum.name} (${vacuum.duid})`);
183
+ return false;
184
+ }
185
+ if (vacuum.rooms === undefined || vacuum.rooms.length === 0) {
186
+ this.log.notice(`Fetching map information for device: ${vacuum.name} (${vacuum.duid}) to get rooms`);
187
+ const map_info = await this.roborockService.getMapInformation(vacuum.duid);
188
+ const rooms = map_info?.allRooms ?? [];
189
+ vacuum.rooms = rooms.map((room) => ({ id: room.globalId, name: room.displayName }));
190
+ }
191
+ const roomMap = await getRoomMapFromDevice(vacuum, this);
192
+ this.log.debug('Initializing - roomMap: ', debugStringify(roomMap));
193
+ const behaviorHandler = configurateBehavior(vacuum.data.model, vacuum.duid, this.roborockService, this.cleanModeSettings, this.enableExperimentalFeature?.advancedFeature?.forceRunAtDefault ?? false, this.log);
194
+ const enableMultipleMap = this.enableExperimentalFeature?.enableExperimentalFeature && this.enableExperimentalFeature.advancedFeature?.enableMultipleMap;
195
+ const { supportedAreas, roomIndexMap } = getSupportedAreas(vacuum.rooms, roomMap, enableMultipleMap, this.log);
196
+ this.roborockService.setSupportedAreas(vacuum.duid, supportedAreas);
197
+ this.roborockService.setSupportedAreaIndexMap(vacuum.duid, roomIndexMap);
198
+ let routineAsRoom = [];
199
+ if (this.enableExperimentalFeature?.enableExperimentalFeature && this.enableExperimentalFeature.advancedFeature?.showRoutinesAsRoom) {
200
+ routineAsRoom = getSupportedScenes(vacuum.scenes ?? [], this.log);
201
+ this.roborockService.setSupportedScenes(vacuum.duid, routineAsRoom);
202
+ }
203
+ const robot = new RoborockVacuumCleaner(username, vacuum, roomMap, routineAsRoom, this.enableExperimentalFeature, this.log);
204
+ robot.configurateHandler(behaviorHandler);
205
+ this.log.info('vacuum:', debugStringify(vacuum));
206
+ this.setSelectDevice(robot.serialNumber ?? '', robot.deviceName ?? '', undefined, 'hub');
207
+ if (this.validateDevice(robot.deviceName ?? '')) {
208
+ await this.registerDevice(robot);
209
+ }
210
+ this.robots.set(robot.serialNumber ?? '', robot);
211
+ return true;
212
+ }
213
+ async onShutdown(reason) {
214
+ await super.onShutdown(reason);
215
+ this.log.notice('onShutdown called with reason:', reason ?? 'none');
216
+ if (this.rvcInterval)
217
+ clearInterval(this.rvcInterval);
218
+ if (this.roborockService)
219
+ this.roborockService.stopService();
220
+ if (this.config.unregisterOnShutdown === true)
221
+ await this.unregisterAllDevices(500);
222
+ }
223
+ async onChangeLoggerLevel(logLevel) {
224
+ this.log.notice(`Change ${PLUGIN_NAME} log level: ${logLevel} (was ${this.log.logLevel})`);
225
+ this.log.logLevel = logLevel;
226
+ return Promise.resolve();
227
+ }
228
+ async authenticateWithPassword(username, password) {
229
+ if (!this.roborockService) {
230
+ throw new Error('RoborockService is not initialized');
231
+ }
232
+ this.log.notice('Attempting login with password...');
233
+ const userData = await this.roborockService.loginWithPassword(username, password, async () => {
234
+ if (this.enableExperimentalFeature?.enableExperimentalFeature && this.enableExperimentalFeature.advancedFeature?.alwaysExecuteAuthentication) {
235
+ this.log.debug('Always execute authentication on startup');
236
+ return undefined;
237
+ }
238
+ const savedUserData = (await this.persist.getItem('userData'));
239
+ if (savedUserData) {
240
+ this.log.debug('Loading saved userData:', debugStringify(savedUserData));
241
+ return savedUserData;
242
+ }
243
+ return undefined;
244
+ }, async (userData) => {
245
+ await this.persist.setItem('userData', userData);
246
+ });
247
+ this.log.notice('Authentication successful!');
248
+ return userData;
249
+ }
250
+ async authenticate2FA(username, verificationCode) {
251
+ if (!this.roborockService) {
252
+ throw new Error('RoborockService is not initialized');
253
+ }
254
+ if (!this.enableExperimentalFeature?.advancedFeature?.alwaysExecuteAuthentication) {
255
+ const savedUserData = (await this.persist.getItem('userData'));
256
+ if (savedUserData) {
257
+ this.log.debug('Found saved userData, attempting to use cached token');
258
+ try {
259
+ const userData = await this.roborockService.loginWithCachedToken(username, savedUserData);
260
+ this.log.notice('Successfully authenticated with cached token');
261
+ return userData;
262
+ }
263
+ catch (error) {
264
+ this.log.warn(`Cached token invalid or expired: ${error instanceof Error ? error.message : String(error)}`);
265
+ await this.persist.removeItem('userData');
266
+ }
267
+ }
268
+ }
269
+ if (!verificationCode || verificationCode.trim() === '') {
270
+ const authState = (await this.persist.getItem('authenticateFlowState'));
271
+ const now = Date.now();
272
+ const RATE_LIMIT_MS = 60000;
273
+ if (authState?.codeRequestedAt && now - authState.codeRequestedAt < RATE_LIMIT_MS) {
274
+ const waitSeconds = Math.ceil((RATE_LIMIT_MS - (now - authState.codeRequestedAt)) / 1000);
275
+ this.log.warn(`Please wait ${waitSeconds} seconds before requesting another code.`);
276
+ this.log.notice('============================================');
277
+ this.log.notice('ACTION REQUIRED: Enter verification code');
278
+ this.log.notice(`A verification code was previously sent to: ${username}`);
279
+ this.log.notice('Enter the 6-digit code in the plugin configuration');
280
+ this.log.notice('under the "verificationCode" field, then restart the plugin.');
281
+ this.log.notice('============================================');
282
+ return undefined;
283
+ }
284
+ try {
285
+ this.log.notice(`Requesting verification code for: ${username}`);
286
+ await this.roborockService.requestVerificationCode(username);
287
+ await this.persist.setItem('authenticateFlowState', {
288
+ email: username,
289
+ codeRequestedAt: now,
290
+ });
291
+ this.log.notice('============================================');
292
+ this.log.notice('ACTION REQUIRED: Enter verification code');
293
+ this.log.notice(`A verification code has been sent to: ${username}`);
294
+ this.log.notice('Enter the 6-digit code in the plugin configuration');
295
+ this.log.notice('under the "verificationCode" field, then restart the plugin.');
296
+ this.log.notice('============================================');
297
+ }
298
+ catch (error) {
299
+ this.log.error(`Failed to request verification code: ${error instanceof Error ? error.message : String(error)}`);
300
+ throw error;
301
+ }
302
+ return undefined;
303
+ }
304
+ this.log.notice('Attempting login with verification code...');
305
+ const userData = await this.roborockService.loginWithVerificationCode(username, verificationCode.trim(), async (data) => {
306
+ await this.persist.setItem('userData', data);
307
+ await this.persist.removeItem('authenticateFlowState');
308
+ });
309
+ this.log.notice('Authentication successful!');
310
+ return userData;
311
+ }
312
+ }
@@ -0,0 +1,90 @@
1
+ import { PowerSource, RvcOperationalState } from 'matterbridge/matter/clusters';
2
+ import { getBatteryStatus, getOperationalErrorState } from './initialData/index.js';
3
+ import { NotifyMessageTypes } from './notifyMessageTypes.js';
4
+ import { debugStringify } from 'matterbridge/logger';
5
+ import { handleLocalMessage } from './runtimes/handleLocalMessage.js';
6
+ import { handleCloudMessage } from './runtimes/handleCloudMessage.js';
7
+ import { updateFromHomeData } from './runtimes/handleHomeDataMessage.js';
8
+ export class PlatformRunner {
9
+ platform;
10
+ constructor(platform) {
11
+ this.platform = platform;
12
+ }
13
+ async updateRobot(messageSource, homeData) {
14
+ if (messageSource === NotifyMessageTypes.HomeData) {
15
+ updateFromHomeData(homeData, this.platform);
16
+ }
17
+ else {
18
+ await this.updateFromMQTTMessage(messageSource, homeData);
19
+ }
20
+ }
21
+ async requestHomeData() {
22
+ const platform = this.platform;
23
+ if (platform.robots.size === 0 || !platform.rrHomeId)
24
+ return;
25
+ if (platform.roborockService === undefined)
26
+ return;
27
+ const homeData = await platform.roborockService.getHomeDataForUpdating(platform.rrHomeId);
28
+ await this.updateRobot(NotifyMessageTypes.HomeData, homeData);
29
+ }
30
+ async updateFromMQTTMessage(messageSource, messageData, duid = '', tracked = false) {
31
+ const platform = this.platform;
32
+ duid = duid || messageData?.duid || '';
33
+ const robot = platform.robots.get(duid);
34
+ if (robot === undefined) {
35
+ platform.log.error(`Error1: Robot with DUID ${duid} not found`);
36
+ return;
37
+ }
38
+ const deviceData = robot.device.data;
39
+ if (deviceData === undefined) {
40
+ platform.log.error('Device data is undefined');
41
+ return;
42
+ }
43
+ if (!tracked) {
44
+ platform.log.debug(`Receive: ${messageSource} updateFromMQTTMessage: ${debugStringify(messageData)}`);
45
+ }
46
+ if (!robot.serialNumber) {
47
+ platform.log.error('Robot serial number is undefined');
48
+ return;
49
+ }
50
+ switch (messageSource) {
51
+ case NotifyMessageTypes.ErrorOccurred: {
52
+ const message = messageData;
53
+ const operationalStateId = getOperationalErrorState(message.errorCode);
54
+ if (operationalStateId) {
55
+ platform.log.error(`Error occurred: ${message.errorCode}`);
56
+ robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', operationalStateId, platform.log);
57
+ }
58
+ break;
59
+ }
60
+ case NotifyMessageTypes.BatteryUpdate: {
61
+ const message = messageData;
62
+ const batteryLevel = message.percentage;
63
+ if (batteryLevel) {
64
+ robot.updateAttribute(PowerSource.Cluster.id, 'batPercentRemaining', batteryLevel * 2, platform.log);
65
+ robot.updateAttribute(PowerSource.Cluster.id, 'batChargeLevel', getBatteryStatus(batteryLevel), platform.log);
66
+ }
67
+ break;
68
+ }
69
+ case NotifyMessageTypes.LocalMessage: {
70
+ const data = messageData;
71
+ const robot = platform.robots.get(duid);
72
+ if (robot && data) {
73
+ await handleLocalMessage(data, platform, duid);
74
+ return;
75
+ }
76
+ platform.log.error(`Error2: Robot with DUID ${duid} not found`);
77
+ break;
78
+ }
79
+ case NotifyMessageTypes.CloudMessage: {
80
+ const data = messageData;
81
+ if (!data)
82
+ return;
83
+ await handleCloudMessage(data, platform, this, duid);
84
+ break;
85
+ }
86
+ default:
87
+ break;
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,213 @@
1
+ import axios from 'axios';
2
+ import crypto from 'node:crypto';
3
+ import { URLSearchParams } from 'node:url';
4
+ import { AuthenticateResponseCode } from '../Zenum/authenticateResponseCode.js';
5
+ export class RoborockAuthenticateApi {
6
+ logger;
7
+ axiosFactory;
8
+ deviceId;
9
+ username;
10
+ authToken;
11
+ cachedBaseUrl;
12
+ cachedCountry;
13
+ cachedCountryCode;
14
+ configRegion;
15
+ constructor(logger, axiosFactory = axios, deviceId, configRegion) {
16
+ this.deviceId = deviceId ?? crypto.randomUUID();
17
+ this.axiosFactory = axiosFactory;
18
+ this.logger = logger;
19
+ this.configRegion = configRegion;
20
+ }
21
+ async loginWithUserData(username, userData) {
22
+ this.loginWithAuthToken(username, userData.token);
23
+ return userData;
24
+ }
25
+ async loginWithPassword(username, password) {
26
+ const api = await this.getAPIFor(username);
27
+ const response = await api.post('api/v1/login', new URLSearchParams({
28
+ username: username,
29
+ password: password,
30
+ needtwostepauth: 'false',
31
+ }).toString());
32
+ return this.auth(username, response.data);
33
+ }
34
+ async requestCodeV4(email) {
35
+ const api = await this.getAPIFor(email);
36
+ const response = await api.post('api/v4/email/code/send', new URLSearchParams({
37
+ email: email,
38
+ type: 'login',
39
+ platform: '',
40
+ }), {
41
+ headers: {
42
+ 'Content-Type': 'application/x-www-form-urlencoded',
43
+ },
44
+ });
45
+ const apiResponse = response.data;
46
+ if (apiResponse.code === AuthenticateResponseCode.AccountNotFound) {
47
+ throw new Error(`Account not found for email: ${email}`);
48
+ }
49
+ if (apiResponse.code === AuthenticateResponseCode.RateLimited) {
50
+ throw new Error('Rate limited. Please wait before requesting another code.');
51
+ }
52
+ if (apiResponse.code !== AuthenticateResponseCode.Success && apiResponse.code !== undefined) {
53
+ throw new Error(`Failed to send verification code: ${apiResponse.msg} (code: ${apiResponse.code})`);
54
+ }
55
+ this.logger.debug('Verification code requested successfully');
56
+ }
57
+ async loginWithCodeV4(email, code) {
58
+ const api = await this.getAPIFor(email);
59
+ const xMercyKs = this.generateRandomString(16);
60
+ const xMercyK = await this.signKeyV3(api, xMercyKs);
61
+ const response = await api.post('api/v4/auth/email/login/code', null, {
62
+ params: {
63
+ email: email,
64
+ code: code,
65
+ country: this.cachedCountry ?? '',
66
+ countryCode: this.cachedCountryCode ?? '',
67
+ majorVersion: '14',
68
+ minorVersion: '0',
69
+ },
70
+ headers: {
71
+ 'Content-Type': 'application/x-www-form-urlencoded',
72
+ 'x-mercy-ks': xMercyKs,
73
+ 'x-mercy-k': xMercyK,
74
+ header_appversion: '4.54.02',
75
+ header_phonesystem: 'iOS',
76
+ header_phonemodel: 'iPhone16,1',
77
+ },
78
+ });
79
+ return this.authV4(email, response.data);
80
+ }
81
+ async getHomeDetails() {
82
+ if (!this.username || !this.authToken) {
83
+ return undefined;
84
+ }
85
+ const api = await this.getAPIFor(this.username);
86
+ const response = await api.get('api/v1/getHomeDetail');
87
+ const apiResponse = response.data;
88
+ if (!apiResponse.data) {
89
+ throw new Error('Failed to retrieve the home details');
90
+ }
91
+ return apiResponse.data;
92
+ }
93
+ getCachedCountryInfo() {
94
+ return {
95
+ country: this.cachedCountry,
96
+ countryCode: this.cachedCountryCode,
97
+ };
98
+ }
99
+ async getAPIFor(username) {
100
+ let baseUrl;
101
+ if (this.configRegion) {
102
+ baseUrl = this.getBaseUrlForRegion(this.configRegion);
103
+ this.logger.info(`Using configured region: ${this.configRegion}`);
104
+ }
105
+ else {
106
+ baseUrl = await this.getBaseUrl(username);
107
+ this.logger.info(`Auto-detected region from email`);
108
+ }
109
+ return this.apiForUser(username, baseUrl);
110
+ }
111
+ async getBaseUrl(username) {
112
+ if (this.cachedBaseUrl && this.username === username) {
113
+ return this.cachedBaseUrl;
114
+ }
115
+ const api = await this.apiForUser(username);
116
+ const response = await api.post('api/v1/getUrlByEmail', new URLSearchParams({
117
+ email: username,
118
+ needtwostepauth: 'false',
119
+ }).toString());
120
+ const apiResponse = response.data;
121
+ if (!apiResponse.data) {
122
+ throw new Error('Failed to retrieve base URL: ' + apiResponse.msg);
123
+ }
124
+ this.cachedBaseUrl = apiResponse.data.url;
125
+ this.cachedCountry = apiResponse.data.country;
126
+ this.cachedCountryCode = apiResponse.data.countrycode;
127
+ this.username = username;
128
+ return apiResponse.data.url;
129
+ }
130
+ getBaseUrlForRegion(region) {
131
+ const regionUrls = {
132
+ 'eu': 'https://euiot.roborock.com',
133
+ 'us': 'https://usiot.roborock.com',
134
+ 'cn': 'https://iotcn.roborock.com',
135
+ };
136
+ return regionUrls[region.toLowerCase()] || regionUrls['us'];
137
+ }
138
+ async apiForUser(username, baseUrl = 'https://usiot.roborock.com') {
139
+ const instance = this.axiosFactory.create({
140
+ baseURL: baseUrl,
141
+ headers: {
142
+ header_clientid: crypto.createHash('md5').update(username).update(this.deviceId).digest('base64'),
143
+ Authorization: this.authToken,
144
+ header_clientlang: 'en',
145
+ },
146
+ });
147
+ instance.interceptors.request.use((config) => {
148
+ this.logger.debug('=== HTTP Request ===');
149
+ this.logger.debug(`URL: ${config.baseURL}/${config.url}`);
150
+ this.logger.debug(`Method: ${config.method?.toUpperCase()}`);
151
+ this.logger.debug(`Params: ${JSON.stringify(config.params)}`);
152
+ this.logger.debug(`Data: ${JSON.stringify(config.data)}`);
153
+ this.logger.debug(`Headers: ${JSON.stringify(config.headers)}`);
154
+ return config;
155
+ });
156
+ instance.interceptors.response.use((response) => {
157
+ this.logger.debug('=== HTTP Response ===');
158
+ this.logger.debug(`Status: ${response.status}`);
159
+ this.logger.debug(`Data: ${JSON.stringify(response.data)}`);
160
+ return response;
161
+ }, (error) => {
162
+ this.logger.debug('=== HTTP Error ===');
163
+ this.logger.debug(`Error: ${JSON.stringify(error.response?.data ?? error.message)}`);
164
+ return Promise.reject(error);
165
+ });
166
+ return instance;
167
+ }
168
+ auth(username, response) {
169
+ const userdata = response.data;
170
+ if (!userdata || !userdata.token) {
171
+ throw new Error('Authentication failed: ' + response.msg + ' code: ' + response.code);
172
+ }
173
+ this.loginWithAuthToken(username, userdata.token);
174
+ return userdata;
175
+ }
176
+ authV4(email, response) {
177
+ if (response.code === AuthenticateResponseCode.InvalidCode) {
178
+ throw new Error('Invalid verification code. Please check and try again.');
179
+ }
180
+ if (response.code === AuthenticateResponseCode.RateLimited) {
181
+ throw new Error('Rate limited. Please wait before trying again.');
182
+ }
183
+ const userdata = response.data;
184
+ if (!userdata || !userdata.token) {
185
+ throw new Error('Authentication failed: ' + response.msg + ' code: ' + response.code);
186
+ }
187
+ this.loginWithAuthToken(email, userdata.token);
188
+ return userdata;
189
+ }
190
+ loginWithAuthToken(username, token) {
191
+ this.username = username;
192
+ this.authToken = token;
193
+ }
194
+ generateRandomString(length) {
195
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
196
+ let result = '';
197
+ const randomBytes = crypto.randomBytes(length);
198
+ for (let i = 0; i < length; i++) {
199
+ result += chars[randomBytes[i] % chars.length];
200
+ }
201
+ return result;
202
+ }
203
+ async signKeyV3(api, s) {
204
+ const response = await api.post('api/v3/key/sign', null, {
205
+ params: { s },
206
+ });
207
+ const apiResponse = response.data;
208
+ if (!apiResponse.data?.k) {
209
+ throw new Error('Failed to sign key: ' + apiResponse.msg);
210
+ }
211
+ return apiResponse.data.k;
212
+ }
213
+ }