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,657 @@
1
+ import assert from 'node:assert';
2
+ import { AnsiLogger, debugStringify } from 'matterbridge/logger';
3
+ import ClientManager from './clientManager.js';
4
+ import { NotifyMessageTypes } from './notifyMessageTypes.js';
5
+ import { clearInterval } from 'node:timers';
6
+ import {
7
+ RoborockAuthenticateApi,
8
+ UserData,
9
+ RoborockIoTApi,
10
+ ClientRouter,
11
+ MessageProcessor,
12
+ Client,
13
+ Device,
14
+ DeviceStatus,
15
+ Home,
16
+ Protocol,
17
+ RequestMessage,
18
+ VacuumErrorCode,
19
+ ResponseMessage,
20
+ Scene,
21
+ SceneParam,
22
+ MapInfo,
23
+ } from './roborockCommunication/index.js';
24
+ import type { AbstractMessageHandler, AbstractMessageListener, BatteryMessage, DeviceErrorMessage, DeviceStatusNotify, MultipleMap } from './roborockCommunication/index.js';
25
+ import { ServiceArea } from 'matterbridge/matter/clusters';
26
+ import { LocalNetworkClient } from './roborockCommunication/broadcast/client/LocalNetworkClient.js';
27
+ import { RoomIndexMap } from './model/roomIndexMap.js';
28
+ import { DpsPayload } from './roborockCommunication/broadcast/model/dps.js';
29
+ import { CleanModeSetting } from './behaviors/roborock.vacuum/default/default.js';
30
+ export type Factory<A, T> = (logger: AnsiLogger, arg: A) => T;
31
+
32
+ interface MapRoomResponse {
33
+ vacuumRoom?: number;
34
+ }
35
+
36
+ interface Security {
37
+ nonce: number;
38
+ }
39
+
40
+ export default class RoborockService {
41
+ private loginApi: RoborockAuthenticateApi;
42
+ private logger: AnsiLogger;
43
+ private readonly iotApiFactory: Factory<UserData, RoborockIoTApi>;
44
+
45
+ private iotApi?: RoborockIoTApi;
46
+ private userdata?: UserData;
47
+ deviceNotify?: (messageSource: NotifyMessageTypes, homeData: unknown) => void;
48
+ messageClient: ClientRouter | undefined;
49
+ remoteDevices = new Set<string>();
50
+ messageProcessorMap = new Map<string, MessageProcessor>();
51
+ ipMap = new Map<string, string>();
52
+ localClientMap = new Map<string, Client>();
53
+ mqttAlwaysOnDevices = new Map<string, boolean>();
54
+ clientManager: ClientManager;
55
+ refreshInterval: number;
56
+ requestDeviceStatusInterval: NodeJS.Timeout | undefined;
57
+
58
+ // These are properties that are used to store the state of the device
59
+ private supportedAreas = new Map<string, ServiceArea.Area[]>();
60
+ private supportedRoutines = new Map<string, ServiceArea.Area[]>();
61
+ private selectedAreas = new Map<string, number[]>();
62
+ private supportedAreaIndexMaps = new Map<string, RoomIndexMap>();
63
+
64
+ private readonly vacuumNeedAPIV3 = ['roborock.vacuum.ss07'];
65
+
66
+ constructor(
67
+ authenticateApiSupplier: Factory<void, RoborockAuthenticateApi> = (logger) => new RoborockAuthenticateApi(logger),
68
+ iotApiSupplier: Factory<UserData, RoborockIoTApi> = (logger, ud) => new RoborockIoTApi(ud, logger),
69
+ refreshInterval: number,
70
+ clientManager: ClientManager,
71
+ logger: AnsiLogger,
72
+ ) {
73
+ this.logger = logger;
74
+ this.loginApi = authenticateApiSupplier(logger);
75
+ this.iotApiFactory = iotApiSupplier;
76
+ this.refreshInterval = refreshInterval;
77
+ this.clientManager = clientManager;
78
+ }
79
+
80
+ /**
81
+ * @deprecated Use requestVerificationCode and loginWithVerificationCode instead
82
+ */
83
+ public async loginWithPassword(
84
+ username: string,
85
+ password: string,
86
+ loadSavedUserData: () => Promise<UserData | undefined>,
87
+ savedUserData: (userData: UserData) => Promise<void>,
88
+ ): Promise<UserData> {
89
+ let userdata = await loadSavedUserData();
90
+
91
+ if (!userdata) {
92
+ this.logger.debug('No saved user data found, logging in with password');
93
+ userdata = await this.loginApi.loginWithPassword(username, password);
94
+ await savedUserData(userdata);
95
+ } else {
96
+ this.logger.debug('Using saved user data for login', debugStringify(userdata));
97
+ userdata = await this.loginApi.loginWithUserData(username, userdata);
98
+ }
99
+
100
+ return this.auth(userdata);
101
+ }
102
+
103
+ /**
104
+ * Request a verification code to be sent to the user's email
105
+ * @param email - The user's email address
106
+ */
107
+ public async requestVerificationCode(email: string): Promise<void> {
108
+ return this.loginApi.requestCodeV4(email);
109
+ }
110
+
111
+ /**
112
+ * Login with a verification code received via email
113
+ * @param email - The user's email address
114
+ * @param code - The 6-digit verification code
115
+ * @param savedUserData - Callback to save the user data after successful login
116
+ * @returns UserData on successful authentication
117
+ */
118
+ public async loginWithVerificationCode(email: string, code: string, savedUserData: (userData: UserData) => Promise<void>): Promise<UserData> {
119
+ const userdata = await this.loginApi.loginWithCodeV4(email, code);
120
+ await savedUserData(userdata);
121
+ return this.auth(userdata);
122
+ }
123
+
124
+ /**
125
+ * Login with cached user data (token reuse)
126
+ * @param username - The user's email address
127
+ * @param userData - The cached user data with token
128
+ * @returns UserData on successful validation
129
+ */
130
+ public async loginWithCachedToken(username: string, userData: UserData): Promise<UserData> {
131
+ const validatedUserData = await this.loginApi.loginWithUserData(username, userData);
132
+ return this.auth(validatedUserData);
133
+ }
134
+
135
+ public getMessageProcessor(duid: string): MessageProcessor | undefined {
136
+ const messageProcessor = this.messageProcessorMap.get(duid);
137
+ if (!messageProcessor) {
138
+ this.logger.error('MessageApi is not initialized.');
139
+ }
140
+ return messageProcessor;
141
+ }
142
+
143
+ public setSelectedAreas(duid: string, selectedAreas: number[]): void {
144
+ this.logger.debug('RoborockService - setSelectedAreas', selectedAreas);
145
+ const roomIds = selectedAreas.map((areaId) => this.supportedAreaIndexMaps.get(duid)?.getRoomId(areaId)) ?? [];
146
+ this.logger.debug('RoborockService - setSelectedAreas - roomIds', roomIds);
147
+ this.selectedAreas.set(
148
+ duid,
149
+ roomIds.filter((id) => id !== undefined).map((id) => id),
150
+ );
151
+ }
152
+
153
+ public getSelectedAreas(duid: string): number[] {
154
+ return this.selectedAreas.get(duid) ?? [];
155
+ }
156
+
157
+ public setSupportedAreas(duid: string, supportedAreas: ServiceArea.Area[]): void {
158
+ this.supportedAreas.set(duid, supportedAreas);
159
+ }
160
+
161
+ public setSupportedAreaIndexMap(duid: string, indexMap: RoomIndexMap): void {
162
+ this.supportedAreaIndexMaps.set(duid, indexMap);
163
+ }
164
+
165
+ public setSupportedScenes(duid: string, routineAsRooms: ServiceArea.Area[]) {
166
+ this.supportedRoutines.set(duid, routineAsRooms);
167
+ }
168
+
169
+ public getSupportedAreas(duid: string): ServiceArea.Area[] | undefined {
170
+ return this.supportedAreas.get(duid);
171
+ }
172
+
173
+ public getSupportedAreasIndexMap(duid: string): RoomIndexMap | undefined {
174
+ return this.supportedAreaIndexMaps.get(duid);
175
+ }
176
+
177
+ public async getCleanModeData(duid: string): Promise<CleanModeSetting> {
178
+ this.logger.notice('RoborockService - getCleanModeData');
179
+ const data = await this.getMessageProcessor(duid)?.getCleanModeData(duid);
180
+ if (!data) {
181
+ throw new Error('Failed to retrieve clean mode data');
182
+ }
183
+ return data;
184
+ }
185
+
186
+ public async getRoomIdFromMap(duid: string): Promise<number | undefined> {
187
+ const data = (await this.customGet(duid, new RequestMessage({ method: 'get_map_v1' }))) as MapRoomResponse;
188
+ return data?.vacuumRoom;
189
+ }
190
+
191
+ public async getMapInformation(duid: string): Promise<MapInfo | undefined> {
192
+ this.logger.debug('RoborockService - getMapInformation', duid);
193
+ assert(this.messageClient !== undefined);
194
+ return this.messageClient.get<MultipleMap[] | undefined>(duid, new RequestMessage({ method: 'get_multi_maps_list' })).then((response) => {
195
+ this.logger.debug('RoborockService - getMapInformation response', debugStringify(response ?? []));
196
+ return response ? new MapInfo(response[0]) : undefined;
197
+ });
198
+ }
199
+
200
+ public async changeCleanMode(duid: string, { suctionPower, waterFlow, distance_off, mopRoute }: CleanModeSetting): Promise<void> {
201
+ this.logger.notice('RoborockService - changeCleanMode');
202
+ return this.getMessageProcessor(duid)?.changeCleanMode(duid, suctionPower, waterFlow, mopRoute ?? 0, distance_off);
203
+ }
204
+
205
+ public async startClean(duid: string): Promise<void> {
206
+ const supportedRooms = this.supportedAreas.get(duid) ?? [];
207
+ const supportedRoutines = this.supportedRoutines.get(duid) ?? [];
208
+ const selected = this.selectedAreas.get(duid) ?? [];
209
+ this.logger.debug('RoborockService - begin cleaning', debugStringify({ duid, supportedRooms, supportedRoutines, selected }));
210
+
211
+ if (supportedRoutines.length === 0) {
212
+ if (selected.length == supportedRooms.length || selected.length === 0 || supportedRooms.length === 0) {
213
+ this.logger.debug('RoborockService - startGlobalClean');
214
+ this.getMessageProcessor(duid)?.startClean(duid);
215
+ } else {
216
+ this.logger.debug('RoborockService - startRoomClean', debugStringify({ duid, selected }));
217
+ return this.getMessageProcessor(duid)?.startRoomClean(duid, selected, 1);
218
+ }
219
+ } else {
220
+ const rooms = selected.filter((slt) => supportedRooms.some((a: ServiceArea.Area) => a.areaId == slt));
221
+ const rt = selected.filter((slt) => supportedRoutines.some((a: ServiceArea.Area) => a.areaId == slt));
222
+
223
+ /**
224
+ * If multiple routines are selected, we log a warning. and continue with global clean
225
+ */
226
+ if (rt.length > 1) {
227
+ this.logger.warn('RoborockService - Multiple routines selected, which is not supported.', debugStringify({ duid, rt }));
228
+ } else if (rt.length === 1) {
229
+ this.logger.debug('RoborockService - startScene', debugStringify({ duid, rooms }));
230
+ await this.iotApi?.startScene(rt[0]);
231
+ return;
232
+ } else if (rooms.length == supportedRooms.length || rooms.length === 0 || supportedRooms.length === 0) {
233
+ /**
234
+ * If no rooms are selected, or all selected rooms match the supported rooms,
235
+ */
236
+ this.logger.debug('RoborockService - startGlobalClean');
237
+ this.getMessageProcessor(duid)?.startClean(duid);
238
+ } else if (rooms.length > 0) {
239
+ /**
240
+ * If there are rooms selected
241
+ */
242
+ this.logger.debug('RoborockService - startRoomClean', debugStringify({ duid, rooms }));
243
+ return this.getMessageProcessor(duid)?.startRoomClean(duid, rooms, 1);
244
+ } else {
245
+ this.logger.warn('RoborockService - something goes wrong.', debugStringify({ duid, rooms, rt, selected, supportedRooms, supportedRoutines }));
246
+ return;
247
+ }
248
+ }
249
+ }
250
+
251
+ public async pauseClean(duid: string): Promise<void> {
252
+ this.logger.debug('RoborockService - pauseClean');
253
+ await this.getMessageProcessor(duid)?.pauseClean(duid);
254
+ }
255
+
256
+ public async stopAndGoHome(duid: string): Promise<void> {
257
+ this.logger.debug('RoborockService - stopAndGoHome');
258
+ await this.getMessageProcessor(duid)?.gotoDock(duid);
259
+ }
260
+
261
+ public async resumeClean(duid: string): Promise<void> {
262
+ this.logger.debug('RoborockService - resumeClean');
263
+ await this.getMessageProcessor(duid)?.resumeClean(duid);
264
+ }
265
+
266
+ public async playSoundToLocate(duid: string): Promise<void> {
267
+ this.logger.debug('RoborockService - findMe');
268
+ await this.getMessageProcessor(duid)?.findMyRobot(duid);
269
+ }
270
+
271
+ public async customGet(duid: string, request: RequestMessage): Promise<unknown> {
272
+ this.logger.debug('RoborockService - customSend-message', request.method, request.params, request.secure);
273
+ return this.getMessageProcessor(duid)?.getCustomMessage(duid, request);
274
+ }
275
+
276
+ public async customSend(duid: string, request: RequestMessage): Promise<void> {
277
+ return this.getMessageProcessor(duid)?.sendCustomMessage(duid, request);
278
+ }
279
+
280
+ public async getCustomAPI(url: string): Promise<unknown> {
281
+ this.logger.debug('RoborockService - getCustomAPI', url);
282
+ assert(this.iotApi !== undefined);
283
+ try {
284
+ return await this.iotApi.getCustom(url);
285
+ } catch (error) {
286
+ this.logger.error(`Failed to get custom API with url ${url}: ${error ? debugStringify(error) : 'undefined'}`);
287
+ return { result: undefined, error: `Failed to get custom API with url ${url}` };
288
+ }
289
+ }
290
+
291
+ public stopService(): void {
292
+ if (this.messageClient) {
293
+ this.messageClient.disconnect();
294
+ this.messageClient = undefined;
295
+ }
296
+
297
+ if (this.localClientMap.size > 0) {
298
+ for (const [duid, client] of this.localClientMap.entries()) {
299
+ this.logger.debug('Disconnecting local client for device', duid);
300
+ client.disconnect();
301
+ this.localClientMap.delete(duid);
302
+ this.logger.debug('Local client disconnected for device', duid);
303
+ }
304
+ }
305
+
306
+ if (this.messageProcessorMap.size > 0) {
307
+ for (const [duid] of this.messageProcessorMap.entries()) {
308
+ this.logger.debug('Disconnecting message processor for device', duid);
309
+ this.messageProcessorMap.delete(duid);
310
+ this.logger.debug('Message processor disconnected for device', duid);
311
+ }
312
+ }
313
+
314
+ if (this.requestDeviceStatusInterval) {
315
+ clearInterval(this.requestDeviceStatusInterval);
316
+ this.requestDeviceStatusInterval = undefined;
317
+ }
318
+ }
319
+
320
+ public setDeviceNotify(callback: (messageSource: NotifyMessageTypes, homeData: unknown) => Promise<void>): void {
321
+ this.deviceNotify = callback;
322
+ }
323
+
324
+ public activateDeviceNotify(device: Device): void {
325
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
326
+ const self = this;
327
+ this.logger.debug('Requesting device info for device', device.duid);
328
+ const messageProcessor = this.getMessageProcessor(device.duid);
329
+ this.requestDeviceStatusInterval = setInterval(async () => {
330
+ if (messageProcessor) {
331
+ await messageProcessor.getDeviceStatus(device.duid).then((response: DeviceStatus | undefined) => {
332
+ if (self.deviceNotify && response) {
333
+ const message = { duid: device.duid, ...response.errorStatus, ...response.message } as DeviceStatusNotify;
334
+ self.logger.debug('Socket - Device status update', debugStringify(message));
335
+ self.deviceNotify(NotifyMessageTypes.LocalMessage, message);
336
+ }
337
+ });
338
+ } else {
339
+ self.logger.error('Local client not initialized');
340
+ }
341
+ }, this.refreshInterval * 1000);
342
+ }
343
+
344
+ public activateDeviceNotifyOverMQTT(device: Device): void {
345
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
346
+ const self = this;
347
+ this.logger.notice('Requesting device info for device over MQTT', device.duid);
348
+ const messageProcessor = this.getMessageProcessor(device.duid);
349
+ this.requestDeviceStatusInterval = setInterval(async () => {
350
+ if (messageProcessor) {
351
+ await messageProcessor.getDeviceStatusOverMQTT(device.duid).then((response: DeviceStatus | undefined) => {
352
+ if (self.deviceNotify && response) {
353
+ const message = { duid: device.duid, ...response.errorStatus, ...response.message } as DeviceStatusNotify;
354
+ self.logger.debug('MQTT - Device status update', debugStringify(message));
355
+ self.deviceNotify(NotifyMessageTypes.LocalMessage, message);
356
+ }
357
+ });
358
+ } else {
359
+ self.logger.error('Local client not initialized');
360
+ }
361
+ }, this.refreshInterval * 500);
362
+ }
363
+
364
+ public async listDevices(username: string): Promise<Device[]> {
365
+ assert(this.iotApi !== undefined);
366
+ assert(this.userdata !== undefined);
367
+
368
+ const homeDetails = await this.loginApi.getHomeDetails();
369
+ if (!homeDetails) {
370
+ throw new Error('Failed to retrieve the home details');
371
+ }
372
+
373
+ const homeData = (await this.iotApi.getHome(homeDetails.rrHomeId)) as Home;
374
+ if (!homeData) {
375
+ return [];
376
+ }
377
+
378
+ const scenes = (await this.iotApi.getScenes(homeDetails.rrHomeId)) ?? [];
379
+
380
+ const products = new Map<string, string>();
381
+ homeData.products.forEach((p) => products.set(p.id, p.model));
382
+
383
+ if (homeData.products.some((p) => this.vacuumNeedAPIV3.includes(p.model))) {
384
+ this.logger.debug('Using v3 API for home data retrieval');
385
+ const homeDataV3 = await this.iotApi.getHomev3(homeDetails.rrHomeId);
386
+ if (!homeDataV3) {
387
+ throw new Error('Failed to retrieve the home data from v3 API');
388
+ }
389
+ homeData.devices = [...homeData.devices, ...homeDataV3.devices.filter((d) => !homeData.devices.some((x) => x.duid === d.duid))];
390
+ homeData.receivedDevices = [...homeData.receivedDevices, ...homeDataV3.receivedDevices.filter((d) => !homeData.receivedDevices.some((x) => x.duid === d.duid))];
391
+ }
392
+
393
+ // Try to get rooms from v2 API if rooms are empty
394
+ if (homeData.rooms.length === 0) {
395
+ const homeDataV2 = await this.iotApi.getHomev2(homeDetails.rrHomeId);
396
+ if (homeDataV2 && homeDataV2.rooms && homeDataV2.rooms.length > 0) {
397
+ homeData.rooms = homeDataV2.rooms;
398
+ } else {
399
+ const homeDataV3 = await this.iotApi.getHomev3(homeDetails.rrHomeId);
400
+ if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
401
+ homeData.rooms = homeDataV3.rooms;
402
+ }
403
+ }
404
+ }
405
+
406
+ const devices: Device[] = [...homeData.devices, ...homeData.receivedDevices];
407
+
408
+ const result = devices.map((device) => {
409
+ return {
410
+ ...device,
411
+ rrHomeId: homeDetails.rrHomeId,
412
+ rooms: homeData.rooms,
413
+ localKey: device.localKey,
414
+ pv: device.pv,
415
+ serialNumber: device.sn,
416
+ scenes: scenes.filter((sc) => sc.param && (JSON.parse(sc.param) as SceneParam).action.items.some((x) => x.entityId == device.duid)),
417
+ data: {
418
+ id: device.duid,
419
+ firmwareVersion: device.fv,
420
+ serialNumber: device.sn,
421
+ model: homeData.products.find((p) => p.id === device.productId)?.model,
422
+ category: homeData.products.find((p) => p.id === device.productId)?.category,
423
+ batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100,
424
+ },
425
+
426
+ store: {
427
+ username: username,
428
+ userData: this.userdata as UserData,
429
+ localKey: device.localKey,
430
+ pv: device.pv,
431
+ model: products.get(device.productId),
432
+ },
433
+ };
434
+ }) as Device[];
435
+
436
+ return result;
437
+ }
438
+
439
+ public async getHomeDataForUpdating(homeid: number): Promise<Home> {
440
+ assert(this.iotApi !== undefined);
441
+ assert(this.userdata !== undefined);
442
+
443
+ const homeData = await this.iotApi.getHomev2(homeid);
444
+
445
+ if (!homeData) {
446
+ throw new Error('Failed to retrieve the home data');
447
+ }
448
+
449
+ const products = new Map<string, string>();
450
+ homeData.products.forEach((p) => products.set(p.id, p.model));
451
+ const devices: Device[] = homeData.devices.length > 0 ? homeData.devices : homeData.receivedDevices;
452
+
453
+ if (homeData.rooms.length === 0) {
454
+ const homeDataV3 = await this.iotApi.getHomev3(homeid);
455
+ if (homeDataV3 && homeDataV3.rooms && homeDataV3.rooms.length > 0) {
456
+ homeData.rooms = homeDataV3.rooms;
457
+ } else {
458
+ const homeDataV1 = await this.iotApi.getHome(homeid);
459
+ if (homeDataV1 && homeDataV1.rooms && homeDataV1.rooms.length > 0) {
460
+ homeData.rooms = homeDataV1.rooms;
461
+ }
462
+ }
463
+ }
464
+
465
+ const dvs = devices.map((device) => {
466
+ return {
467
+ ...device,
468
+ rrHomeId: homeid,
469
+ rooms: homeData.rooms,
470
+ serialNumber: device.sn,
471
+ data: {
472
+ id: device.duid,
473
+ firmwareVersion: device.fv,
474
+ serialNumber: device.sn,
475
+ model: homeData.products.find((p) => p.id === device.productId)?.model,
476
+ category: homeData.products.find((p) => p.id === device.productId)?.category,
477
+ batteryLevel: device.deviceStatus?.[Protocol.battery] ?? 100,
478
+ },
479
+
480
+ store: {
481
+ userData: this.userdata as UserData,
482
+ localKey: device.localKey,
483
+ pv: device.pv,
484
+ model: products.get(device.productId),
485
+ },
486
+ };
487
+ }) as Device[];
488
+
489
+ return {
490
+ ...homeData,
491
+ devices: dvs,
492
+ };
493
+ }
494
+
495
+ public async getScenes(homeId: number): Promise<Scene[] | undefined> {
496
+ assert(this.iotApi !== undefined);
497
+ return this.iotApi.getScenes(homeId);
498
+ }
499
+
500
+ public async startScene(sceneId: number): Promise<unknown> {
501
+ assert(this.iotApi !== undefined);
502
+ return this.iotApi.startScene(sceneId);
503
+ }
504
+
505
+ public async getRoomMappings(duid: string): Promise<number[][] | undefined> {
506
+ if (!this.messageClient) {
507
+ this.logger.warn('messageClient not initialized. Waititing for next execution');
508
+ return Promise.resolve(undefined);
509
+ }
510
+ return this.messageClient.get(duid, new RequestMessage({ method: 'get_room_mapping', secure: this.isRequestSecure(duid) }));
511
+
512
+ // return await this.getMessageProcessor(duid)?.getRooms(duid);
513
+ }
514
+
515
+ public async initializeMessageClient(username: string, device: Device, userdata: UserData): Promise<void> {
516
+ if (this.clientManager === undefined) {
517
+ this.logger.error('ClientManager not initialized');
518
+ return;
519
+ }
520
+
521
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
522
+ const self = this;
523
+ this.messageClient = this.clientManager.get(username, userdata);
524
+ this.messageClient.registerDevice(device.duid, device.localKey, device.pv, undefined);
525
+
526
+ this.messageClient.registerMessageListener({
527
+ onMessage: (message: ResponseMessage) => {
528
+ if (message instanceof ResponseMessage) {
529
+ const duid = message.duid;
530
+
531
+ // ignore battery updates here
532
+ if (message.contain(Protocol.battery)) return;
533
+
534
+ if (duid && self.deviceNotify) {
535
+ self.deviceNotify(NotifyMessageTypes.CloudMessage, message);
536
+ }
537
+ }
538
+
539
+ if (message instanceof ResponseMessage && message.contain(Protocol.hello_response)) {
540
+ const dps = message.dps[Protocol.hello_response] as DpsPayload;
541
+ const result = dps.result as Security;
542
+ self.messageClient?.updateNonce(message.duid, result.nonce);
543
+ }
544
+ },
545
+ } as AbstractMessageListener);
546
+
547
+ this.messageClient.connect();
548
+
549
+ while (!this.messageClient.isConnected()) {
550
+ await this.sleep(500);
551
+ }
552
+
553
+ this.logger.debug('MessageClient connected');
554
+ }
555
+
556
+ public async initializeMessageClientForLocal(device: Device): Promise<boolean> {
557
+ this.logger.debug('Begin get local ip');
558
+ if (this.messageClient === undefined) {
559
+ this.logger.error('messageClient not initialized');
560
+ return false;
561
+ }
562
+
563
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
564
+ const self = this;
565
+
566
+ const messageProcessor = new MessageProcessor(this.messageClient);
567
+ messageProcessor.injectLogger(this.logger);
568
+ messageProcessor.registerListener({
569
+ onError: (message: VacuumErrorCode) => {
570
+ if (self.deviceNotify) {
571
+ self.deviceNotify(NotifyMessageTypes.ErrorOccurred, { duid: device.duid, errorCode: message } as DeviceErrorMessage);
572
+ }
573
+ },
574
+ onBatteryUpdate: (percentage: number) => {
575
+ if (self.deviceNotify) {
576
+ self.deviceNotify(NotifyMessageTypes.BatteryUpdate, { duid: device.duid, percentage } as BatteryMessage);
577
+ }
578
+ },
579
+ onStatusChanged: () => {
580
+ // status: DeviceStatus
581
+ // if (self.deviceNotify) {
582
+ // const message: DeviceStatusNotify = { duid: device.duid, ...status.errorStatus, ...status.message } as DeviceStatusNotify;
583
+ // self.logger.debug('Device status update', debugStringify(message));
584
+ // self.deviceNotify(NotifyMessageTypes.LocalMessage, message);
585
+ // }
586
+ },
587
+ } as AbstractMessageHandler);
588
+
589
+ this.messageProcessorMap.set(device.duid, messageProcessor);
590
+
591
+ this.logger.debug('Checking if device supports local connection', device.pv, device.data.model, device.duid);
592
+ if (device.pv === 'B01') {
593
+ this.logger.warn('Device does not support local connection', device.duid);
594
+ this.mqttAlwaysOnDevices.set(device.duid, true);
595
+ return true;
596
+ } else {
597
+ this.mqttAlwaysOnDevices.set(device.duid, false);
598
+ }
599
+
600
+ this.logger.debug('Local device', device.duid);
601
+ let localIp = this.ipMap.get(device.duid);
602
+ try {
603
+ if (!localIp) {
604
+ this.logger.debug('Requesting network info for device', device.duid);
605
+ const networkInfo = await messageProcessor.getNetworkInfo(device.duid);
606
+ if (!networkInfo || !networkInfo.ip) {
607
+ this.logger.error('Failed to retrieve network info for device', device.duid, 'Network info:', networkInfo);
608
+ return false;
609
+ }
610
+
611
+ this.logger.debug('Network ip for device', device.duid, 'is', networkInfo.ip);
612
+
613
+ localIp = networkInfo.ip;
614
+ }
615
+
616
+ if (localIp) {
617
+ this.logger.debug('initializing the local connection for this client towards ' + localIp);
618
+ const localClient = this.messageClient.registerClient(device.duid, localIp) as LocalNetworkClient;
619
+ localClient.connect();
620
+
621
+ let count = 0;
622
+ while (!localClient.isConnected() && count < 20) {
623
+ this.logger.debug('Keep waiting for local client to connect');
624
+ count++;
625
+ await this.sleep(500);
626
+ }
627
+
628
+ if (!localClient.isConnected()) {
629
+ throw new Error('Local client did not connect after 10 attempts, something is wrong');
630
+ }
631
+
632
+ this.ipMap.set(device.duid, localIp);
633
+ this.localClientMap.set(device.duid, localClient);
634
+ this.logger.debug('LocalClient connected');
635
+ }
636
+ } catch (error) {
637
+ this.logger.error('Error requesting network info', error);
638
+ return false;
639
+ }
640
+
641
+ return true;
642
+ }
643
+
644
+ private sleep(ms: number): Promise<void> {
645
+ return new Promise((resolve) => setTimeout(resolve, ms));
646
+ }
647
+
648
+ private auth(userdata: UserData): UserData {
649
+ this.userdata = userdata;
650
+ this.iotApi = this.iotApiFactory(this.logger, userdata);
651
+ return userdata;
652
+ }
653
+
654
+ private isRequestSecure(duid: string): boolean {
655
+ return this.mqttAlwaysOnDevices.get(duid) ?? false;
656
+ }
657
+ }