homebridge-roborock-matter 2.9.4 → 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 CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## 2.9.4
4
9
 
5
10
  Startup-cost cleanup release (also refreshes the npm README with the Donate button and the prominent Verified badge).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-roborock-matter",
3
- "version": "2.9.4",
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": {
@@ -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",
@@ -279,6 +288,19 @@ class Roborock {
279
288
  async setStateAsync(id, state) {
280
289
  try {
281
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
+ }
282
304
  const persistPath = this.getPersistPath(id);
283
305
  fs.mkdirSync(path.dirname(persistPath), { recursive: true });
284
306
  fs.writeFileSync(persistPath, JSON.stringify(state, null, 2, "utf8"));
@@ -1528,6 +1550,7 @@ class Roborock {
1528
1550
 
1529
1551
  async stopService() {
1530
1552
  try {
1553
+ this.flushPendingPersistedStates();
1531
1554
  await this.clearTimersAndIntervals();
1532
1555
  this.bInited = false;
1533
1556
  } catch (e) {
@@ -1535,6 +1558,59 @@ class Roborock {
1535
1558
  }
1536
1559
  }
1537
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
+
1538
1614
  async getUserData(loginApi) {
1539
1615
  try {
1540
1616
  if (this.isValidUserData(this.userData)) {