homebridge-roborock-matter 3.19.8 → 3.20.0
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 +26 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/roborockLib/roborockAPI.js +108 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.20.0
|
|
4
|
+
|
|
5
|
+
**Two ways the plugin could stop working and never start again. Both silent, both permanent until somebody restarted Homebridge.**
|
|
6
|
+
|
|
7
|
+
Neither was a crash, which is why neither had been noticed. The plugin stayed up and stopped doing its job.
|
|
8
|
+
|
|
9
|
+
### A classic robot that flapped offline was never polled again
|
|
10
|
+
|
|
11
|
+
`manageDeviceIntervals` stops both polling intervals when a robot reads as offline and restarts them when it reads as online. The restart half was unreachable for a classic robot: the only caller sits **inside the `get_status` handler**, so it runs only when a status poll succeeds — and `getStatusIntervalHandle` is the one thing driving those polls. Once they were cleared, nothing was left alive to notice the robot had come back. The one other caller, the home-data supervisor, filtered on B01 and skipped every classic robot deliberately.
|
|
12
|
+
|
|
13
|
+
`onlineChecker` reads the cached home-data snapshot, which lags by up to one refresh, so the trigger was ordinary rather than exotic: robot drops off wifi, comes back, its LAN socket reconnects first, the next local poll succeeds, the snapshot still says offline, both intervals die. From then on the tile froze on the last known state until the user pressed a button or restarted.
|
|
14
|
+
|
|
15
|
+
Same dead end at boot, for a different reason: the intervals are only started `if (device.online)`, so a robot that was offline when Homebridge started was **never polled at all** for the life of the process.
|
|
16
|
+
|
|
17
|
+
The supervisor now covers every robot. It runs on the home-data cadence, which is the same clock that decides `onlineChecker`'s answer, so a robot that comes back is picked up on the tick that notices it.
|
|
18
|
+
|
|
19
|
+
### A cloud outage during startup wedged the plugin permanently
|
|
20
|
+
|
|
21
|
+
`getUserData` returns a stored session **without touching the network**, so any install that has logged in once — essentially all of them — never reaches the login retry with backoff. It reaches `getHomeDetail` instead. When that failed, the plugin logged one warning and stopped.
|
|
22
|
+
|
|
23
|
+
The ordering is what made it terminal: `homedataInterval` and `reconnectIntervall` are created _after_ that call, and `initUser` was never reached, so there was no MQTT client either. **Not one timer existed that would ever try again.** A Pi rebooting after a power cut, with the router still coming up, registered nothing and sat idle until a human intervened. The README's "retries with increasing backoff, up to 10 attempts" described only the login step, which this path skips.
|
|
24
|
+
|
|
25
|
+
There is now a retry: 1 minute, doubling to a 10-minute ceiling, then holding there. Deliberately with no attempt cap — the failure it recovers from is "the network was not ready yet" and the device is unattended, so giving up means a person has to notice. One timer at a time, unref'd so it can never hold Homebridge open, cleared on shutdown, and reset by a success so a later outage starts from 1 minute again.
|
|
26
|
+
|
|
27
|
+
9 new tests, and the supervisor's own test previously asserted the skip that caused the first bug.
|
|
28
|
+
|
|
3
29
|
## 3.19.8
|
|
4
30
|
|
|
5
31
|
**A recovery line now names only a failure the log actually announced — and the test that says so had been failing on disk, uncommitted, for 3 days.**
|
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. 1663 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
|
|
|
@@ -255,7 +255,7 @@ The complete path — robot → plugin → Homebridge → matter.js store — wa
|
|
|
255
255
|
|
|
256
256
|
## Contributing
|
|
257
257
|
|
|
258
|
-
Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with
|
|
258
|
+
Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 1663 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.
|
|
259
259
|
|
|
260
260
|
## Support the project
|
|
261
261
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-roborock-matter",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
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": {
|
|
@@ -1968,6 +1968,13 @@ class Roborock {
|
|
|
1968
1968
|
await this.initializeDeviceUpdates();
|
|
1969
1969
|
|
|
1970
1970
|
this.bInited = true;
|
|
1971
|
+
// A success ends the retry chain and forgets the backoff, so a
|
|
1972
|
+
// later outage starts from one minute again rather than ten.
|
|
1973
|
+
if (this.startServiceRetryTimer) {
|
|
1974
|
+
this.clearTimeout(this.startServiceRetryTimer);
|
|
1975
|
+
this.startServiceRetryTimer = null;
|
|
1976
|
+
}
|
|
1977
|
+
this.startServiceRetryAttempts = 0;
|
|
1971
1978
|
this.log.info(
|
|
1972
1979
|
`Roborock connection ready; ${this.getVacuumList().length} robot(s) available.`
|
|
1973
1980
|
);
|
|
@@ -1989,10 +1996,19 @@ class Roborock {
|
|
|
1989
1996
|
// maintenance, a rate-limited response and plain DNS failure. The stack
|
|
1990
1997
|
// tells the user nothing and the line used to end without saying what
|
|
1991
1998
|
// now happens.
|
|
1992
|
-
this.log.warn(
|
|
1993
|
-
`Could not fetch your Roborock home details: ${error?.message || error}. This is almost always a temporary Roborock cloud or network problem; no robots will load until the next successful start.`
|
|
1994
|
-
);
|
|
1995
1999
|
this.log.debug(error?.stack || String(error));
|
|
2000
|
+
// A CACHED SESSION MEANS THIS PATH IS THE COMMON ONE, AND IT USED TO BE
|
|
2001
|
+
// TERMINAL.
|
|
2002
|
+
//
|
|
2003
|
+
// `getUserData` returns a stored session without touching the network,
|
|
2004
|
+
// so an install that has logged in once never reaches the login retry
|
|
2005
|
+
// above. It reaches `getHomeDetail` instead — and both `homedataInterval`
|
|
2006
|
+
// and `reconnectIntervall` are created AFTER that call, so a failure here
|
|
2007
|
+
// left the plugin with no MQTT client and not one timer that would ever
|
|
2008
|
+
// try again. A Pi that reboots after a power cut while the router is
|
|
2009
|
+
// still coming up registered nothing and sat idle until a human
|
|
2010
|
+
// restarted Homebridge.
|
|
2011
|
+
this.scheduleStartServiceRetry(error);
|
|
1996
2012
|
}
|
|
1997
2013
|
|
|
1998
2014
|
if (callback) {
|
|
@@ -3576,7 +3592,62 @@ class Roborock {
|
|
|
3576
3592
|
return `${duid}:${mapId}`;
|
|
3577
3593
|
}
|
|
3578
3594
|
|
|
3595
|
+
/**
|
|
3596
|
+
* Try `startService` again after a failure that left nothing running.
|
|
3597
|
+
*
|
|
3598
|
+
* Backoff doubles from 1 minute to a 10-minute ceiling and then keeps
|
|
3599
|
+
* going, deliberately without an attempt cap: the failure this recovers
|
|
3600
|
+
* from is "the network was not ready yet", and the device it runs on is
|
|
3601
|
+
* unattended. Giving up would mean a human has to notice. One timer at a
|
|
3602
|
+
* time, unref'd so it can never hold Homebridge open, and cleared on
|
|
3603
|
+
* shutdown.
|
|
3604
|
+
*
|
|
3605
|
+
* @param {unknown} [error]
|
|
3606
|
+
*/
|
|
3607
|
+
scheduleStartServiceRetry(error) {
|
|
3608
|
+
if (this.startServiceRetryTimer || this.bInited) {
|
|
3609
|
+
return;
|
|
3610
|
+
}
|
|
3611
|
+
this.startServiceRetryAttempts = (this.startServiceRetryAttempts || 0) + 1;
|
|
3612
|
+
const delay = Math.min(
|
|
3613
|
+
60000 * Math.pow(2, this.startServiceRetryAttempts - 1),
|
|
3614
|
+
600000
|
|
3615
|
+
);
|
|
3616
|
+
this.log.warn(
|
|
3617
|
+
`Could not fetch your Roborock home details: ${
|
|
3618
|
+
/** @type {any} */ (error)?.message || error
|
|
3619
|
+
}. This is almost always a temporary Roborock cloud or network problem. Retrying in ${Math.round(
|
|
3620
|
+
delay / 60000
|
|
3621
|
+
)} minute(s); no robots will load until it succeeds.`
|
|
3622
|
+
);
|
|
3623
|
+
const timer = setTimeout(() => {
|
|
3624
|
+
this.startServiceRetryTimer = null;
|
|
3625
|
+
if (this.bInited) {
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
this.log.info(
|
|
3629
|
+
`Retrying the Roborock connection (attempt ${this.startServiceRetryAttempts + 1}).`
|
|
3630
|
+
);
|
|
3631
|
+
void Promise.resolve(this.startService()).catch((retryError) => {
|
|
3632
|
+
this.log.debug(
|
|
3633
|
+
`Roborock connection retry failed: ${
|
|
3634
|
+
/** @type {any} */ (retryError)?.message || retryError
|
|
3635
|
+
}`
|
|
3636
|
+
);
|
|
3637
|
+
this.scheduleStartServiceRetry(retryError);
|
|
3638
|
+
});
|
|
3639
|
+
}, delay);
|
|
3640
|
+
if (typeof timer?.unref === "function") {
|
|
3641
|
+
timer.unref();
|
|
3642
|
+
}
|
|
3643
|
+
this.startServiceRetryTimer = timer;
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3579
3646
|
clearTimersAndIntervals() {
|
|
3647
|
+
if (this.startServiceRetryTimer) {
|
|
3648
|
+
this.clearTimeout(this.startServiceRetryTimer);
|
|
3649
|
+
this.startServiceRetryTimer = null;
|
|
3650
|
+
}
|
|
3580
3651
|
if (this.reconnectIntervall) {
|
|
3581
3652
|
this.clearInterval(this.reconnectIntervall);
|
|
3582
3653
|
}
|
|
@@ -3641,7 +3712,7 @@ class Roborock {
|
|
|
3641
3712
|
const homedata = home.data.result;
|
|
3642
3713
|
|
|
3643
3714
|
if (homedata) {
|
|
3644
|
-
this.
|
|
3715
|
+
this.superviseDeviceIntervals();
|
|
3645
3716
|
await this.refreshLocalKeysFromHomeData(homedata);
|
|
3646
3717
|
await this.setStateAsync("HomeData", {
|
|
3647
3718
|
val: JSON.stringify(homedata),
|
|
@@ -4554,19 +4625,45 @@ class Roborock {
|
|
|
4554
4625
|
* their status polling forever. Called from the periodic HomeData refresh
|
|
4555
4626
|
* as a supervisor: restarts intervals when a B01 robot is back online.
|
|
4556
4627
|
*/
|
|
4557
|
-
|
|
4628
|
+
/**
|
|
4629
|
+
* Re-check every robot's polling intervals, from the home-data refresh.
|
|
4630
|
+
*
|
|
4631
|
+
* THIS USED TO SKIP EVERY CLASSIC ROBOT, AND THAT WAS A DEAD END IT COULD
|
|
4632
|
+
* NOT COME BACK FROM.
|
|
4633
|
+
*
|
|
4634
|
+
* `manageDeviceIntervals` stops both intervals when the robot reads as
|
|
4635
|
+
* offline and restarts them when it reads as online. The restart half was
|
|
4636
|
+
* unreachable for a classic robot, because the only other caller sits
|
|
4637
|
+
* inside the `get_status` handler — i.e. it only runs when a status poll
|
|
4638
|
+
* SUCCEEDS, and `getStatusIntervalHandle` is the one thing that drives
|
|
4639
|
+
* those polls. Once the intervals were cleared, nothing was left alive to
|
|
4640
|
+
* notice the robot had come back.
|
|
4641
|
+
*
|
|
4642
|
+
* `onlineChecker` reads the cached home-data snapshot, which lags by up to
|
|
4643
|
+
* one refresh, so the trigger was ordinary: robot drops off wifi, comes
|
|
4644
|
+
* back, its LAN socket reconnects first, the next local poll succeeds, the
|
|
4645
|
+
* stale snapshot still says offline, both intervals die. From then on the
|
|
4646
|
+
* Apple Home tile froze on the last known state until the user pressed a
|
|
4647
|
+
* button or restarted Homebridge.
|
|
4648
|
+
*
|
|
4649
|
+
* This runs on the home-data cadence, which is exactly the clock that
|
|
4650
|
+
* decides `onlineChecker`'s answer, so a robot that comes back is picked up
|
|
4651
|
+
* on the same tick that notices it. The B01 status loop stays B01-only —
|
|
4652
|
+
* that is a different mechanism and it is started unconditionally below.
|
|
4653
|
+
*/
|
|
4654
|
+
superviseDeviceIntervals() {
|
|
4558
4655
|
this.startB01StatusLoop();
|
|
4559
4656
|
|
|
4560
4657
|
for (const duid of this.initializedVacuumDuids) {
|
|
4561
|
-
|
|
4562
|
-
this.getVacuumDeviceInfo(duid, "pv") ===
|
|
4563
|
-
b01Q7Adapter.B01_PROTOCOL_VERSION
|
|
4564
|
-
) {
|
|
4565
|
-
void this.manageDeviceIntervals(duid).catch(() => undefined);
|
|
4566
|
-
}
|
|
4658
|
+
void this.manageDeviceIntervals(duid).catch(() => undefined);
|
|
4567
4659
|
}
|
|
4568
4660
|
}
|
|
4569
4661
|
|
|
4662
|
+
/** @deprecated Kept so existing callers and tests keep working. */
|
|
4663
|
+
superviseB01DeviceIntervals() {
|
|
4664
|
+
this.superviseDeviceIntervals();
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4570
4667
|
/**
|
|
4571
4668
|
* Dedicated status loop for B01/Q7 robots, independent of the fragile
|
|
4572
4669
|
* per-device v1 interval machinery. Ticks every 15 seconds and asks
|