homebridge-roborock-matter 3.11.1 → 3.11.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.11.2
4
+
5
+ **"Attempt 12 this run" counted every run since Homebridge started. So did "failed 10 times in a row", and the once-per-run explanation of why no room could be named was only ever printed on the first run of the process.**
6
+
7
+ Caught on my own a70. It ran a two-room clean from Apple Home, the cloud map channel was timing out that morning, and `get_map_v1` failed all ten times it was asked — ten guaranteed-to-fail cloud requests, 10 seconds of timeout each, spread across one ten-minute clean, and not one room named.
8
+
9
+ The clear that runs at every run boundary exists so nothing leaks into the next run. It dropped the cached room and nothing else. Every counter behind the log lines survived for the lifetime of the process, so three lines were telling you about a window that was not the one they named. A run that fails every attempt is exactly the run that leaves the counters high, and that run never had a cached room to clear in the first place — which is why the leak survived a release that went looking for the same class of thing. 3.11.0 stopped placeholder poses from inflating the miss count; it did not make the count per-run. It is per-run now.
10
+
11
+ **The failing fetch is also no longer retried at live-display cadence for the whole run.** After 2 failures in a row the gap doubles with each further failure, capped at 5 minutes, and drops straight back to the live cadence the moment one succeeds. The first two failures are deliberately not slowed: a single lost frame on a healthy channel must not make a working live room sluggish, the same rule 3.11.1's local-mute limit follows. A streak long enough to have been slowed now says so when it ends, because "failed N times in a row" at warn level had no counterpart and a channel that recovered left the log's last word saying it was broken.
12
+
13
+ Both protocol paths are covered — the classic `get_map_v1` fetch and the Q7/B01 SCMap fetch had the same two defects in the same shape. The test enumerates the rule across every live-room state the plugin keeps rather than the two call sites that happened to be found, so a third path added later fails the test instead of leaking quietly.
14
+
15
+ Not addressed, because there is no measurement to justify it: why the cloud map channel timed out at all. `get_map_v1` has the default 10-second timeout and had been resolving every position on this same robot nine days earlier, so a longer timeout would be a guess. The morning also carried an unrelated cloud `get_prop` timeout on the same robot, which points at the account's cloud rather than at the map request.
16
+
3
17
  ## 3.11.1
4
18
 
5
19
  **The LAN port was open, the robot never answered on it, and the plugin kept asking for the life of the process — 10 seconds thrown away on every poll and every command.**
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. 1134 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. 1151 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
 
@@ -211,13 +211,14 @@ The complete path — robot → plugin → Homebridge → matter.js store — wa
211
211
 
212
212
  - **Diagnostics first:** the plugin settings include per-device connection state, the last cloud/local transport used, a live **Test Local Connection** probe, and a **redacted diagnostics report** you can paste straight into a GitHub issue.
213
213
  - **Robot shows "Updating…" in Apple Home:** remove the robot from Apple Home and pair it again — a stale controller cache from an earlier pairing is the usual cause (tracked upstream in homebridge/homebridge#3951).
214
+ - **Robot shows "No Response" on one Apple device while another shows it fine:** update to **iOS 26.6.1**. On earlier versions the controller could declare its own subscription invalid (Matter status `0x7D INVALID_SUBSCRIPTION`) and then never subscribe again, so one controller kept rendering the accessory while another had no live subscription to it — the same tile, dead on the phone and alive on the Mac at the same moment. The bridge does the right thing in that situation (it drops the dead subscription and re-announces over mDNS), but no Matter device can force a controller to subscribe, so there is nothing to fix on this side. Confirmed fixed on 26.6.1 by the reporter in [#7](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/7). On an older version, "Add Accessory → More Options → Cancel" revives the tile for about a minute, because it forces the controller to re-resolve and briefly re-subscribe.
214
215
  - **Rooms missing for a Q7/B01 robot:** wait for the `B01 rooms for ...` log line, then re-pair once so the Service Area cluster is announced with room data.
215
216
  - **Debug logging needs two switches, not one:** the plugin's own **Debug Mode** only decides whether it _calls_ the debug logger — Homebridge decides whether anything is _printed_, and it suppresses plugin debug output unless Homebridge itself runs with `-D`. Turn on **Homebridge Settings → Homebridge Debug Mode** as well, or the log will look exactly the same as before.
216
217
  - **Startup without network:** the plugin retries the Roborock cloud with increasing backoff (up to 10 attempts) and never crash-loops Homebridge; wrong credentials stop cleanly with a clear log message.
217
218
 
218
219
  ## Contributing
219
220
 
220
- Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 1134 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.
221
+ Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 1151 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.
221
222
 
222
223
  ## Support the project
223
224
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-roborock-matter",
3
- "version": "3.11.1",
3
+ "version": "3.11.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": {
@@ -87,6 +87,57 @@ const LOCAL_MUTE_TIMEOUT_LIMIT = 3;
87
87
  // modest while making the room track the robot closely enough to be useful.
88
88
  const B01_LIVE_ROOM_MIN_FETCH_GAP_MS = 10000;
89
89
 
90
+ // A live-room fetch that keeps failing is a channel that is down for this
91
+ // robot, not a lost frame. The request is heavy, it always rides the cloud
92
+ // (get_map_v1 is a secure request), and every failure costs a full request
93
+ // timeout. Retrying it at live-display cadence for a whole run buys nothing:
94
+ // measured on an a70 whose map channel was timing out, one ten-minute clean
95
+ // spent ten guaranteed-to-fail cloud requests and never named a room. So widen
96
+ // the gap as failures pile up, and drop straight back to the live cadence the
97
+ // moment one answers. The first two failures are deliberately NOT slowed — a
98
+ // single lost frame on a healthy channel must not make a working live display
99
+ // sluggish, the same rule the local-mute limit follows.
100
+ const LIVE_ROOM_FAILURE_BACKOFF_AFTER = 2;
101
+ const LIVE_ROOM_FAILURE_BACKOFF_MAX_MS = 300000; // 5 min
102
+
103
+ /**
104
+ * Required gap before the next live-room fetch attempt, given how many
105
+ * attempts in a row have already failed.
106
+ * @param {number} [consecutiveFailures]
107
+ * @returns {number}
108
+ */
109
+ function liveRoomFetchGapMs(consecutiveFailures) {
110
+ const over = Number(consecutiveFailures) - LIVE_ROOM_FAILURE_BACKOFF_AFTER;
111
+ if (!Number.isFinite(over) || over <= 0) {
112
+ return B01_LIVE_ROOM_MIN_FETCH_GAP_MS;
113
+ }
114
+ return Math.min(
115
+ B01_LIVE_ROOM_MIN_FETCH_GAP_MS * 2 ** over,
116
+ LIVE_ROOM_FAILURE_BACKOFF_MAX_MS
117
+ );
118
+ }
119
+
120
+ /**
121
+ * Zero the counters that describe THIS run. clearLiveRoomForDevice runs at
122
+ * every run boundary and its stated job is to stop state leaking into the next
123
+ * run, but it used to drop only the cached room. Everything else survived, so
124
+ * a line reading "attempt N this run" counted every run since Homebridge
125
+ * started, the placeholder explanation meant to be said once per run was only
126
+ * ever visible on the very first run of the process, and "failed N times in a
127
+ * row" could greet a new run's first failure with N already at 5. Resetting
128
+ * has to happen even when no room was ever resolved — a run that failed every
129
+ * attempt is precisely the run that left the counters high.
130
+ * @param {{consecutiveFailures?: number, unresolvedPoseCount?: number, placeholderReported?: boolean} | null | undefined} liveState
131
+ */
132
+ function resetLiveRoomRunCounters(liveState) {
133
+ if (!liveState) {
134
+ return;
135
+ }
136
+ liveState.consecutiveFailures = 0;
137
+ liveState.unresolvedPoseCount = 0;
138
+ liveState.placeholderReported = false;
139
+ }
140
+
90
141
  // B01/Q7 status cadence. These were literals in three places — the two gap
91
142
  // values, the loop interval, and a hand-written startup line quoting all
92
143
  // three. 3.2.0 changed the idle gap from 45s to 25s and the startup line kept
@@ -4602,7 +4653,10 @@ class Roborock {
4602
4653
  if (liveState.inflight) {
4603
4654
  return liveState.inflight;
4604
4655
  }
4605
- if (Date.now() - liveState.lastAttemptAt < B01_LIVE_ROOM_MIN_FETCH_GAP_MS) {
4656
+ if (
4657
+ Date.now() - liveState.lastAttemptAt <
4658
+ liveRoomFetchGapMs(liveState.consecutiveFailures)
4659
+ ) {
4606
4660
  return liveState.current;
4607
4661
  }
4608
4662
  liveState.lastAttemptAt = Date.now();
@@ -4644,7 +4698,7 @@ class Roborock {
4644
4698
 
4645
4699
  const resolution2 = b01Q7Adapter.describeLiveRoomResolution(parsed);
4646
4700
  const roomId = resolution2.roomId;
4647
- liveState.consecutiveFailures = 0;
4701
+ this.noteLiveRoomFetchRecovered(duid, liveState);
4648
4702
 
4649
4703
  if (roomId === null) {
4650
4704
  // Debug-only used to make this invisible, and it is the single most
@@ -4749,6 +4803,27 @@ class Roborock {
4749
4803
  return this._b01LiveRoomState?.get(duid)?.current || null;
4750
4804
  }
4751
4805
 
4806
+ /**
4807
+ * Close the loop on a failure streak. "Live-room map fetch has failed N
4808
+ * times in a row" had no counterpart, so a channel that came back left the
4809
+ * last word in the log saying it was broken. Said exactly when the backoff
4810
+ * had begun slowing the fetch down, so the message and the behaviour it
4811
+ * reports on cannot drift apart.
4812
+ * @param {string} duid
4813
+ * @param {{consecutiveFailures?: number} | null | undefined} liveState
4814
+ */
4815
+ noteLiveRoomFetchRecovered(duid, liveState) {
4816
+ const failures = liveState?.consecutiveFailures || 0;
4817
+ if (failures > LIVE_ROOM_FAILURE_BACKOFF_AFTER) {
4818
+ this.log.info(
4819
+ `Live-room map fetch for ${this.describeDevice(duid)} recovered after ${failures} failed attempt(s).`
4820
+ );
4821
+ }
4822
+ if (liveState) {
4823
+ liveState.consecutiveFailures = 0;
4824
+ }
4825
+ }
4826
+
4752
4827
  /** @param {string} duid */
4753
4828
  clearB01LiveRoom(duid) {
4754
4829
  const liveState = this._b01LiveRoomState?.get(duid);
@@ -4758,6 +4833,7 @@ class Roborock {
4758
4833
  );
4759
4834
  liveState.current = null;
4760
4835
  }
4836
+ resetLiveRoomRunCounters(liveState);
4761
4837
  }
4762
4838
 
4763
4839
  /**
@@ -4805,6 +4881,7 @@ class Roborock {
4805
4881
  );
4806
4882
  liveState.current = null;
4807
4883
  }
4884
+ resetLiveRoomRunCounters(liveState);
4808
4885
  }
4809
4886
 
4810
4887
  /**
@@ -4876,7 +4953,7 @@ class Roborock {
4876
4953
  // microseconds).
4877
4954
  const segmentId =
4878
4955
  RRMapParser.resolveLiveSegmentFromMapBuffer(mapBuffer);
4879
- liveState.consecutiveFailures = 0;
4956
+ this.noteLiveRoomFetchRecovered(duid, liveState);
4880
4957
 
4881
4958
  if (segmentId === null) {
4882
4959
  this.log.debug(