homebridge-roborock-matter 2.9.4 → 2.9.6

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.9.6
4
+
5
+ - **Friendlier plugin description and README.** The npm description shown in the Homebridge UI now leads with what the plugin does for you ("sign in with your Roborock account — start cleans, pick rooms, set suction power, and see live which room the robot is cleaning") instead of protocol terminology. The README's intro, feature matrix and live-room section were rewritten in plain language, with the technical depth preserved in collapsible under-the-hood sections. No functional changes.
6
+
7
+ ## 2.9.5
8
+
9
+ - **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.
10
+ - Full suite: 263 passing (3 new persistence-debounce tests).
11
+
3
12
  ## 2.9.4
4
13
 
5
14
  Startup-cost cleanup release (also refreshes the npm README with the Donate button and the prominent Verified badge).
package/README.md CHANGED
@@ -27,29 +27,29 @@
27
27
 
28
28
  ---
29
29
 
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.
30
+ Sign in with the same account you already use in the Roborock app — that's the whole setup. Every robot then appears in Apple Home as a real vacuum: start and stop cleans, send it to specific rooms, pick the suction power, check the battery — and watch the Home app tell you **which room it's cleaning right now**. No token extraction, no network tricks, no command line.
31
31
 
32
32
  ## Why this plugin
33
33
 
34
- - 🗣️ **The only plugin that speaks 2025 Roborock.** The B01/Q7-series (`roborock.vacuum.sc05`, Q7 M5 / M5+) exists solely in the Roborock app ecosystem — a new RPC dialect with an encrypted protobuf map channel that miio-based plugins cannot talk to at all. Fully implemented here: commands, status, battery, suction levels, room cleaning, and the map channel.
35
- - 📍 **Live room tracking — on every robot.** While the robot works, its position is read from the map channel (encrypted SCMap on B01/Q7, classic RRMap on S/Q-series), matched against your room geometry, and published as the current Matter Service Area. Apple Home shows _"Cleaning — Kitchen"_ — including runs started from the robot's button or the Roborock app. No other Homebridge plugin does this.
36
- - 🧭 **Matter-only, by design.** No legacy fan tiles, no helper-switch clutter. One robot, one native accessory, on Homebridge 2's built-in Matter bridge — including room/map selection sourced from your Roborock account's named rooms.
37
- - 🔌 **Cloud + local, automatically.** Commands prefer a direct local TCP connection to the robot and fall back to the Roborock cloud transparently, with per-device connection diagnostics in the settings UI when you want to see exactly what happened.
38
- - 🛡️ **Hardened and boring where it counts.** 260 automated tests, CI on Node 22/24 against Homebridge 1.11 and 2.x, zero known vulnerabilities, no analytics, no post-install scripts, and a startup that retries with backoff instead of ever crash-looping Homebridge. **Verified by Homebridge** after review by the Homebridge team.
34
+ - 🥇 **The only plugin that works with the newest Roborocks.** The 2025 Q7 series (Q7 M5 / M5+) speaks a brand-new protocol that no other Homebridge plugin understands. Fully supported here — alongside the classic S- and Q-series models.
35
+ - 📍 **See where it's cleaning — live.** Apple Home shows _"Cleaning — Kitchen"_ with the room the robot is actually inside, updating as it moves from room to room. Works even for cleans started from the robot's button or the Roborock app. No other Homebridge plugin does this.
36
+ - 🧭 **One robot, one tile.** Your vacuum shows up as a single, native accessory in Apple Home — no clutter of fake fans and helper switches. Rooms appear with the names you gave them in the Roborock app.
37
+ - ⚡ **Fast and reliable.** Commands go directly to the robot over your own network whenever possible, with the Roborock cloud as automatic backup — and built-in diagnostics in the settings if you ever want to look under the hood.
38
+ - 🛡️ **Verified by Homebridge.** Reviewed and endorsed by the Homebridge team. 263 automated tests, zero known vulnerabilities, no analytics, and a startup designed to never crash your Homebridge — even when your Wi-Fi or the Roborock cloud has a bad day.
39
39
 
40
40
  ## Features
41
41
 
42
- | | |
43
- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
44
- | 🤖 **Native Matter RVC** | Start / stop / pause / return-to-dock, run modes, operational state, error reporting |
45
- | 🚪 **Room cleaning from Apple Home** | Matter Service Area selection with your real room names, multi-map homes included |
46
- | 📍 **Live room tracking** | The room the robot is physically inside, updated every ~20 s while cleaning ([details](#live-room-tracking)) |
47
- | 📊 **Honest cleaning progress** | Per-room pending → operating → completed, only claiming rooms the robot was actually detected in |
48
- | 🌀 **Cleaning modes** | Vacuum / Mop / Vacuum + Mop, capability-gated per robot — plus optional Quiet / Balanced / Turbo / Max suction modes (and Max+ on Q7) |
49
- | 🔋 **Battery & charging** | Live percentage and charge state via Matter PowerSource ([one Apple-side caveat](#battery-percentage-in-apple-home)) |
50
- | 🧠 **Self-adapting model support** | Unknown models get capability-derived polling; requests a robot reports as unsupported are disabled automatically |
51
- | 🩺 **Built-in diagnostics** | Connection state, transport history, live LAN probe, and a redacted report generator for bug reports |
52
- | 🔐 **2FA-friendly login** | Roborock account two-factor authentication handled entirely in the settings UI |
42
+ | | |
43
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
44
+ | 🤖 **Full control from Apple Home** | Start, stop, pause and send the robot home to its dock — from the Home app, Siri, or automations |
45
+ | 🚪 **Clean specific rooms** | Pick rooms right in Apple Home, with the names you gave them in the Roborock app — multi-floor homes included |
46
+ | 📍 **Live room tracking** | See which room the robot is cleaning right now, updated as it moves ([details](#live-room-tracking)) |
47
+ | 📊 **Honest cleaning progress** | Each room goes pending → cleaning → done — and a room only counts as done when the robot was actually there |
48
+ | 🌀 **Cleaning & suction modes** | Vacuum / Mop / Vacuum + Mop on models that support it — plus optional Quiet / Balanced / Turbo / Max suction levels (Max+ on Q7) |
49
+ | 🔋 **Battery & charging** | Battery level and charging state on the accessory ([one Apple-side caveat](#battery-percentage-in-apple-home)) |
50
+ | 🧠 **New models just work** | Brand-new Roborock models get sensible defaults automatically, and the plugin adapts to what each robot actually supports |
51
+ | 🩺 **Built-in diagnostics** | Connection status, a one-click connection test, and a ready-to-share report if you ever need help |
52
+ | 🔐 **Easy, safe login** | Sign in with your Roborock account right in the settings — two-factor supported, session stored encrypted |
53
53
 
54
54
  ## Quick start
55
55
 
@@ -66,13 +66,16 @@ For B01/Q7 robots, room selection appears once the map has been fetched (watch f
66
66
 
67
67
  ## Live room tracking
68
68
 
69
- While a robot is actively cleaning, the plugin fetches its live position from the map channel (throttled to ~20 s, active runs only, nothing while docked or paused) and publishes the room it is inside as the Matter Service Area `currentArea`. Both robot generations are covered: **B01/Q7** robots via the encrypted SCMap protobuf (position ray-cast against per-room boundary outlines), **classic S/Q-series** robots via the RRMap segment grid (position resolved against per-pixel room segments):
69
+ While your robot cleans, the plugin follows its position on the map and tells Apple Home which room it's in — _"Cleaning — Kitchen"_, just like the Roborock app shows it. It updates as the robot moves, works for whole-home cleans, and even for cleans you start from the robot's button.
70
70
 
71
- - Apple Home's status pill names the room the robot is **physically inside** — the way the vendor app does it.
72
- - Works for full-home cleans and for runs started from the robot's button or the Roborock app, which previously had no room to show at all.
73
- - Progress stays honest: a room is only marked _completed_ once the robot was actually detected inside it and has moved on. The plugin never invents data the robot didn't report.
71
+ Progress stays honest: a room is only shown as _completed_ once the robot was actually seen inside it. The plugin never invents data the robot didn't report. Enabled by default; turn it off with `enableLiveRoomTracking: false`.
74
72
 
75
- Enabled by default; opt out with `enableLiveRoomTracking: false`.
73
+ <details>
74
+ <summary>How it works under the hood</summary>
75
+
76
+ While a robot is actively cleaning, the plugin fetches its live position from the map channel (throttled to ~20 s, active runs only, nothing while docked or paused) and publishes the room it is inside as the Matter Service Area `currentArea`. Both robot generations are covered: **B01/Q7** robots via the encrypted SCMap protobuf (position ray-cast against per-room boundary outlines), **classic S/Q-series** robots via the RRMap segment grid (position resolved against per-pixel room segments — a single-byte lookup on the raw map buffer, ~1 µs per check).
77
+
78
+ </details>
76
79
 
77
80
  ## Suction modes (optional)
78
81
 
@@ -122,7 +125,7 @@ The complete path — robot → plugin → Homebridge → matter.js store — wa
122
125
 
123
126
  ## Contributing
124
127
 
125
- Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 256 tests (protocol fixtures verified against the [python-roborock](https://github.com/Python-roborock/python-roborock) reference), strict TypeScript checking, and CI across Node 22/24 × Homebridge 1.11/2.x — `npm test` before you push and you're set.
128
+ Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 263 tests (protocol fixtures verified against the [python-roborock](https://github.com/Python-roborock/python-roborock) reference), strict TypeScript checking, and CI across Node 22/24 × Homebridge 1.11/2.x — `npm test` before you push and you're set.
126
129
 
127
130
  ## Support the project
128
131
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-roborock-matter",
3
- "version": "2.9.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.",
3
+ "version": "2.9.6",
4
+ "description": "Control your Roborock robot vacuums from Apple Home — just sign in with your Roborock account. Start cleans, pick rooms, set suction power, and see live which room the robot is cleaning. The only plugin supporting the new 2025 Q7 series, alongside classic S- and Q-series models. Verified by Homebridge.",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Mathias Hornbek"
@@ -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)) {