homebridge-roborock-matter 2.9.3 → 2.9.5
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/CHANGELOG.md +12 -0
- package/README.md +8 -1
- package/package.json +1 -1
- package/roborockLib/lib/message.js +12 -1
- package/roborockLib/lib/roborock_mqtt_connector.js +4 -19
- package/roborockLib/roborockAPI.js +76 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.9.5
|
|
4
|
+
|
|
5
|
+
- **One synchronous disk write per received robot message eliminated.** The per-device diagnostics states (last cloud/local message, transport history) were flushed to disk with a blocking `fs.writeFileSync` on EVERY message a robot pushed — every few seconds per robot while cleaning. They are served from memory (the settings UI never reads the file); the on-disk copy only needs to survive restarts. Disk flushes for these two states are now debounced to at most once per minute, with a guaranteed flush on shutdown. Result: event-loop stalls removed from the message hot path, and meaningfully less SD-card wear on Raspberry Pi installs. Critical states (credentials, HomeData, room caches) still persist immediately.
|
|
6
|
+
- Full suite: 263 passing (3 new persistence-debounce tests).
|
|
7
|
+
|
|
8
|
+
## 2.9.4
|
|
9
|
+
|
|
10
|
+
Startup-cost cleanup release (also refreshes the npm README with the Donate button and the prominent Verified badge).
|
|
11
|
+
|
|
12
|
+
- **Two fewer RSA-2048 key generations at every startup.** The MQTT connector generated a protocol keypair that nothing ever read (removed), and the message layer generated its keypair eagerly even though it is only needed for the rare photo request path on camera-equipped models (now created lazily on first use). Measured ~50 ms per keygen on fast hardware — substantially more on a Raspberry Pi.
|
|
13
|
+
- Removed dead code: the never-called `decryptWithPrivateKey` helper and the unused `scenesData` field (both HomeKit-era leftovers).
|
|
14
|
+
|
|
3
15
|
## 2.9.3
|
|
4
16
|
|
|
5
17
|
**The plugin is now Verified by Homebridge!** 🎉 Reviewed and endorsed by the Homebridge team (homebridge/plugins#1124), with specific praise for the encrypted at-rest session storage, the preserved fork attribution, and the per-release notes.
|
package/README.md
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<a href="https://github.com/homebridge/homebridge/wiki/Verified-Plugins"><img src="https://img.shields.io/badge/homebridge-verified-blueviolet?color=%23491F59&style=flat" alt="verified-by-homebridge"></a>
|
|
13
12
|
<a href="https://www.npmjs.com/package/homebridge-roborock-matter"><img src="https://img.shields.io/npm/v/homebridge-roborock-matter?label=npm&color=cb3837" alt="npm version"></a>
|
|
14
13
|
<a href="https://www.npmjs.com/package/homebridge-roborock-matter"><img src="https://img.shields.io/npm/dt/homebridge-roborock-matter?label=downloads&color=8a5cf5" alt="npm downloads"></a>
|
|
15
14
|
<a href="https://github.com/mathiashornbek/homebridge-roborock-matter/actions"><img src="https://img.shields.io/github/actions/workflow/status/mathiashornbek/homebridge-roborock-matter/nodejs.yml?label=CI" alt="CI status"></a>
|
|
@@ -18,6 +17,14 @@
|
|
|
18
17
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
|
|
19
18
|
</p>
|
|
20
19
|
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="https://paypal.me/MathiasHornbek"><img src="https://img.shields.io/badge/PayPal-Donate-00457C?logo=paypal&logoColor=white" alt="Donate via PayPal"></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<a href="https://github.com/homebridge/homebridge/wiki/Verified-Plugins"><img src="https://img.shields.io/badge/homebridge-verified-blueviolet?color=%23491F59&style=for-the-badge&logoColor=%23FFFFFF&logo=homebridge" alt="Verified by Homebridge"></a>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
21
28
|
---
|
|
22
29
|
|
|
23
30
|
Log in with your **Roborock app account** — no token extraction, no rooted apps, no packet sniffing — and every robot appears in Apple Home as a first-class **Matter Robotic Vacuum Cleaner**: start, pause, dock, pick rooms, choose cleaning modes, and watch the status pill name the room the robot is _actually inside_, live.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-roborock-matter",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.5",
|
|
4
4
|
"description": "Matter-only Homebridge plugin publishing Roborock robot vacuums (including 2025 B01/Q7-series) as native Matter accessories for Apple Home. Fork of homebridge-roborock-vacuum2.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -32,7 +32,18 @@ class message {
|
|
|
32
32
|
this.adapter = adapter;
|
|
33
33
|
this.missingLocalKeyWarnings = new Set();
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// The protocol RSA keypair is only needed for the rare photo request
|
|
36
|
+
// path (camera-equipped models). Generate it lazily on first use
|
|
37
|
+
// instead of paying a full RSA-2048 keygen (~50 ms on fast hardware,
|
|
38
|
+
// substantially more on a Raspberry Pi) at every startup.
|
|
39
|
+
this._keys = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get keys() {
|
|
43
|
+
if (!this._keys) {
|
|
44
|
+
this._keys = roborockCrypto.generateRsaKeyPair();
|
|
45
|
+
}
|
|
46
|
+
return this._keys;
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
async buildPayload(
|
|
@@ -77,7 +77,10 @@ class roborock_mqtt_connector {
|
|
|
77
77
|
|
|
78
78
|
this.connected = false;
|
|
79
79
|
|
|
80
|
-
this
|
|
80
|
+
// NOTE: this class previously generated its own RSA-2048 keypair here,
|
|
81
|
+
// but nothing ever read it — the protocol keypair lives in message.js
|
|
82
|
+
// (lazily created for the rare photo path). Removed: one full RSA
|
|
83
|
+
// keygen less at every startup.
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
async initUser(userdata) {
|
|
@@ -524,24 +527,6 @@ class roborock_mqtt_connector {
|
|
|
524
527
|
|
|
525
528
|
return false;
|
|
526
529
|
}
|
|
527
|
-
|
|
528
|
-
decryptWithPrivateKey(privateKeyPem, encryptedData) {
|
|
529
|
-
const privateKey = crypto.createPrivateKey({
|
|
530
|
-
key: privateKeyPem,
|
|
531
|
-
format: "pem",
|
|
532
|
-
type: "pkcs8",
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
const decryptedData = crypto.privateDecrypt(
|
|
536
|
-
{
|
|
537
|
-
key: privateKey,
|
|
538
|
-
padding: crypto.constants.RSA_PKCS1_PADDING,
|
|
539
|
-
},
|
|
540
|
-
encryptedData
|
|
541
|
-
);
|
|
542
|
-
|
|
543
|
-
return decryptedData;
|
|
544
|
-
}
|
|
545
530
|
}
|
|
546
531
|
|
|
547
532
|
/**
|
|
@@ -42,6 +42,15 @@ const B01_LIVE_ROOM_CLEAR_V1_STATES = new Set([3, 8]);
|
|
|
42
42
|
// slower cadence than the ~12s active status polls.
|
|
43
43
|
const B01_LIVE_ROOM_MIN_FETCH_GAP_MS = 20000;
|
|
44
44
|
|
|
45
|
+
// Persisted states whose disk flush is debounced (see setStateAsync): they
|
|
46
|
+
// change on every received robot message, are served from memory, and only
|
|
47
|
+
// need the on-disk copy for restart survival.
|
|
48
|
+
const DEBOUNCED_PERSIST_IDS = new Set([
|
|
49
|
+
"TransportDiagnostics",
|
|
50
|
+
"RoborockDiagnostics",
|
|
51
|
+
]);
|
|
52
|
+
const PERSIST_FLUSH_DEBOUNCE_MS = 60000;
|
|
53
|
+
|
|
45
54
|
const PERSISTED_STATE_IDS = new Set([
|
|
46
55
|
"UserData",
|
|
47
56
|
"clientID",
|
|
@@ -122,8 +131,6 @@ class Roborock {
|
|
|
122
131
|
this.localDevices = {};
|
|
123
132
|
this.remoteDevices = new Set();
|
|
124
133
|
|
|
125
|
-
this.scenesData = null; // Store scenes data locally
|
|
126
|
-
|
|
127
134
|
this.name = "roborock";
|
|
128
135
|
this.deviceNotify = null;
|
|
129
136
|
this.serviceAreaRoomMapRefreshAttempts = new Map();
|
|
@@ -281,6 +288,19 @@ class Roborock {
|
|
|
281
288
|
async setStateAsync(id, state) {
|
|
282
289
|
try {
|
|
283
290
|
if (PERSISTED_STATE_IDS.has(id)) {
|
|
291
|
+
// Chatty diagnostic states update on every received robot message
|
|
292
|
+
// (every few seconds while cleaning). They are read from memory by
|
|
293
|
+
// the settings UI; the on-disk copy only needs to survive restarts.
|
|
294
|
+
// Debouncing their disk flush to once per minute turns one
|
|
295
|
+
// SYNCHRONOUS write per robot message into at most one per minute
|
|
296
|
+
// — a real win for event-loop latency and SD-card wear on
|
|
297
|
+
// Raspberry Pi installs. Critical states (credentials, HomeData,
|
|
298
|
+
// room caches) still persist immediately.
|
|
299
|
+
if (DEBOUNCED_PERSIST_IDS.has(id)) {
|
|
300
|
+
this.states[id] = state;
|
|
301
|
+
this.schedulePersistFlush(id);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
284
304
|
const persistPath = this.getPersistPath(id);
|
|
285
305
|
fs.mkdirSync(path.dirname(persistPath), { recursive: true });
|
|
286
306
|
fs.writeFileSync(persistPath, JSON.stringify(state, null, 2, "utf8"));
|
|
@@ -1530,6 +1550,7 @@ class Roborock {
|
|
|
1530
1550
|
|
|
1531
1551
|
async stopService() {
|
|
1532
1552
|
try {
|
|
1553
|
+
this.flushPendingPersistedStates();
|
|
1533
1554
|
await this.clearTimersAndIntervals();
|
|
1534
1555
|
this.bInited = false;
|
|
1535
1556
|
} catch (e) {
|
|
@@ -1537,6 +1558,59 @@ class Roborock {
|
|
|
1537
1558
|
}
|
|
1538
1559
|
}
|
|
1539
1560
|
|
|
1561
|
+
/**
|
|
1562
|
+
* Schedule a debounced disk flush for a chatty persisted state. The
|
|
1563
|
+
* in-memory copy is already current; the trailing flush (unref'd so it
|
|
1564
|
+
* never keeps the process alive) writes the LATEST value at most once
|
|
1565
|
+
* per PERSIST_FLUSH_DEBOUNCE_MS.
|
|
1566
|
+
* @param {string} id
|
|
1567
|
+
*/
|
|
1568
|
+
schedulePersistFlush(id) {
|
|
1569
|
+
if (!this._pendingPersistFlushes) {
|
|
1570
|
+
this._pendingPersistFlushes = new Map();
|
|
1571
|
+
}
|
|
1572
|
+
if (this._pendingPersistFlushes.has(id)) {
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
const timer = setTimeout(() => {
|
|
1576
|
+
this._pendingPersistFlushes.delete(id);
|
|
1577
|
+
this.persistStateToDisk(id);
|
|
1578
|
+
}, PERSIST_FLUSH_DEBOUNCE_MS);
|
|
1579
|
+
if (typeof timer?.unref === "function") {
|
|
1580
|
+
timer.unref();
|
|
1581
|
+
}
|
|
1582
|
+
this._pendingPersistFlushes.set(id, timer);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/** Write the current in-memory value of a persisted state to disk now. */
|
|
1586
|
+
persistStateToDisk(id) {
|
|
1587
|
+
try {
|
|
1588
|
+
const state = this.states[id];
|
|
1589
|
+
if (state === undefined) {
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const persistPath = this.getPersistPath(id);
|
|
1593
|
+
fs.mkdirSync(path.dirname(persistPath), { recursive: true });
|
|
1594
|
+
fs.writeFileSync(persistPath, JSON.stringify(state, null, 2));
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
this.log.debug(
|
|
1597
|
+
`Debounced persist of '${id}' failed: ${error?.message || error}`
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
/** Flush all pending debounced persists immediately (shutdown path). */
|
|
1603
|
+
flushPendingPersistedStates() {
|
|
1604
|
+
if (!this._pendingPersistFlushes) {
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
for (const [id, timer] of this._pendingPersistFlushes) {
|
|
1608
|
+
clearTimeout(timer);
|
|
1609
|
+
this.persistStateToDisk(id);
|
|
1610
|
+
}
|
|
1611
|
+
this._pendingPersistFlushes.clear();
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1540
1614
|
async getUserData(loginApi) {
|
|
1541
1615
|
try {
|
|
1542
1616
|
if (this.isValidUserData(this.userData)) {
|