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.
|
|
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
|
|
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
|
|
package/homebridge-ui/server.js
CHANGED
|
@@ -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.
|
|
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 =
|
|
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
|
-
|
|
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) ||
|