homebridge-roborock-vacuum 0.1.0

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 (45) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -0
  4. package/config.schema.json +31 -0
  5. package/dist/index.js +10 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/logger.js +39 -0
  8. package/dist/logger.js.map +1 -0
  9. package/dist/platform.js +167 -0
  10. package/dist/platform.js.map +1 -0
  11. package/dist/settings.js +8 -0
  12. package/dist/settings.js.map +1 -0
  13. package/dist/types.js +3 -0
  14. package/dist/types.js.map +1 -0
  15. package/dist/vacuum_accessory.js +152 -0
  16. package/dist/vacuum_accessory.js.map +1 -0
  17. package/package.json +66 -0
  18. package/roborockLib/data/UserData +4 -0
  19. package/roborockLib/data/clientID +4 -0
  20. package/roborockLib/i18n/de/translations.json +188 -0
  21. package/roborockLib/i18n/en/translations.json +208 -0
  22. package/roborockLib/i18n/es/translations.json +188 -0
  23. package/roborockLib/i18n/fr/translations.json +188 -0
  24. package/roborockLib/i18n/it/translations.json +188 -0
  25. package/roborockLib/i18n/nl/translations.json +188 -0
  26. package/roborockLib/i18n/pl/translations.json +188 -0
  27. package/roborockLib/i18n/pt/translations.json +188 -0
  28. package/roborockLib/i18n/ru/translations.json +188 -0
  29. package/roborockLib/i18n/uk/translations.json +188 -0
  30. package/roborockLib/i18n/zh-cn/translations.json +188 -0
  31. package/roborockLib/lib/RRMapParser.js +447 -0
  32. package/roborockLib/lib/deviceFeatures.js +995 -0
  33. package/roborockLib/lib/localConnector.js +249 -0
  34. package/roborockLib/lib/map/map.html +110 -0
  35. package/roborockLib/lib/map/zones.js +713 -0
  36. package/roborockLib/lib/mapCreator.js +692 -0
  37. package/roborockLib/lib/message.js +223 -0
  38. package/roborockLib/lib/messageQueueHandler.js +87 -0
  39. package/roborockLib/lib/roborockPackageHelper.js +116 -0
  40. package/roborockLib/lib/roborock_mqtt_connector.js +349 -0
  41. package/roborockLib/lib/sniffing/mitmproxy_roborock.py +300 -0
  42. package/roborockLib/lib/vacuum.js +636 -0
  43. package/roborockLib/roborockAPI.js +1365 -0
  44. package/roborockLib/test.js +31 -0
  45. package/roborockLib/userdata.json +24 -0
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const Parser = require("binary-parser").Parser;
5
+ const net = require("net");
6
+ const dgram = require("dgram");
7
+
8
+ const server = dgram.createSocket("udp4");
9
+ const PORT = 58866;
10
+ const TIMEOUT = 5000; // 5 Sekunden Timeout
11
+
12
+ const BROADCAST_TOKEN = Buffer.from("qWKYcdQWrbm9hPqe", "utf8");
13
+
14
+ class EnhancedSocket extends net.Socket {
15
+ constructor(options) {
16
+ super(options);
17
+ this.connected = false;
18
+ this.chunkBuffer = Buffer.alloc(0);
19
+
20
+ this.on("connect", () => {
21
+ this.connected = true;
22
+ });
23
+
24
+ this.on("close", () => {
25
+ this.connected = false;
26
+ });
27
+
28
+ this.on("error", () => {
29
+ this.connected = false;
30
+ });
31
+
32
+ this.on("end", () => {
33
+ this.connected = false;
34
+ });
35
+ }
36
+ }
37
+
38
+ const localMessageParser = new Parser()
39
+ .endianess("big")
40
+ .string("version", {
41
+ length: 3,
42
+ })
43
+ .uint32("seq")
44
+ .uint16("protocol")
45
+ .uint16("payloadLen")
46
+ .buffer("payload", {
47
+ length: "payloadLen",
48
+ })
49
+ .uint32("crc32");
50
+
51
+ class localConnector {
52
+ constructor(adapter) {
53
+ this.adapter = adapter;
54
+
55
+ this.localClients = {};
56
+ }
57
+
58
+ async createClient(duid, ip) {
59
+ const client = new EnhancedSocket();
60
+
61
+ // Wrap the connect method in a promise to await its completion
62
+ await new Promise((resolve, reject) => {
63
+ client
64
+ .connect(58867, ip, () => {
65
+ this.adapter.log.debug(`tcp client for ${duid} connected`);
66
+ resolve();
67
+ })
68
+ .on("error", (error) => {
69
+ this.adapter.log.debug(`error on tcp client for ${duid}. ${error.message}`);
70
+ reject(error);
71
+ });
72
+ }).catch((error) => {
73
+ const online = this.adapter.onlineChecker(duid);
74
+ if (online) { // if the device is online, we can assume that the device is a remote device
75
+ this.adapter.log.info(`error on tcp client for ${duid}. Marking this device as remote device. Connecting via MQTT instead ${error.message}`);
76
+ this.adapter.remoteDevices.add(duid);
77
+ // this.adapter.catchError(`Failed to create tcp client: ${error.stack}`, `function createClient`, duid);
78
+ }
79
+ });
80
+
81
+ client.on("data", async (message) => {
82
+ try {
83
+
84
+ if (client.chunkBuffer.length == 0) {
85
+ this.adapter.log.debug(`new chunk started`);
86
+ client.chunkBuffer = message;
87
+ } else {
88
+ this.adapter.log.debug(`new chunk received`);
89
+ client.chunkBuffer = Buffer.concat([client.chunkBuffer, message]);
90
+ }
91
+ // this.adapter.log.debug(`new chunk received: ${message.toString("hex")}`);
92
+
93
+ let offset = 0;
94
+ if (this.checkComplete(client.chunkBuffer)) {
95
+ this.adapter.log.debug(`Chunk buffer data is complete. Processing...`);
96
+ // this.adapter.log.debug(`chunkBuffer: ${client.chunkBuffer.toString("hex")}`);
97
+ while (offset + 4 <= client.chunkBuffer.length) {
98
+ const segmentLength = client.chunkBuffer.readUInt32BE(offset);
99
+ // length of 17 does not contain any useful data.
100
+ // The parser for this looks like this: const shortMessageParser = new Parser().endianess("big").string("version", {length: 3,}).uint32("seq").uint32("random").uint32("timestamp").uint16("protocol")
101
+ if (segmentLength != 17) {
102
+ const currentBuffer = client.chunkBuffer.subarray(offset + 4, offset + segmentLength + 4);
103
+ const data = this.adapter.message._decodeMsg(currentBuffer, duid);
104
+
105
+ if (data.protocol == 4) {
106
+ const dps = JSON.parse(data.payload).dps;
107
+
108
+ if (dps) {
109
+ const _102 = JSON.stringify(dps["102"]);
110
+ const parsed_102 = JSON.parse(JSON.parse(_102));
111
+ const id = parsed_102.id;
112
+ const result = parsed_102.result;
113
+
114
+ if (this.adapter.pendingRequests.has(id)) {
115
+ this.adapter.log.debug(`Local message with protocol 4 and id ${id} received. Result: ${JSON.stringify(result)}`);
116
+ const { resolve, timeout } = this.adapter.pendingRequests.get(id);
117
+ this.adapter.clearTimeout(timeout);
118
+ this.adapter.pendingRequests.delete(id);
119
+ resolve(result);
120
+
121
+ if(this.adapter.deviceNotify !== undefined){
122
+ this.adapter.deviceNotify("LocalMessage", result);
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ offset += 4 + segmentLength;
129
+ }
130
+ this.clearChunkBuffer(duid);
131
+ }
132
+ } catch (error) {
133
+ this.adapter.catchError(`Failed to create tcp client: ${error.stack}`, `function createClient`, duid);
134
+ }
135
+ });
136
+
137
+ client.on("close", () => {
138
+ this.adapter.log.debug(`tcp client for ${duid} disconnected, attempting to reconnect...`);
139
+ setTimeout(async () => {
140
+ await this.createClient(duid, ip);
141
+ }, 60000);
142
+ client.connected = false;
143
+ });
144
+
145
+ client.on("error", (error) => {
146
+ this.adapter.log.debug(`error on tcp client for ${duid}. ${error.message}`);
147
+ });
148
+
149
+ this.localClients[duid] = client;
150
+ }
151
+
152
+ checkComplete(buffer) {
153
+ let totalLength = 0;
154
+ let offset = 0;
155
+
156
+ while (offset + 4 <= buffer.length) {
157
+ const segmentLength = buffer.readUInt32BE(offset);
158
+ totalLength += 4 + segmentLength;
159
+ offset += 4 + segmentLength;
160
+
161
+ if (offset > buffer.length) {
162
+ return false; // Data is not complete yet
163
+ }
164
+ }
165
+
166
+ return totalLength <= buffer.length;
167
+ }
168
+
169
+ clearChunkBuffer(duid) {
170
+ if (this.localClients[duid]) {
171
+ this.localClients[duid].chunkBuffer = Buffer.alloc(0);
172
+ }
173
+ }
174
+
175
+ sendMessage(duid, message) {
176
+ const client = this.localClients[duid];
177
+ if (client) {
178
+ client.write(message);
179
+ }
180
+ }
181
+
182
+ isConnected(duid) {
183
+ if (this.localClients[duid]) {
184
+ return this.localClients[duid].connected;
185
+ }
186
+ }
187
+
188
+ async getLocalDevices() {
189
+ return new Promise((resolve, reject) => {
190
+ const devices = {};
191
+
192
+ server.on("message", (msg) => {
193
+ const parsedMessage = localMessageParser.parse(msg);
194
+ const decodedMessage = this.decryptECB(parsedMessage.payload, BROADCAST_TOKEN); // this might be decryptCBC for A01. Haven't checked this yet
195
+ const parsedDecodedMessage = JSON.parse(decodedMessage);
196
+ this.adapter.log.debug(`getLocalDevices parsedDecodedMessage: ${JSON.stringify(parsedDecodedMessage)}`);
197
+
198
+ if (parsedDecodedMessage) {
199
+ const localKey = this.adapter.localKeys.get(parsedDecodedMessage.duid);
200
+ this.adapter.log.debug(`getLocalDevices localKey: ${localKey}`);
201
+
202
+ if (localKey) {
203
+ // if there's no localKey, decryption cannot work. For example when the found robot is not associated with a roborock account
204
+ if (!devices[parsedDecodedMessage.duid]) {
205
+ devices[parsedDecodedMessage.duid] = parsedDecodedMessage.ip;
206
+ }
207
+ }
208
+ }
209
+ });
210
+
211
+ server.on("error", (error) => {
212
+ this.adapter.catchError(`Discover server error: ${error.stack}`);
213
+ server.close();
214
+ reject(error);
215
+ });
216
+
217
+ server.bind(PORT);
218
+
219
+ this.localDevicesTimeout = this.adapter.setTimeout(() => {
220
+ server.close();
221
+
222
+ resolve(devices);
223
+ }, TIMEOUT);
224
+ });
225
+ }
226
+
227
+ decryptECB(encrypted, aesKey) {
228
+ const decipher = crypto.createDecipheriv("aes-128-ecb", aesKey, null);
229
+ decipher.setAutoPadding(false);
230
+ let decrypted = decipher.update(encrypted, "binary", "utf8");
231
+ decrypted += decipher.final("utf8");
232
+ return this.removePadding(decrypted);
233
+ }
234
+
235
+ removePadding(str) {
236
+ const paddingLength = str.charCodeAt(str.length - 1);
237
+ return str.slice(0, -paddingLength);
238
+ }
239
+
240
+ clearLocalDevicedTimeout() {
241
+ if (this.localDevicesTimeout) {
242
+ this.adapter.clearTimeout(this.localDevicesTimeout);
243
+ }
244
+ }
245
+ }
246
+
247
+ module.exports = {
248
+ localConnector,
249
+ };
@@ -0,0 +1,110 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <script src="zones.js"></script>
5
+ <style>
6
+ body {
7
+ display: flex;
8
+ align-items: center;
9
+ background-color: #000000;
10
+ }
11
+
12
+ #myCanvas {
13
+ border: 2px solid #4F4F4F;
14
+ box-shadow: 0px 0px 10px #333;
15
+ border-radius: 15px;
16
+ /* image-rendering: pixelated; */
17
+ }
18
+
19
+ #addButton, #deleteButton {
20
+ display: inline-block;
21
+ width: 100px;
22
+ }
23
+ #addButton {
24
+ margin-right: 0px;
25
+ }
26
+ #deleteButton {
27
+ margin-left: 0px;
28
+ }
29
+
30
+ #startButton, #stopButton, #pauseButton {
31
+ display: inline-block;
32
+ width: 100px;
33
+ }
34
+ #startButton, #pauseButton {
35
+ margin-right: 0px;
36
+ }
37
+ #stopButton {
38
+ margin-left: 0px;
39
+ }
40
+
41
+ button, select {
42
+ margin: 10px 20px;
43
+ padding: 10px 20px;
44
+ border-radius: 10px;
45
+ background-color: #2D9CDB;
46
+ color: #F0F0F0;
47
+ cursor: pointer;
48
+ transition: background-color 0.2s ease-in-out;
49
+ font-size: 16px;
50
+ font-weight: bold;
51
+ border: 1px solid #4F4F4F;
52
+ width: 200px;
53
+ }
54
+
55
+ button:hover, select:hover {
56
+ background-color: #47A9DC;
57
+ }
58
+
59
+ button:disabled {
60
+ color: gray;
61
+ background-color: lightgray;
62
+ cursor: not-allowed;
63
+ }
64
+
65
+ .hovered-option {
66
+ background-color: #47A9DC !important;
67
+ }
68
+ </style>
69
+ </head>
70
+ <body>
71
+ <canvas id="myCanvas" width="450" height="450"></canvas>
72
+
73
+ <div id="popup" style="display: none; position: absolute">
74
+ <div id="triangle" style="position: absolute; width: 0; height: 0; border-style: solid; border-width: 10px 5px 0 5px; border-color: #ffffff transparent transparent transparent; transform: translateY(1px);"></div>
75
+
76
+ <img id="popup-image" src="" style="width: 100px; height: 100px; border: 1px solid white;" />
77
+ </div>
78
+
79
+ <div id="largePhoto" style="top: 25px; left: 25px; display: none; position: absolute">
80
+ <img id="largePhoto-image" src="" style="width: 415px; height: 415px; border: 1px solid white;" />
81
+ </div>
82
+
83
+ <div style="display: flex; flex-direction: column; align-items: center;">
84
+ <div style="margin-left: auto;">
85
+ <select id="robotSelect"></select>
86
+ </div>
87
+ <div style="margin-left: auto;">
88
+ <select id="cleanCount">
89
+ <option value="1">1 Time</option>
90
+ <option value="2">2 Times</option>
91
+ <option value="3">3 Times</option>
92
+ </select>
93
+ </div>
94
+
95
+ <div style="display: flex;" id="zoneButtons">
96
+ <button id="addButton">+ Zone</button>
97
+ <button disabled id="deleteButton">- Zone</button>
98
+ </div>
99
+ <div style="display: flex;" id="robotButtons">
100
+ <button id="pauseButton" style="display: none;">Pause</button>
101
+ <button id="startButton">Start</button>
102
+ <button id="stopButton">Stop</button>
103
+ </div>
104
+ <button id="dockButton">Dock</button>
105
+ <button id="goToButton">Go to</button>
106
+ <button id="resetZoomButton">Reset zoom</button>
107
+ </div>
108
+
109
+ </body>
110
+ </html>