homebridge-roborock-matter 3.17.0 → 3.17.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.17.2
4
+
5
+ **The Qrevo CurvX's dock can now offer the Empty Bin switch.** Reported with a diagnostics export, and then settled by hand, by [@jcoz00](https://github.com/jcoz00) in [#6](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/6). His a185 reports `dock_type: 20`, and the dock table this plugin inherited stops at 9 — so the CurvX fell through to "unknown dock" and was treated as having no auto-empty capability, which kept the optional Empty Bin switch added in 3.17.0 from ever being offered for it. Dock type 20 is now a named, recognised auto-empty dock.
6
+
7
+ The switch is still opt-in and still off by default, so nothing changes for anyone who has not asked for it.
8
+
9
+ **Only the auto-empty is granted, and only because its owner confirmed it.** Upstream also names a dozen dock codes above 9 that this project has had no report for, and none of those was added — a capability granted on a table alone is what cost a Q Revo S owner a suction level its robot does not have in [#10](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/10). Wash and dry are unconfirmed on this dock and stay unclaimed. A test now pins both halves: dock type 20 is in the set because an owner said so, and the codes nobody has reported stay out until one does.
10
+
11
+ ## 3.17.1
12
+
13
+ **Closing the plugin's settings page could print a Node crash dump into your Homebridge log.** Reported with the log to prove it by [@jcoz00](https://github.com/jcoz00) in [#6](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/6). The Homebridge UI runs the settings-page server as a child process and closes its IPC channel the moment the page goes away; every reply that server sends is a `process.send()`, including the `ready()` handshake it fires before serving a single request. A send that loses the race against that close is reported asynchronously as an unhandled `'error'` event, which is fatal — so a closed settings page ended in `Error: write EPIPE`, a stack trace and a `Node.js v24.19.0` banner in the log. Nothing was broken and nothing in the log said so. A dead channel now ends that child process quietly; every other error stays exactly as loud as it was.
14
+
15
+ **A robot's dock capability is announced when it changes, instead of on every poll.** `dock_type` rides along in nearly every `get_status`, and 3.17.0 told the platform about it each time, re-running the HomeKit action-switch sync roughly once a minute per robot. At default settings that sync returns immediately, but anyone who had switched the Empty Bin action on for a robot whose dock cannot auto-empty collected a `Not publishing the Empty Bin switch…` debug line every minute per robot — enough to shorten the useful reach of the debug log. Detection itself still runs on every poll; only the announcement is gated, and a dock type that genuinely changes is still announced.
16
+
3
17
  ## 3.17.0
4
18
 
5
19
  **Compatible auto-empty docks can now expose an optional Empty Bin action switch in Apple Home.** Contributed by [@jbyhb](https://github.com/jbyhb) in [#13](https://github.com/mathiashornbek/homebridge-roborock-matter/pull/13). It uses the same opt-in HomeKit action-switch bridge as Start, Dock, Pause and Find, appears only when the robot reports dust-collection support, and sends the dock's native `app_start_collect_dust` command through the normal confirmed command path. A cached status that does not show the robot docked is advisory rather than a hard gate: the robot is the authoritative judge and its refusal follows the existing command-error path.
package/README.md CHANGED
@@ -37,7 +37,7 @@ This is the most feature-packed, most thoroughly engineered Roborock plugin for
37
37
  - 📍 **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.
38
38
  - 🧭 **One robot, one tile — and as many robots as you own.** Sign in once and your whole fleet comes along: every vacuum on your account appears as its own clean, native accessory in Apple Home. No clutter of fake fans and helper switches, and rooms appear with the names you gave them in the Roborock app.
39
39
  - ⚡ **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.
40
- - 🛡️ **Verified by Homebridge.** Reviewed and endorsed by the Homebridge team. 1393 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.
40
+ - 🛡️ **Verified by Homebridge.** Reviewed and endorsed by the Homebridge team. 1438 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.
41
41
 
42
42
  ## Features
43
43
 
@@ -252,7 +252,7 @@ The complete path — robot → plugin → Homebridge → matter.js store — wa
252
252
 
253
253
  ## Contributing
254
254
 
255
- Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 1393 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.
255
+ Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 1438 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.
256
256
 
257
257
  ## Support the project
258
258
 
@@ -7,6 +7,13 @@ import { createRequire } from "node:module";
7
7
  import { HomebridgePluginUiServer } from "@homebridge/plugin-ui-utils";
8
8
 
9
9
  const require = createRequire(import.meta.url);
10
+ const {
11
+ installChannelGoneGuard,
12
+ } = require("../roborockLib/lib/uiServerLifecycle.js");
10
13
  const { RoborockUiServer } = require("../dist/ui/index.js");
11
14
 
15
+ // Before the server is built, not after: its constructor calls ready(), and
16
+ // that send is the one that crashed for a user who closed the settings page.
17
+ installChannelGoneGuard(process);
18
+
12
19
  new RoborockUiServer(HomebridgePluginUiServer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-roborock-matter",
3
- "version": "3.17.0",
3
+ "version": "3.17.2",
4
4
  "description": "The most complete Roborock plugin for Apple Home. Supports the entire Roborock lineup — from the classic S-series to the new 2025 Q7 series that no other plugin can control. Sign in with your Roborock account and get native start/stop, room cleaning, suction levels, battery, and live 'cleaning in the kitchen' room tracking. Verified by Homebridge.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -68,6 +68,12 @@ const dockTypes = {
68
68
  7: "Empty Wash Fill Dry Dock (S8 Pro Ultra)",
69
69
  8: "Empty Wash Fill Dry Dock (Q Revo)",
70
70
  9: "Empty Wash Fill Dry Dock (Q Revo Pro)",
71
+ // Upstream python-roborock calls 20 `k1s_dock`. It reached this project as a
72
+ // diagnostics export from an a185 (Qrevo CurvX) owner in issue #6 — the
73
+ // first dock code ever seen here above 9 — and he confirmed the auto-empty
74
+ // by hand. Named for the capability he reported, not for the ones upstream's
75
+ // codename might imply: nobody has confirmed wash or dry on this dock.
76
+ 20: "Auto-Empty Dock (K1S — Qrevo CurvX)",
71
77
  };
72
78
 
73
79
  const firmwareFeatures = {
@@ -1393,6 +1399,15 @@ class deviceFeatures {
1393
1399
  this.isWashThenChargeCmdSupported();
1394
1400
  this.isSupportedDrying();
1395
1401
  break;
1402
+ // K1S: a185 Qrevo CurvX. Auto-empty only, and deliberately so — its
1403
+ // owner confirmed the dock empties the robot's bin (issue #6, 23 Aug
1404
+ // 2026), which is the whole of what was asked and the whole of what is
1405
+ // granted. Wash and dry are unconfirmed on this dock, and the a104
1406
+ // suction level in issue #10 is what a capability granted on a table
1407
+ // alone costs. Add them here on an owner's word, not on a codename's.
1408
+ case 20:
1409
+ this.isDustCollectionSettingSupported();
1410
+ break;
1396
1411
  default:
1397
1412
  break;
1398
1413
  }
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+
3
+ // The Homebridge UI runs this plugin's settings-page server as a child
4
+ // process and closes the IPC channel the moment the page goes away. Every
5
+ // answer that server gives is a `process.send()` — including the one-shot
6
+ // `ready()` handshake its constructor fires before it has served a single
7
+ // request — and a send that loses the race against that close does not throw.
8
+ // Node reports it asynchronously as an `'error'` event on `process`, and an
9
+ // `'error'` event with no listener is fatal.
10
+ //
11
+ // Measured in the wild on an a185 (issue #6, 23 August 2026): closing the
12
+ // settings page printed a full Node crash dump into the user's Homebridge log
13
+ // — `Error: write EPIPE` inside `HomebridgePluginUiServer.ready`, "Unhandled
14
+ // 'error' event", a stack trace and a `Node.js v24.19.0` banner — for a page
15
+ // they had already closed and a process that had nothing left to do. Nothing
16
+ // was broken. Nothing in the log said so.
17
+ //
18
+ // Checking `process.connected` before sending does not fix this: the channel
19
+ // can close between the check and the write. The listener does, so the child
20
+ // gets exactly one, installed before the server is constructed. A dead
21
+ // channel is a normal end of life and exits quietly; anything else is a real
22
+ // fault and stays exactly as loud as it was before this file existed.
23
+
24
+ /**
25
+ * The error codes Node uses when the other end of the IPC channel is already
26
+ * gone. `EPIPE` is the write losing the race, the `ERR_IPC_*` pair is the
27
+ * same condition caught before the write is attempted, and `ECONNRESET` is
28
+ * the parent tearing the socket down mid-write.
29
+ *
30
+ * @type {readonly string[]}
31
+ */
32
+ const CHANNEL_GONE_CODES = Object.freeze([
33
+ "EPIPE",
34
+ "ERR_IPC_CHANNEL_CLOSED",
35
+ "ERR_IPC_DISCONNECTED",
36
+ "ECONNRESET",
37
+ ]);
38
+
39
+ /**
40
+ * Is this the parent having gone away, rather than a fault worth reporting?
41
+ *
42
+ * @param {unknown} error
43
+ * @returns {boolean}
44
+ */
45
+ function isChannelGoneError(error) {
46
+ if (!error || typeof error !== "object") {
47
+ return false;
48
+ }
49
+
50
+ const code = /** @type {{ code?: unknown }} */ (error).code;
51
+
52
+ return typeof code === "string" && CHANNEL_GONE_CODES.includes(code);
53
+ }
54
+
55
+ /**
56
+ * Install the one listener that keeps a closed settings page from looking
57
+ * like a plugin crash.
58
+ *
59
+ * @param {NodeJS.EventEmitter & { exit?: (code?: number) => void }} proc
60
+ * The process to guard. Injected rather than closed over so the rule can be
61
+ * exercised without ending the test runner.
62
+ * @param {(error: unknown) => void} [onChannelGone]
63
+ * What to do once the channel is confirmed gone. Defaults to exiting
64
+ * cleanly: the parent that asked for this server no longer exists, so
65
+ * lingering would leak a child process per opened settings page.
66
+ * @returns {NodeJS.EventEmitter} the same process, for chaining.
67
+ */
68
+ function installChannelGoneGuard(proc, onChannelGone) {
69
+ const handleChannelGone =
70
+ onChannelGone ||
71
+ ((/** @type {unknown} */ _error) => {
72
+ if (typeof proc.exit === "function") {
73
+ proc.exit(0);
74
+ }
75
+ });
76
+
77
+ proc.on("error", (error) => {
78
+ if (isChannelGoneError(error)) {
79
+ handleChannelGone(error);
80
+ return;
81
+ }
82
+
83
+ // Not our case. Re-throwing from the listener turns this back into the
84
+ // uncaught exception it would have been, stack intact.
85
+ throw error;
86
+ });
87
+
88
+ return proc;
89
+ }
90
+
91
+ module.exports = {
92
+ CHANNEL_GONE_CODES,
93
+ isChannelGoneError,
94
+ installChannelGoneGuard,
95
+ };
@@ -137,6 +137,41 @@ class vacuum {
137
137
  * @type {Map<string, Set<string>>}
138
138
  */
139
139
  this.reportedUnmappedStatusAttributes = new Map();
140
+
141
+ /**
142
+ * The dock type last seen from a robot's live `get_status`, per duid.
143
+ *
144
+ * `processDockType()` is idempotent and cheap, so it keeps running on
145
+ * every poll. Telling the platform about it does not: the notification
146
+ * re-runs the HomeKit action-switch sync behind it. A robot that reports
147
+ * `dock_type` in every `get_status` — which is most of them — therefore
148
+ * re-announced unchanged capabilities roughly once a minute per robot,
149
+ * and a user who had opted the Empty Bin switch on for a robot without an
150
+ * auto-empty dock collected the "Not publishing the Empty Bin switch"
151
+ * debug line at that same rate. A dock type is worth announcing when it
152
+ * is new or has actually changed; a repeat of the same value cannot tell
153
+ * the platform anything it did not already act on.
154
+ *
155
+ * @type {Map<string, unknown>}
156
+ */
157
+ this.lastSeenDockType = new Map();
158
+ }
159
+
160
+ /**
161
+ * Record the dock type a robot just reported, and say whether it is news.
162
+ *
163
+ * @param {string} duid
164
+ * @param {unknown} dockType
165
+ * @returns {boolean} true on the first sighting, and on every real change
166
+ */
167
+ rememberDockType(duid, dockType) {
168
+ const isNews =
169
+ !this.lastSeenDockType.has(duid) ||
170
+ this.lastSeenDockType.get(duid) !== dockType;
171
+
172
+ this.lastSeenDockType.set(duid, dockType);
173
+
174
+ return isNews;
140
175
  }
141
176
 
142
177
  /**
@@ -574,7 +609,10 @@ class vacuum {
574
609
  this.adapter.vacuums[duid].features.processDockType(
575
610
  deviceStatus[0][attribute]
576
611
  );
577
- dockCapabilityUpdated = true;
612
+ dockCapabilityUpdated = this.rememberDockType(
613
+ duid,
614
+ deviceStatus[0][attribute]
615
+ );
578
616
  }
579
617
 
580
618
  if (
@@ -2456,7 +2456,13 @@ class Roborock {
2456
2456
 
2457
2457
  supportsDustCollection(duid) {
2458
2458
  const dockType = Number(this.getVacuumDeviceStatus(duid, "dock_type"));
2459
- const autoEmptyDockTypes = new Set([1, 3, 5, 6, 7, 8, 9]);
2459
+ // 20 (upstream `k1s_dock`, the a185 Qrevo CurvX) is here because its owner
2460
+ // confirmed the auto-empty in issue #6, not because upstream names the
2461
+ // code. Upstream also names 10, 11, 13-19, 21-24 and 26; none of those has
2462
+ // an owner report, so none of them is here. Keep this set and
2463
+ // `processDockType()` in step — a code in one but not the other is a dock
2464
+ // that either offers a switch it cannot drive or hides one it can.
2465
+ const autoEmptyDockTypes = new Set([1, 3, 5, 6, 7, 8, 9, 20]);
2460
2466
 
2461
2467
  return (
2462
2468
  autoEmptyDockTypes.has(dockType) ||