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,302 @@
1
+ import axios, { AxiosInstance, AxiosStatic } from 'axios';
2
+ import crypto from 'node:crypto';
3
+ import { AnsiLogger } from 'matterbridge/logger';
4
+ import { URLSearchParams } from 'node:url';
5
+ import { AuthenticateResponse } from '../Zmodel/authenticateResponse.js';
6
+ import { BaseUrl } from '../Zmodel/baseURL.js';
7
+ import { HomeInfo } from '../Zmodel/homeInfo.js';
8
+ import { UserData } from '../Zmodel/userData.js';
9
+ import { AuthenticateResponseCode } from '../Zenum/authenticateResponseCode.js';
10
+
11
+ export class RoborockAuthenticateApi {
12
+ private readonly logger: AnsiLogger;
13
+ private axiosFactory: AxiosStatic;
14
+ private deviceId: string;
15
+ private username?: string;
16
+ private authToken?: string;
17
+ // Cached values from base URL lookup for v4 login
18
+ private cachedBaseUrl?: string;
19
+ private cachedCountry?: string;
20
+ private cachedCountryCode?: string;
21
+ private configRegion?: string;
22
+
23
+ constructor(logger: AnsiLogger, axiosFactory: AxiosStatic = axios, deviceId?: string, configRegion?: string) {
24
+ this.deviceId = deviceId ?? crypto.randomUUID();
25
+ this.axiosFactory = axiosFactory;
26
+ this.logger = logger;
27
+ this.configRegion = configRegion;
28
+ }
29
+
30
+ public async loginWithUserData(username: string, userData: UserData): Promise<UserData> {
31
+ this.loginWithAuthToken(username, userData.token);
32
+ return userData;
33
+ }
34
+
35
+ /**
36
+ * @deprecated Use requestCodeV4 and loginWithCodeV4 instead
37
+ */
38
+ public async loginWithPassword(username: string, password: string): Promise<UserData> {
39
+ const api = await this.getAPIFor(username);
40
+ const response = await api.post(
41
+ 'api/v1/login',
42
+ new URLSearchParams({
43
+ username: username,
44
+ password: password,
45
+ needtwostepauth: 'false',
46
+ }).toString(),
47
+ );
48
+ return this.auth(username, response.data);
49
+ }
50
+
51
+ /**
52
+ * Request a verification code to be sent to the user's email
53
+ * @param email - The user's email address
54
+ * @throws Error if the account is not found, rate limited, or other API error
55
+ */
56
+ public async requestCodeV4(email: string): Promise<void> {
57
+ const api = await this.getAPIFor(email);
58
+ const response = await api.post(
59
+ 'api/v4/email/code/send',
60
+ new URLSearchParams({
61
+ email: email,
62
+ type: 'login',
63
+ platform: '',
64
+ }),
65
+ {
66
+ headers: {
67
+ 'Content-Type': 'application/x-www-form-urlencoded',
68
+ },
69
+ },
70
+ );
71
+
72
+ const apiResponse: AuthenticateResponse<unknown> = response.data;
73
+
74
+ if (apiResponse.code === AuthenticateResponseCode.AccountNotFound) {
75
+ throw new Error(`Account not found for email: ${email}`);
76
+ }
77
+ if (apiResponse.code === AuthenticateResponseCode.RateLimited) {
78
+ throw new Error('Rate limited. Please wait before requesting another code.');
79
+ }
80
+ if (apiResponse.code !== AuthenticateResponseCode.Success && apiResponse.code !== undefined) {
81
+ throw new Error(`Failed to send verification code: ${apiResponse.msg} (code: ${apiResponse.code})`);
82
+ }
83
+
84
+ this.logger.debug('Verification code requested successfully');
85
+ }
86
+
87
+ /**
88
+ * Login with a verification code received via email
89
+ * @param email - The user's email address
90
+ * @param code - The 6-digit verification code
91
+ * @returns UserData on successful authentication
92
+ * @throws Error if the code is invalid, rate limited, or other API error
93
+ */
94
+ public async loginWithCodeV4(email: string, code: string): Promise<UserData> {
95
+ const api = await this.getAPIFor(email);
96
+
97
+ // Generate x_mercy_ks (random 16-char alphanumeric string)
98
+ const xMercyKs = this.generateRandomString(16);
99
+
100
+ // Get signed key from API
101
+ const xMercyK = await this.signKeyV3(api, xMercyKs);
102
+
103
+ const response = await api.post('api/v4/auth/email/login/code', null, {
104
+ params: {
105
+ email: email,
106
+ code: code,
107
+ country: this.cachedCountry ?? '',
108
+ countryCode: this.cachedCountryCode ?? '',
109
+ majorVersion: '14',
110
+ minorVersion: '0',
111
+ },
112
+ headers: {
113
+ 'Content-Type': 'application/x-www-form-urlencoded',
114
+ 'x-mercy-ks': xMercyKs,
115
+ 'x-mercy-k': xMercyK,
116
+ header_appversion: '4.54.02',
117
+ header_phonesystem: 'iOS',
118
+ header_phonemodel: 'iPhone16,1',
119
+ },
120
+ });
121
+
122
+ return this.authV4(email, response.data);
123
+ }
124
+
125
+ public async getHomeDetails(): Promise<HomeInfo | undefined> {
126
+ if (!this.username || !this.authToken) {
127
+ return undefined;
128
+ }
129
+
130
+ const api = await this.getAPIFor(this.username);
131
+ const response = await api.get('api/v1/getHomeDetail');
132
+
133
+ const apiResponse: AuthenticateResponse<HomeInfo> = response.data;
134
+ if (!apiResponse.data) {
135
+ throw new Error('Failed to retrieve the home details');
136
+ }
137
+ return apiResponse.data;
138
+ }
139
+
140
+ /**
141
+ * Get cached country info from the last base URL lookup
142
+ */
143
+ public getCachedCountryInfo(): { country?: string; countryCode?: string } {
144
+ return {
145
+ country: this.cachedCountry,
146
+ countryCode: this.cachedCountryCode,
147
+ };
148
+ }
149
+
150
+ private async getAPIFor(username: string): Promise<AxiosInstance> {
151
+ let baseUrl: string;
152
+
153
+ // Check if region is manually configured
154
+ if (this.configRegion) {
155
+ baseUrl = this.getBaseUrlForRegion(this.configRegion);
156
+ this.logger.info(`Using configured region: ${this.configRegion}`);
157
+ } else {
158
+ // Fall back to auto-detection
159
+ baseUrl = await this.getBaseUrl(username);
160
+ this.logger.info(`Auto-detected region from email`);
161
+ }
162
+
163
+ return this.apiForUser(username, baseUrl);
164
+ }
165
+
166
+ private async getBaseUrl(username: string): Promise<string> {
167
+ if (this.cachedBaseUrl && this.username === username) {
168
+ return this.cachedBaseUrl;
169
+ }
170
+
171
+ const api = await this.apiForUser(username);
172
+ const response = await api.post(
173
+ 'api/v1/getUrlByEmail',
174
+ new URLSearchParams({
175
+ email: username,
176
+ needtwostepauth: 'false',
177
+ }).toString(),
178
+ );
179
+
180
+ const apiResponse: AuthenticateResponse<BaseUrl> = response.data;
181
+ if (!apiResponse.data) {
182
+ throw new Error('Failed to retrieve base URL: ' + apiResponse.msg);
183
+ }
184
+
185
+ this.cachedBaseUrl = apiResponse.data.url;
186
+ this.cachedCountry = apiResponse.data.country;
187
+ this.cachedCountryCode = apiResponse.data.countrycode;
188
+ this.username = username;
189
+
190
+ return apiResponse.data.url;
191
+ }
192
+
193
+ private getBaseUrlForRegion(region: string): string {
194
+ const regionUrls: { [key: string]: string } = {
195
+ 'eu': 'https://euiot.roborock.com',
196
+ 'us': 'https://usiot.roborock.com',
197
+ 'cn': 'https://iotcn.roborock.com',
198
+ };
199
+ return regionUrls[region.toLowerCase()] || regionUrls['us'];
200
+ }
201
+
202
+ private async apiForUser(username: string, baseUrl = 'https://usiot.roborock.com'): Promise<AxiosInstance> {
203
+ const instance = this.axiosFactory.create({
204
+ baseURL: baseUrl,
205
+ headers: {
206
+ header_clientid: crypto.createHash('md5').update(username).update(this.deviceId).digest('base64'),
207
+ Authorization: this.authToken,
208
+ header_clientlang: 'en',
209
+ },
210
+ });
211
+
212
+ instance.interceptors.request.use((config) => {
213
+ this.logger.debug('=== HTTP Request ===');
214
+ this.logger.debug(`URL: ${config.baseURL}/${config.url}`);
215
+ this.logger.debug(`Method: ${config.method?.toUpperCase()}`);
216
+ this.logger.debug(`Params: ${JSON.stringify(config.params)}`);
217
+ this.logger.debug(`Data: ${JSON.stringify(config.data)}`);
218
+ this.logger.debug(`Headers: ${JSON.stringify(config.headers)}`);
219
+ return config;
220
+ });
221
+
222
+ instance.interceptors.response.use(
223
+ (response) => {
224
+ this.logger.debug('=== HTTP Response ===');
225
+ this.logger.debug(`Status: ${response.status}`);
226
+ this.logger.debug(`Data: ${JSON.stringify(response.data)}`);
227
+ return response;
228
+ },
229
+ (error) => {
230
+ this.logger.debug('=== HTTP Error ===');
231
+ this.logger.debug(`Error: ${JSON.stringify(error.response?.data ?? error.message)}`);
232
+ return Promise.reject(error);
233
+ },
234
+ );
235
+
236
+ return instance;
237
+ }
238
+
239
+ private auth(username: string, response: AuthenticateResponse<UserData>): UserData {
240
+ const userdata = response.data;
241
+ if (!userdata || !userdata.token) {
242
+ throw new Error('Authentication failed: ' + response.msg + ' code: ' + response.code);
243
+ }
244
+
245
+ this.loginWithAuthToken(username, userdata.token);
246
+ return userdata;
247
+ }
248
+
249
+ /**
250
+ * Handle v4 authentication response with specific error code handling
251
+ */
252
+ private authV4(email: string, response: AuthenticateResponse<UserData>): UserData {
253
+ if (response.code === AuthenticateResponseCode.InvalidCode) {
254
+ throw new Error('Invalid verification code. Please check and try again.');
255
+ }
256
+ if (response.code === AuthenticateResponseCode.RateLimited) {
257
+ throw new Error('Rate limited. Please wait before trying again.');
258
+ }
259
+
260
+ const userdata = response.data;
261
+ if (!userdata || !userdata.token) {
262
+ throw new Error('Authentication failed: ' + response.msg + ' code: ' + response.code);
263
+ }
264
+
265
+ this.loginWithAuthToken(email, userdata.token);
266
+ return userdata;
267
+ }
268
+
269
+ private loginWithAuthToken(username: string, token: string): void {
270
+ this.username = username;
271
+ this.authToken = token;
272
+ }
273
+
274
+ /**
275
+ * Generate a random alphanumeric string of specified length
276
+ */
277
+ private generateRandomString(length: number): string {
278
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
279
+ let result = '';
280
+ const randomBytes = crypto.randomBytes(length);
281
+ for (let i = 0; i < length; i++) {
282
+ result += chars[randomBytes[i] % chars.length];
283
+ }
284
+ return result;
285
+ }
286
+
287
+ /**
288
+ * Sign a key using the v3 API endpoint
289
+ */
290
+ private async signKeyV3(api: AxiosInstance, s: string): Promise<string> {
291
+ const response = await api.post('api/v3/key/sign', null, {
292
+ params: { s },
293
+ });
294
+
295
+ const apiResponse: AuthenticateResponse<{ k: string }> = response.data;
296
+ if (!apiResponse.data?.k) {
297
+ throw new Error('Failed to sign key: ' + apiResponse.msg);
298
+ }
299
+
300
+ return apiResponse.data.k;
301
+ }
302
+ }
@@ -0,0 +1,107 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import crypto from 'node:crypto';
3
+ import { AnsiLogger, debugStringify } from 'matterbridge/logger';
4
+ import { ApiResponse } from '../Zmodel/apiResponse.js';
5
+ import { Home } from '../Zmodel/home.js';
6
+ import { UserData } from '../Zmodel/userData.js';
7
+ import { Scene } from '../Zmodel/scene.js';
8
+
9
+ export class RoborockIoTApi {
10
+ logger: AnsiLogger;
11
+ private readonly api: AxiosInstance;
12
+
13
+ constructor(userdata: UserData, logger: AnsiLogger) {
14
+ this.logger = logger;
15
+
16
+ this.api = axios.create({ baseURL: userdata.rriot.r.a });
17
+ this.api.interceptors.request.use((config) => {
18
+ try {
19
+ const timestamp = Math.floor(Date.now() / 1000);
20
+ const nonce = crypto
21
+ .randomBytes(6)
22
+ .toString('base64')
23
+ .substring(0, 6)
24
+ .replace(/[+/]/g, (m) => (m === '+' ? 'X' : 'Y'));
25
+ const url = this.api ? new URL(this.api.getUri(config)).pathname : '';
26
+ const data = [userdata.rriot.u, userdata.rriot.s, nonce, timestamp, crypto.createHash('md5').update(url).digest('hex'), '', ''].join(':');
27
+ const hmac = crypto.createHmac('sha256', userdata.rriot.h).update(data).digest('base64');
28
+ config.headers['Authorization'] = `Hawk id="${userdata.rriot.u}", s="${userdata.rriot.s}", ts="${timestamp}", nonce="${nonce}", mac="${hmac}"`;
29
+ } catch (error) {
30
+ this.logger.error(`Failed to initialize RESTAPI ${error ? debugStringify(error) : 'undefined'}`);
31
+ }
32
+ return config;
33
+ });
34
+ }
35
+
36
+ public async getHome(homeId: number): Promise<Home | undefined> {
37
+ const result = await this.api.get(`user/homes/${homeId}`);
38
+
39
+ const apiResponse: ApiResponse<Home> = result.data;
40
+ if (apiResponse.result) {
41
+ return apiResponse.result;
42
+ } else {
43
+ this.logger.error('Failed to retrieve the home data');
44
+ return undefined;
45
+ }
46
+ }
47
+
48
+ public async getHomev2(homeId: number): Promise<Home | undefined> {
49
+ const result = await this.api.get('v2/user/homes/' + homeId);
50
+
51
+ const apiResponse: ApiResponse<Home> = result.data;
52
+ if (apiResponse.result) {
53
+ return apiResponse.result;
54
+ } else {
55
+ this.logger.error('Failed to retrieve the home data');
56
+ return undefined;
57
+ }
58
+ }
59
+
60
+ public async getHomev3(homeId: number): Promise<Home | undefined> {
61
+ const result = await this.api.get('v3/user/homes/' + homeId); // can be v3 also
62
+
63
+ const apiResponse: ApiResponse<Home> = result.data;
64
+ if (apiResponse.result) {
65
+ return apiResponse.result;
66
+ } else {
67
+ this.logger.error('Failed to retrieve the home data');
68
+ return undefined;
69
+ }
70
+ }
71
+
72
+ public async getScenes(homeId: number): Promise<Scene[] | undefined> {
73
+ const result = await this.api.get('user/scene/home/' + homeId);
74
+
75
+ const apiResponse: ApiResponse<Scene[]> = result.data;
76
+ if (apiResponse.result) {
77
+ return apiResponse.result;
78
+ } else {
79
+ this.logger.error('Failed to retrieve scene');
80
+ return undefined;
81
+ }
82
+ }
83
+
84
+ public async startScene(sceneId: number): Promise<unknown> {
85
+ const result = await this.api.post(`user/scene/${sceneId}/execute`);
86
+ const apiResponse: ApiResponse<unknown> = result.data;
87
+
88
+ if (apiResponse.result) {
89
+ return apiResponse.result;
90
+ } else {
91
+ this.logger.error('Failed to execute scene');
92
+ return undefined;
93
+ }
94
+ }
95
+
96
+ public async getCustom(url: string): Promise<unknown> {
97
+ const result = await this.api.get(url);
98
+ const apiResponse: ApiResponse<unknown> = result.data;
99
+
100
+ if (apiResponse.result) {
101
+ return apiResponse.result;
102
+ } else {
103
+ this.logger.error('Failed to execute scene');
104
+ return undefined;
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,3 @@
1
+ export enum AdditionalPropCode {
2
+ map_change = 4,
3
+ }
@@ -0,0 +1,6 @@
1
+ export enum AuthenticateResponseCode {
2
+ Success = 200,
3
+ AccountNotFound = 2008,
4
+ InvalidCode = 2018,
5
+ RateLimited = 9002,
6
+ }
@@ -0,0 +1,3 @@
1
+ export enum DockType {
2
+ qrevo_curv_dock = 17,
3
+ }
@@ -0,0 +1,43 @@
1
+ export enum OperationStatusCode {
2
+ Unknown = 0,
3
+ Initiating = 1,
4
+ Sleeping = 2, // or ChargerDisconnected
5
+ Idle = 3,
6
+ RemoteControl = 4,
7
+ Cleaning = 5,
8
+ ReturningDock = 6,
9
+ ManualMode = 7,
10
+ Charging = 8,
11
+ ChargingError = 9,
12
+ Paused = 10,
13
+ SpotCleaning = 11,
14
+ InError = 12,
15
+ ShuttingDown = 13,
16
+ Updating = 14,
17
+ ReturnToDock = 15,
18
+ GoTo = 16,
19
+ ZoneClean = 17,
20
+ RoomClean = 18,
21
+ EmptyingDustContainer = 22,
22
+ WashingTheMop = 23,
23
+ WashingTheMop2 = 25,
24
+ GoingToWashTheMop = 26,
25
+ InCall = 28,
26
+ Mapping = 29,
27
+ SOMETHING_NEED_TO_FIGUREOUT = 30,
28
+ Patrol = 32,
29
+ FullyCharged = 100,
30
+ DeviceOffline = 101,
31
+ Locked = 103,
32
+ AirDryingStopping = 202,
33
+ RobotStatusMopping = 6301,
34
+ CleanMopCleaning = 6302,
35
+ CleanMopMopping = 6303,
36
+ RoomMopping = 6304,
37
+ RoomCleanMopCleaning = 6305,
38
+ RoomCleanMopMopping = 6306,
39
+ ZoneMopping = 6307,
40
+ ZoneCleanMopCleaning = 6308,
41
+ ZoneCleanMopMopping = 6309,
42
+ BackToDockWashingDuster = 6310,
43
+ }
@@ -0,0 +1,68 @@
1
+ export enum VacuumErrorCode {
2
+ None = 0,
3
+ LidarBlocked = 1,
4
+ BumperStuck = 2,
5
+ WheelsSuspended = 3,
6
+ CliffSensorError = 4,
7
+ MainBrushJammed = 5,
8
+ SideBrushJammed = 6,
9
+ WheelsJammed = 7,
10
+ RobotTrapped = 8,
11
+ NoDustbin = 9,
12
+ StrainerError = 10,
13
+ CompassError = 11,
14
+ LowBattery = 12,
15
+ ChargingError = 13,
16
+ BatteryError = 14,
17
+ WallSensorDirty = 15,
18
+ RobotTilted = 16,
19
+ SideBrushError = 17,
20
+ FanError = 18,
21
+ DockNotConnectedToPower = 19,
22
+ OpticalFlowSensorDirt = 20,
23
+ VerticalBumperPressed = 21,
24
+ DockLocatorError = 22,
25
+ ReturnToDockFail = 23,
26
+ NogoZoneDetected = 24,
27
+ CameraError = 25,
28
+ WallSensorError = 26,
29
+ VibrariseJammed = 27,
30
+ RobotOnCarpet = 28,
31
+ FilterBlocked = 29,
32
+ InvisibleWallDetected = 30,
33
+ CannotCrossCarpet = 31,
34
+ InternalError = 32,
35
+ CleanAutoEmptyDock = 34,
36
+ AutoEmptyDockVoltage = 35,
37
+ MoppingRollerJammed = 36, // Wash roller may be jammed
38
+ MoppingRollerNotLowered = 37, // wash roller not lowered properly
39
+ ClearWaterBoxHoare = 38,
40
+ DirtyWaterBoxHoare = 39,
41
+ SinkStrainerHoare = 40,
42
+ ClearWaterTankEmpty = 41,
43
+ ClearBrushInstalledProperly = 42, // Check that the water filter has been correctly installed
44
+ ClearBrushPositioningError = 43,
45
+ FilterScreenException = 44, // Clean the dock water filter
46
+ MoppingRollerJammed2 = 45, // Wash roller may be jammed
47
+ UpWaterException = 48,
48
+ DrainWaterException = 49,
49
+ TemperatureProtection = 51,
50
+ CleanCarouselException = 52,
51
+ CleanCarouselWaterFull = 53,
52
+ WaterCarriageDrop = 54,
53
+ CheckCleanCarouse = 55,
54
+ AudioError = 56,
55
+ }
56
+
57
+ export enum DockErrorCode {
58
+ None = 0,
59
+ DuctBlockage = 34, // Duct blockage detected
60
+ WaterEmpty = 38, // Clean water tank empty
61
+ WasteWaterTankFull = 39, // Waste water tank full
62
+ MaintenanceBrushJammed = 42, // Maintenance brush jammed
63
+ DirtyTankLatchOpen = 44, // Dirty tank latch open
64
+ NoDustbin = 46, // No dustbin detected
65
+ CleaningTankFullOrBlocked = 53, // Cleaning tank full or blocked
66
+ }
67
+
68
+ export const SUPPORTED_ATTACHMENTS = ['WATERTANK', 'MOP'];
@@ -0,0 +1,3 @@
1
+ export interface ApiResponse<T> {
2
+ result?: T;
3
+ }
@@ -0,0 +1,6 @@
1
+ export interface AuthenticateFlowState {
2
+ email: string;
3
+ codeRequestedAt?: number;
4
+ country?: string;
5
+ countryCode?: string;
6
+ }
@@ -0,0 +1,5 @@
1
+ export interface AuthenticateResponse<T> {
2
+ msg?: string;
3
+ data?: T;
4
+ code?: number;
5
+ }
@@ -0,0 +1,5 @@
1
+ export interface BaseUrl {
2
+ url: string;
3
+ country: string;
4
+ countrycode: string;
5
+ }
@@ -0,0 +1,16 @@
1
+ import { DeviceStatus, VacuumErrorCode } from '../index.js';
2
+ import { CloudMessageResult } from './messageResult.js';
3
+
4
+ export interface BatteryMessage {
5
+ duid: string;
6
+ percentage: number;
7
+ }
8
+
9
+ export interface DeviceErrorMessage {
10
+ duid: string;
11
+ errorCode: VacuumErrorCode;
12
+ }
13
+
14
+ export interface DeviceStatusNotify extends DeviceStatus, CloudMessageResult {
15
+ duid: string;
16
+ }
@@ -0,0 +1,50 @@
1
+ import { DeviceSchema } from './deviceSchema.js';
2
+ import { Room } from './room.js';
3
+ import { Scene } from './scene.js';
4
+ import { UserData } from './userData.js';
5
+
6
+ export interface DeviceData {
7
+ id: string;
8
+ firmwareVersion: string;
9
+ serialNumber: string;
10
+ model: string;
11
+ category: string;
12
+ batteryLevel: number;
13
+ }
14
+
15
+ interface DeviceInformation {
16
+ userData: UserData;
17
+ localKey: string;
18
+ pv: string;
19
+ model?: string;
20
+ }
21
+
22
+ export interface Device {
23
+ duid: string;
24
+ name: string;
25
+ sn: string;
26
+ serialNumber: string;
27
+
28
+ featureSet?: string;
29
+ newFeatureSet?: string;
30
+ silentOtaSwitch?: boolean;
31
+
32
+ activeTime: number;
33
+ createTime: number;
34
+ localKey: string;
35
+
36
+ /** The protocol version of the robot. */
37
+ pv: string;
38
+ online: boolean;
39
+ productId: string;
40
+ rrHomeId: number;
41
+ /** The firmware version of the robot. */
42
+ fv: string;
43
+
44
+ deviceStatus: Record<string, number>;
45
+ rooms: Room[];
46
+ schema: DeviceSchema[];
47
+ data: DeviceData;
48
+ store?: DeviceInformation;
49
+ scenes?: Scene[];
50
+ }
@@ -0,0 +1,27 @@
1
+ export enum DeviceModel {
2
+ Q5 = 'roborock.vacuum.a34',
3
+ Q5_PRO = 'roborock.vacuum.a72',
4
+ S5 = 'roborock.vacuum.s5',
5
+ S5_MAX = 'roborock.vacuum.s5e',
6
+ S6 = 'roborock.vacuum.s6',
7
+ S6_MAXV = 'roborock.vacuum.a10',
8
+ S6_PURE = 'roborock.vacuum.a08',
9
+ Q7 = 'roborock.vacuum.a40',
10
+ Q7_PLUS = 'roborock.vacuum.axx', // TODO
11
+ Q7_MAX = 'roborock.vacuum.a38',
12
+ S7 = 'roborock.vacuum.a15',
13
+ S7_MAXV = 'roborock.vacuum.a27',
14
+ S7_MAXV_ULTRA = 'roborock.vacuum.a65',
15
+ S7_PRO_ULTRA = 'roborock.vacuum.a62',
16
+ Q8_MAX = 'roborock.vacuum.a73',
17
+ S8 = 'roborock.vacuum.a51',
18
+ S8_PRO_ULTRA = 'roborock.vacuum.a70',
19
+ S8_MAXV_ULTRA = 'roborock.vacuum.a97',
20
+ QREVO_MASTER = 'roborock.vacuum.a117',
21
+ QREVO_CURV = 'roborock.vacuum.a135',
22
+ QREVO_S = 'roborock.vacuum.a104',
23
+ QREVO_PRO = 'roborock.vacuum.a101',
24
+ QREVO_MAXV = 'roborock.vacuum.a87',
25
+ QREVO_EDGE_5V1 = 'roborock.vacuum.a187',
26
+ QREVO_EDGE_5AE = 'roborock.vacuum.xxxx',
27
+ }
@@ -0,0 +1,8 @@
1
+ export interface DeviceSchema {
2
+ id: number;
3
+ name: string;
4
+ code: string;
5
+ mode: string;
6
+ type: string;
7
+ property: unknown;
8
+ }