homebridge-roborock-vacuum 1.7.0 → 1.7.2
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.
- package/dist/ui/index.js +32 -1
- package/dist/ui/index.js.map +1 -1
- package/homebridge-ui/public/index.html +63 -11
- package/homebridge-ui/public/index.js +135 -0
- package/homebridge-ui/public/styles.css +22 -0
- package/package.json +1 -1
- package/roborockLib/lib/localConnector.js +27 -10
- package/roborockLib/lib/message.js +21 -10
- package/roborockLib/lib/roborockHome.js +96 -0
package/dist/ui/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const crypto_2 = require("../crypto");
|
|
10
10
|
const roborockAuth = require("../../roborockLib/lib/roborockAuth");
|
|
11
|
+
const roborockHome = require("../../roborockLib/lib/roborockHome");
|
|
11
12
|
class RoborockUiServer {
|
|
12
13
|
constructor(HomebridgePluginUiServer) {
|
|
13
14
|
this.homebridgePluginUiServer = new HomebridgePluginUiServer();
|
|
@@ -17,6 +18,7 @@ class RoborockUiServer {
|
|
|
17
18
|
this.homebridgePluginUiServer.onRequest("/auth/verify-2fa-code", this.verifyTwoFactorCode.bind(this));
|
|
18
19
|
this.homebridgePluginUiServer.onRequest("/auth/login", this.loginWithPassword.bind(this));
|
|
19
20
|
this.homebridgePluginUiServer.onRequest("/auth/logout", this.logout.bind(this));
|
|
21
|
+
this.homebridgePluginUiServer.onRequest("/devices/list", this.listDevices.bind(this));
|
|
20
22
|
this.homebridgePluginUiServer.ready();
|
|
21
23
|
}
|
|
22
24
|
getStoragePath() {
|
|
@@ -181,6 +183,35 @@ class RoborockUiServer {
|
|
|
181
183
|
}
|
|
182
184
|
return { ok: true, message: "Logged out. Token cleared." };
|
|
183
185
|
}
|
|
186
|
+
async listDevices(payload) {
|
|
187
|
+
if (!payload.encryptedToken) {
|
|
188
|
+
return { ok: false, message: "Log in first to load your devices." };
|
|
189
|
+
}
|
|
190
|
+
const userData = (0, crypto_2.decryptSession)(payload.encryptedToken, this.getStoragePath());
|
|
191
|
+
if (!userData) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
message: "Saved login could not be read. Please log in again.",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const clientID = await this.getClientId();
|
|
199
|
+
const devices = await roborockHome.fetchDevices({
|
|
200
|
+
baseURL: payload.baseURL || "usiot.roborock.com",
|
|
201
|
+
username: payload.email,
|
|
202
|
+
clientID,
|
|
203
|
+
userData,
|
|
204
|
+
});
|
|
205
|
+
return { ok: true, devices };
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
console.error("Device list request failed:", (error === null || error === void 0 ? void 0 : error.message) || error);
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
message: (error === null || error === void 0 ? void 0 : error.message) || "Failed to load devices.",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
184
215
|
buildNonce() {
|
|
185
216
|
return crypto_1.default
|
|
186
217
|
.randomBytes(12)
|
|
@@ -191,7 +222,7 @@ class RoborockUiServer {
|
|
|
191
222
|
}
|
|
192
223
|
}
|
|
193
224
|
// IMPORTANT: Use Function constructor to create a dynamic import that TypeScript won't transform
|
|
194
|
-
//
|
|
225
|
+
//
|
|
195
226
|
// Background: @homebridge/plugin-ui-utils v2+ is a pure ES module that cannot be loaded with require()
|
|
196
227
|
// in Node.js 18+. Normally we would use `await import('@homebridge/plugin-ui-utils')`, but because
|
|
197
228
|
// this project uses TypeScript with "module": "commonjs" in tsconfig.json, TypeScript transforms
|
package/dist/ui/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":";;;;;AAAA,oDAA4B;AAC5B,gDAAwB;AACxB,4CAAoB;AACpB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -39,21 +39,33 @@
|
|
|
39
39
|
<div class="field">
|
|
40
40
|
<div class="field-header">
|
|
41
41
|
<span>Skipped Device IDs</span>
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
<div class="field-actions">
|
|
43
|
+
<button
|
|
44
|
+
id="load-devices"
|
|
45
|
+
class="secondary btn-small"
|
|
46
|
+
type="button"
|
|
47
|
+
title="Load devices from your account"
|
|
48
|
+
>
|
|
49
|
+
Load devices
|
|
50
|
+
</button>
|
|
51
|
+
<button
|
|
52
|
+
id="add-skip-device"
|
|
53
|
+
class="icon-button"
|
|
54
|
+
type="button"
|
|
55
|
+
title="Add skipped device"
|
|
56
|
+
aria-label="Add skipped device"
|
|
57
|
+
>
|
|
58
|
+
+
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
51
61
|
</div>
|
|
62
|
+
<div id="discovered-devices" class="discovered-devices"></div>
|
|
52
63
|
<div id="skip-devices" class="skip-device-list"></div>
|
|
53
64
|
</div>
|
|
54
65
|
<p class="help">
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
Click <strong>Load devices</strong> to pick which vacuums to hide, or
|
|
67
|
+
enter device IDs manually. Matching devices will not be added to HomeKit
|
|
68
|
+
and existing cached accessories will be removed on restart.
|
|
57
69
|
</p>
|
|
58
70
|
<label class="checkbox">
|
|
59
71
|
<input id="debug-mode" type="checkbox" />
|
|
@@ -65,6 +77,46 @@
|
|
|
65
77
|
</div>
|
|
66
78
|
</section>
|
|
67
79
|
|
|
80
|
+
<section class="card">
|
|
81
|
+
<h2>Current Room → MQTT</h2>
|
|
82
|
+
<p class="help">
|
|
83
|
+
Publish the room each vacuum is currently cleaning to a local MQTT broker
|
|
84
|
+
(telemetry only; not exposed to HomeKit). Off by default.
|
|
85
|
+
</p>
|
|
86
|
+
<label class="checkbox">
|
|
87
|
+
<input id="room-mqtt-enabled" type="checkbox" />
|
|
88
|
+
Publish current cleaning room to MQTT
|
|
89
|
+
</label>
|
|
90
|
+
<label>
|
|
91
|
+
Broker URL
|
|
92
|
+
<input
|
|
93
|
+
id="room-mqtt-broker"
|
|
94
|
+
type="text"
|
|
95
|
+
placeholder="mqtt://127.0.0.1:1883"
|
|
96
|
+
/>
|
|
97
|
+
</label>
|
|
98
|
+
<label>
|
|
99
|
+
Topic Template
|
|
100
|
+
<input
|
|
101
|
+
id="room-mqtt-topic"
|
|
102
|
+
type="text"
|
|
103
|
+
placeholder="homebridge/roborock/{duid}/current_room"
|
|
104
|
+
/>
|
|
105
|
+
</label>
|
|
106
|
+
<p class="help">
|
|
107
|
+
Supports <code>{duid}</code> and <code>{name}</code> tokens; if neither is
|
|
108
|
+
present, <code>/{duid}</code> is appended automatically so multiple
|
|
109
|
+
vacuums never collide.
|
|
110
|
+
</p>
|
|
111
|
+
<label>
|
|
112
|
+
Cleaning Poll Interval (s)
|
|
113
|
+
<input id="room-mqtt-poll" type="number" min="5" placeholder="10" />
|
|
114
|
+
</label>
|
|
115
|
+
<p class="help">
|
|
116
|
+
How often status is polled while the vacuum is actively cleaning.
|
|
117
|
+
</p>
|
|
118
|
+
</section>
|
|
119
|
+
|
|
68
120
|
<section class="card" id="two-factor-section">
|
|
69
121
|
<h2>Two-Factor Authentication</h2>
|
|
70
122
|
<p class="help">
|
|
@@ -5,7 +5,13 @@ const elements = {
|
|
|
5
5
|
baseUrl: document.getElementById("base-url"),
|
|
6
6
|
skipDevices: document.getElementById("skip-devices"),
|
|
7
7
|
addSkipDevice: document.getElementById("add-skip-device"),
|
|
8
|
+
loadDevices: document.getElementById("load-devices"),
|
|
9
|
+
discoveredDevices: document.getElementById("discovered-devices"),
|
|
8
10
|
debugMode: document.getElementById("debug-mode"),
|
|
11
|
+
roomMqttEnabled: document.getElementById("room-mqtt-enabled"),
|
|
12
|
+
roomMqttBroker: document.getElementById("room-mqtt-broker"),
|
|
13
|
+
roomMqttTopic: document.getElementById("room-mqtt-topic"),
|
|
14
|
+
roomMqttPoll: document.getElementById("room-mqtt-poll"),
|
|
9
15
|
code: document.getElementById("two-factor-code"),
|
|
10
16
|
login: document.getElementById("login"),
|
|
11
17
|
logout: document.getElementById("logout"),
|
|
@@ -69,6 +75,13 @@ async function loadConfig() {
|
|
|
69
75
|
renderSkipDevices(config.skipDevices);
|
|
70
76
|
elements.debugMode.checked = Boolean(config.debugMode);
|
|
71
77
|
|
|
78
|
+
const roomMqtt = config.currentRoomMqtt || {};
|
|
79
|
+
elements.roomMqttEnabled.checked = Boolean(roomMqtt.enabled);
|
|
80
|
+
elements.roomMqttBroker.value = roomMqtt.brokerUrl || "";
|
|
81
|
+
elements.roomMqttTopic.value = roomMqtt.topic || "";
|
|
82
|
+
elements.roomMqttPoll.value =
|
|
83
|
+
roomMqtt.cleaningPollSeconds != null ? roomMqtt.cleaningPollSeconds : "";
|
|
84
|
+
|
|
72
85
|
setLoggedInState(Boolean(config.encryptedToken));
|
|
73
86
|
}
|
|
74
87
|
|
|
@@ -99,6 +112,32 @@ function getCode() {
|
|
|
99
112
|
return elements.code.value.trim();
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
function getRoomMqtt() {
|
|
116
|
+
const config = { enabled: Boolean(elements.roomMqttEnabled.checked) };
|
|
117
|
+
|
|
118
|
+
const brokerUrl = elements.roomMqttBroker.value.trim();
|
|
119
|
+
if (brokerUrl) {
|
|
120
|
+
config.brokerUrl = brokerUrl;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const topic = elements.roomMqttTopic.value.trim();
|
|
124
|
+
if (topic) {
|
|
125
|
+
config.topic = topic;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const pollRaw = elements.roomMqttPoll.value.trim();
|
|
129
|
+
const cleaningPollSeconds = Number(pollRaw);
|
|
130
|
+
if (pollRaw && Number.isFinite(cleaningPollSeconds)) {
|
|
131
|
+
config.cleaningPollSeconds = cleaningPollSeconds;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return config;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function saveRoomMqtt() {
|
|
138
|
+
await updatePluginConfig({ currentRoomMqtt: getRoomMqtt() });
|
|
139
|
+
}
|
|
140
|
+
|
|
102
141
|
function parseDeviceIds(value) {
|
|
103
142
|
if (!value) {
|
|
104
143
|
return [];
|
|
@@ -156,6 +195,93 @@ function addSkipDeviceRow(value = "", shouldFocus = true) {
|
|
|
156
195
|
}
|
|
157
196
|
}
|
|
158
197
|
|
|
198
|
+
function getSkipDeviceSet() {
|
|
199
|
+
return new Set(
|
|
200
|
+
getSkipDeviceInputs()
|
|
201
|
+
.map((input) => input.value.trim())
|
|
202
|
+
.filter((entry) => entry)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function toggleSkipDevice(deviceId, shouldSkip) {
|
|
207
|
+
const skipped = getSkipDeviceSet();
|
|
208
|
+
if (shouldSkip) {
|
|
209
|
+
skipped.add(deviceId);
|
|
210
|
+
} else {
|
|
211
|
+
skipped.delete(deviceId);
|
|
212
|
+
}
|
|
213
|
+
renderSkipDevices([...skipped]);
|
|
214
|
+
saveCredentials();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function renderDiscoveredDevices(devices) {
|
|
218
|
+
elements.discoveredDevices.textContent = "";
|
|
219
|
+
|
|
220
|
+
if (!Array.isArray(devices) || devices.length === 0) {
|
|
221
|
+
const empty = document.createElement("p");
|
|
222
|
+
empty.className = "help";
|
|
223
|
+
empty.textContent = "No devices found on this account.";
|
|
224
|
+
elements.discoveredDevices.appendChild(empty);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const skipped = getSkipDeviceSet();
|
|
229
|
+
devices.forEach((device) => {
|
|
230
|
+
const row = document.createElement("label");
|
|
231
|
+
row.className = "checkbox";
|
|
232
|
+
|
|
233
|
+
const input = document.createElement("input");
|
|
234
|
+
input.type = "checkbox";
|
|
235
|
+
input.checked = skipped.has(device.duid);
|
|
236
|
+
input.addEventListener("change", () =>
|
|
237
|
+
toggleSkipDevice(device.duid, input.checked)
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const label = document.createElement("span");
|
|
241
|
+
const model = device.model ? ` (${device.model})` : "";
|
|
242
|
+
const shared = device.shared ? " — shared" : "";
|
|
243
|
+
label.textContent = `${device.name || device.duid}${model} — ${device.duid}${shared}`;
|
|
244
|
+
|
|
245
|
+
row.append(input, label);
|
|
246
|
+
elements.discoveredDevices.appendChild(row);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function loadDevices() {
|
|
251
|
+
if (
|
|
252
|
+
!window.homebridge ||
|
|
253
|
+
typeof window.homebridge.getPluginConfig !== "function"
|
|
254
|
+
) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const configs = await window.homebridge.getPluginConfig();
|
|
259
|
+
const config = configs.find(
|
|
260
|
+
(entry) => entry.platform === "RoborockVacuumPlatform"
|
|
261
|
+
);
|
|
262
|
+
const encryptedToken = config && config.encryptedToken;
|
|
263
|
+
if (!encryptedToken) {
|
|
264
|
+
showToast("warning", "Log in first to load your devices.");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
elements.loadDevices.disabled = true;
|
|
269
|
+
try {
|
|
270
|
+
const result = await request("/devices/list", {
|
|
271
|
+
email: getEmail(),
|
|
272
|
+
baseURL: getBaseUrl(),
|
|
273
|
+
encryptedToken,
|
|
274
|
+
});
|
|
275
|
+
if (result.ok) {
|
|
276
|
+
renderDiscoveredDevices(result.devices);
|
|
277
|
+
} else {
|
|
278
|
+
showToast("error", result.message || "Failed to load devices.");
|
|
279
|
+
}
|
|
280
|
+
} finally {
|
|
281
|
+
elements.loadDevices.disabled = false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
159
285
|
async function saveCredentials() {
|
|
160
286
|
const email = getEmail();
|
|
161
287
|
const baseURL = getBaseUrl();
|
|
@@ -325,8 +451,17 @@ function init() {
|
|
|
325
451
|
elements.addSkipDevice.addEventListener("click", () => {
|
|
326
452
|
addSkipDeviceRow();
|
|
327
453
|
});
|
|
454
|
+
elements.loadDevices.addEventListener("click", () => {
|
|
455
|
+
loadDevices().catch(() => {
|
|
456
|
+
showToast("error", "Failed to load devices.");
|
|
457
|
+
});
|
|
458
|
+
});
|
|
328
459
|
elements.debugMode.addEventListener("change", saveCredentials);
|
|
329
460
|
elements.email.addEventListener("change", saveCredentials);
|
|
461
|
+
elements.roomMqttEnabled.addEventListener("change", saveRoomMqtt);
|
|
462
|
+
elements.roomMqttBroker.addEventListener("change", saveRoomMqtt);
|
|
463
|
+
elements.roomMqttTopic.addEventListener("change", saveRoomMqtt);
|
|
464
|
+
elements.roomMqttPoll.addEventListener("change", saveRoomMqtt);
|
|
330
465
|
}
|
|
331
466
|
|
|
332
467
|
if (window.homebridge) {
|
|
@@ -96,6 +96,28 @@ textarea {
|
|
|
96
96
|
resize: vertical;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
.field-actions {
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: 8px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.btn-small {
|
|
106
|
+
padding: 8px 14px;
|
|
107
|
+
font-size: 0.85rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.discovered-devices {
|
|
111
|
+
display: grid;
|
|
112
|
+
gap: 8px;
|
|
113
|
+
margin-top: 12px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.discovered-devices .checkbox {
|
|
117
|
+
margin-top: 0;
|
|
118
|
+
word-break: break-all;
|
|
119
|
+
}
|
|
120
|
+
|
|
99
121
|
.skip-device-list {
|
|
100
122
|
display: grid;
|
|
101
123
|
gap: 10px;
|
package/package.json
CHANGED
|
@@ -111,9 +111,12 @@ class localConnector {
|
|
|
111
111
|
while (offset + 4 <= client.chunkBuffer.length) {
|
|
112
112
|
const segmentLength = client.chunkBuffer.readUInt32BE(offset);
|
|
113
113
|
const currentBuffer = client.chunkBuffer.subarray(offset + 4, offset + segmentLength + 4);
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
114
|
+
// Short, header-only handshake frames carry no encrypted payload:
|
|
115
|
+
// the L01 HELLO_RESPONSE is 21 bytes (version+seq+random+timestamp+
|
|
116
|
+
// protocol+CRC32); a bare 17-byte header may also appear. Route both
|
|
117
|
+
// to the shortMessage handler below; anything larger is a data
|
|
118
|
+
// message with an encrypted payload.
|
|
119
|
+
if (segmentLength != 17 && segmentLength != 21) {
|
|
117
120
|
const data = this.adapter.message._decodeMsg(currentBuffer, duid);
|
|
118
121
|
if (!data) {
|
|
119
122
|
offset += 4 + segmentLength;
|
|
@@ -146,7 +149,9 @@ class localConnector {
|
|
|
146
149
|
else {
|
|
147
150
|
try {
|
|
148
151
|
const shortMessage = shortMessageParser.parse(currentBuffer);
|
|
149
|
-
|
|
152
|
+
// HELLO_RESPONSE (protocol 1) echoing our seq=1 HELLO; its
|
|
153
|
+
// random field is the ack_nonce.
|
|
154
|
+
if (shortMessage.version == "L01" && shortMessage.protocol == 1 && shortMessage.seq == 1) {
|
|
150
155
|
const currentNonces = this.adapter.localL01Nonces.get(duid) || {};
|
|
151
156
|
this.adapter.localL01Nonces.set(duid, {
|
|
152
157
|
connectNonce: currentNonces.connectNonce,
|
|
@@ -157,6 +162,7 @@ class localConnector {
|
|
|
157
162
|
if (waiter) {
|
|
158
163
|
this.adapter.clearTimeout(waiter.timeout);
|
|
159
164
|
this.l01HandshakeWaiters.delete(duid);
|
|
165
|
+
this.adapter.log.debug(`L01 handshake complete for ${duid}: ackNonce=${shortMessage.random}`);
|
|
160
166
|
waiter.resolve(true);
|
|
161
167
|
}
|
|
162
168
|
}
|
|
@@ -247,18 +253,29 @@ class localConnector {
|
|
|
247
253
|
return;
|
|
248
254
|
}
|
|
249
255
|
|
|
256
|
+
// HELLO request: protocol 0 (HELLO_REQUEST), 21-byte header+CRC frame, seq=1, a
|
|
257
|
+
// real timestamp, and random=connect_nonce. The robot replies with a protocol-1
|
|
258
|
+
// (HELLO_RESPONSE) frame whose random field is the ack_nonce. Pick a stable
|
|
259
|
+
// connect_nonce in [10000, 32767] and store it BEFORE sending, so the follow-up
|
|
260
|
+
// L01 data-message AAD reuses the exact value the robot saw in the HELLO.
|
|
250
261
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
251
|
-
const
|
|
252
|
-
if (!handshakeMessage) {
|
|
253
|
-
throw new Error(`Failed to build protocol 1 handshake message for ${duid}`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const connectNonce = handshakeMessage.readUInt32BE(7);
|
|
262
|
+
const connectNonce = 10000 + Math.floor(Math.random() * 22768);
|
|
257
263
|
this.adapter.localL01Nonces.set(duid, {
|
|
258
264
|
connectNonce,
|
|
259
265
|
ackNonce: undefined,
|
|
260
266
|
});
|
|
261
267
|
|
|
268
|
+
const handshakeMessage = await this.adapter.message.buildRoborockMessage(
|
|
269
|
+
duid,
|
|
270
|
+
0,
|
|
271
|
+
timestamp,
|
|
272
|
+
Buffer.alloc(0),
|
|
273
|
+
{ seq: 1, random: connectNonce }
|
|
274
|
+
);
|
|
275
|
+
if (!handshakeMessage) {
|
|
276
|
+
throw new Error(`Failed to build HELLO handshake message for ${duid}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
262
279
|
if (this.l01HandshakeWaiters.has(duid)) {
|
|
263
280
|
const waiter = this.l01HandshakeWaiters.get(duid);
|
|
264
281
|
this.adapter.clearTimeout(waiter.timeout);
|
|
@@ -123,7 +123,7 @@ class message {
|
|
|
123
123
|
return payload;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
async buildRoborockMessage(duid, protocol, timestamp, payload) {
|
|
126
|
+
async buildRoborockMessage(duid, protocol, timestamp, payload, options = {}) {
|
|
127
127
|
const version = await this.adapter.getRobotVersion(duid);
|
|
128
128
|
|
|
129
129
|
let encrypted;
|
|
@@ -131,18 +131,29 @@ class message {
|
|
|
131
131
|
const currentSeq = seq & 0xffffffff;
|
|
132
132
|
const currentRandom = random & 0xffffffff;
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
// Header-only frames (the L01 HELLO request, protocol 0) carry no encrypted
|
|
135
|
+
// payload, so they have NO payloadLen field — just version(3) + seq(4) + random(4)
|
|
136
|
+
// + timestamp(4) + protocol(2) + CRC32(4) = 21 bytes. (The previous build emitted a
|
|
137
|
+
// 23-byte frame with a spurious payloadLen, which L01 robots silently drop, so the
|
|
138
|
+
// handshake never completed.) Optional explicit seq/random let the HELLO pin seq=1
|
|
139
|
+
// and a stable connect_nonce without disturbing the module-global counters used by
|
|
140
|
+
// data messages.
|
|
141
|
+
if (protocol == 0 || protocol == 1) {
|
|
142
|
+
const headerSeq = (options.seq !== undefined ? options.seq : currentSeq) >>> 0;
|
|
143
|
+
const headerRandom =
|
|
144
|
+
(options.random !== undefined ? options.random : currentRandom) >>> 0;
|
|
145
|
+
const msg = Buffer.alloc(21);
|
|
136
146
|
msg.write(version);
|
|
137
|
-
msg.writeUint32BE(
|
|
138
|
-
msg.writeUint32BE(
|
|
147
|
+
msg.writeUint32BE(headerSeq, 3);
|
|
148
|
+
msg.writeUint32BE(headerRandom, 7);
|
|
139
149
|
msg.writeUint32BE(timestamp, 11);
|
|
140
150
|
msg.writeUint16BE(protocol, 15);
|
|
141
|
-
msg.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
151
|
+
const crc32 = CRC32.buf(msg.subarray(0, 17)) >>> 0;
|
|
152
|
+
msg.writeUint32BE(crc32, 17);
|
|
153
|
+
if (options.seq === undefined) {
|
|
154
|
+
seq++;
|
|
155
|
+
random++;
|
|
156
|
+
}
|
|
146
157
|
|
|
147
158
|
return msg;
|
|
148
159
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const axios = require("axios");
|
|
5
|
+
const roborockAuth = require("./roborockAuth");
|
|
6
|
+
|
|
7
|
+
function md5hex(str) {
|
|
8
|
+
return crypto.createHash("md5").update(str).digest("hex");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Build an axios instance that signs each request with the rriot Hawk
|
|
12
|
+
// credentials, mirroring the signing the main plugin uses for the Roborock
|
|
13
|
+
// "real" API (see roborockAPI.js initUser).
|
|
14
|
+
function createSignedApi(rriot) {
|
|
15
|
+
const api = axios.create({ baseURL: rriot.r.a });
|
|
16
|
+
api.interceptors.request.use((config) => {
|
|
17
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
18
|
+
const nonce = crypto
|
|
19
|
+
.randomBytes(6)
|
|
20
|
+
.toString("base64")
|
|
21
|
+
.substring(0, 6)
|
|
22
|
+
.replace("+", "X")
|
|
23
|
+
.replace("/", "Y");
|
|
24
|
+
const url = new URL(api.getUri(config));
|
|
25
|
+
const prestr = [
|
|
26
|
+
rriot.u,
|
|
27
|
+
rriot.s,
|
|
28
|
+
nonce,
|
|
29
|
+
timestamp,
|
|
30
|
+
md5hex(url.pathname),
|
|
31
|
+
/*queryparams*/ "",
|
|
32
|
+
/*body*/ "",
|
|
33
|
+
].join(":");
|
|
34
|
+
const mac = crypto
|
|
35
|
+
.createHmac("sha256", rriot.h)
|
|
36
|
+
.update(prestr)
|
|
37
|
+
.digest("base64");
|
|
38
|
+
config.headers["Authorization"] =
|
|
39
|
+
`Hawk id="${rriot.u}", s="${rriot.s}", ts="${timestamp}", nonce="${nonce}", mac="${mac}"`;
|
|
40
|
+
return config;
|
|
41
|
+
});
|
|
42
|
+
return api;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Read-only: fetch the account's devices (owned + shared) so the config UI can
|
|
46
|
+
// let the user pick which to skip instead of typing raw device IDs. The caller
|
|
47
|
+
// is responsible for providing a valid, already-decrypted userData; network or
|
|
48
|
+
// auth failures reject so the UI can surface a message.
|
|
49
|
+
async function fetchDevices({ baseURL, username, clientID, userData }) {
|
|
50
|
+
if (!userData || !userData.token || !userData.rriot) {
|
|
51
|
+
throw new Error("Not logged in.");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const loginApi = roborockAuth.createLoginApi({
|
|
55
|
+
baseURL,
|
|
56
|
+
username,
|
|
57
|
+
clientID,
|
|
58
|
+
language: "en",
|
|
59
|
+
});
|
|
60
|
+
loginApi.defaults.headers.common["Authorization"] = userData.token;
|
|
61
|
+
|
|
62
|
+
const homeDetail = await loginApi.get("api/v1/getHomeDetail");
|
|
63
|
+
const homeId =
|
|
64
|
+
homeDetail && homeDetail.data && homeDetail.data.data
|
|
65
|
+
? homeDetail.data.data.rrHomeId
|
|
66
|
+
: null;
|
|
67
|
+
if (!homeId) {
|
|
68
|
+
throw new Error("Failed to resolve the Roborock home id.");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const api = createSignedApi(userData.rriot);
|
|
72
|
+
const homedata = await api.get(`v2/user/homes/${homeId}`);
|
|
73
|
+
const result = (homedata && homedata.data && homedata.data.result) || {};
|
|
74
|
+
|
|
75
|
+
const products = result.products || [];
|
|
76
|
+
const modelOf = (productId) => {
|
|
77
|
+
const product = products.find((entry) => entry.id == productId);
|
|
78
|
+
return product ? product.model : null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const toEntry = (device, shared) => ({
|
|
82
|
+
duid: device.duid,
|
|
83
|
+
name: device.name,
|
|
84
|
+
model: modelOf(device.productId),
|
|
85
|
+
shared,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const owned = (result.devices || []).map((device) => toEntry(device, false));
|
|
89
|
+
const received = (result.receivedDevices || []).map((device) =>
|
|
90
|
+
toEntry(device, true)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return owned.concat(received);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { fetchDevices };
|