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 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. 1655 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. 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 1655 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.
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.19.8",
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.superviseB01DeviceIntervals();
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
- superviseB01DeviceIntervals() {
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
- if (
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