homebridge-roborock-matter 3.32.0 → 3.33.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,52 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.33.0
4
+
5
+ **The one method users name most often was the one method the new register could not see.**
6
+
7
+ ### Two users asked the same question, and one of them had the answer
8
+
9
+ [@DSimeone1989](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/22) and [@Marrand](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/24) run different robots — an `a144` and an `a51` — and both pasted a line with two method names in it that disagree. Marrand asked it outright: _"some messages say `Failed to execute get_room_mapping`, but the actual error underneath says `method get_status timed out`. I'm not sure whether that's expected, or whether the method name in that log line is misleading."_
10
+
11
+ ```
12
+ Failed to execute get_room_mapping on robot Rocky (roborock.vacuum.a144):
13
+ Error: Cloud request with id 5563 with method get_status timed out after 10
14
+ seconds … 483 similar warning(s) across get_status (359), get_room_mapping
15
+ (119) … were suppressed.
16
+ ```
17
+
18
+ The name is not misleading. It is the diagnosis, and it is the same one [#14](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/14) produced on a B01 robot in 3.11.0: the classic room-mapping poll opens by fetching `get_status`, purely to read `map_status` and derive a floor number. `get_room_mapping` is the caller's label. `get_status` is what went on the wire.
19
+
20
+ 3.11.0 fixed that for B01 robots by returning early. On classic robots the request is answerable, so it was left alone as merely wasteful. It was not merely wasteful.
21
+
22
+ ### Why that put the poll permanently out of the register's reach
23
+
24
+ 3.32.0 moved the give-up register to the message layer, because that is the only place that knows whether a reply arrived. That layer sees **every** request, so a robot/method pair is counted only once a caller that can skip it has claimed it with `govern()`. `get_status` is never claimable — the tile lives on it.
25
+
26
+ `pollParameter` claims `get_room_mapping`. The wire saw `get_status`. The register discarded the timeout, exactly as designed, and the claimed pair recorded nothing — not a failure, not an answer. **Six strikes were unreachable.**
27
+
28
+ That is why Marrand's 3.32.0 report lists `get_multi_maps_list`, `get_consumable`, `get_server_timer`, `get_timer`, `get_carpet_mode`, `get_carpet_clean_mode` and `get_water_box_custom_mode` all reaching their cooldown — and not `get_room_mapping`, the method with 119 suppressed warnings in #22.
29
+
30
+ And there is a second half I had not seen. When `get_status` is the request that times out — which is DSimeone1989's case, 359 of them — the branch threw at the first `await`. **`get_room_mapping` was never sent at all.** The robot was not being asked the question it was failing to answer; it was being asked a different one, and the failure was filed under a name the register was built to ignore.
31
+
32
+ ### The fix is to not make the request
33
+
34
+ The floor number is already known. The status poll runs on its own interval and stores `map_status` — already right-shifted — and `app_segment_clean` has read the floor from there since long before any of this. The room-mapping poll now reads the same value.
35
+
36
+ So on a classic robot the poll puts exactly one request on the wire, `get_room_mapping`, under its own name:
37
+
38
+ - One fewer cloud round-trip per poll cycle per robot, each with its own ten-second timeout.
39
+ - The register can count it. Six unanswered polls now stop the flood the same way they already stopped the other seven methods.
40
+ - When the robot is silent, it is silent about the thing it was actually asked.
41
+
42
+ `get_status` is still fetched when no status has landed yet — the first cycle after a restart starts both intervals together. Filing rooms under a guessed floor would hide them from `app_segment_clean`, which looks them up under the real one, so asking once is the lesser cost. A cached floor of `0`, the normal case for a home with one map, is a floor and not a missing value.
43
+
44
+ ### The rule, pinned
45
+
46
+ The test is written against the class rather than this branch: **a poll that claims a method must put that method on the wire first.** Any future branch that fronts a governed poll with a different request re-opens this hole, and now fails in CI rather than in someone's log six weeks later.
47
+
48
+ 2054 tests, 9 new, all 5 of the behavioural ones red against 3.32.0.
49
+
3
50
  ## 3.32.0
4
51
 
5
52
  **The give-up register had never counted a single poll failure. Not one, in two releases that were built around it.**
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. 2045 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. 2054 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
 
@@ -264,7 +264,7 @@ The complete path — robot → plugin → Homebridge → matter.js store — wa
264
264
 
265
265
  ## Contributing
266
266
 
267
- Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 2045 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.
267
+ Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 2054 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.
268
268
 
269
269
  ## Support the project
270
270
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-roborock-matter",
3
- "version": "3.32.0",
3
+ "version": "3.33.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": {
@@ -55,6 +55,33 @@ function buildForwardedRequestOptions(options = {}) {
55
55
  return requestOptions;
56
56
  }
57
57
 
58
+ /**
59
+ * The floor number the last `get_status` reported, or null if none has landed.
60
+ *
61
+ * The value in this state is ALREADY right-shifted — the status handler does
62
+ * `map_status >> 2` before storing it — so it is returned as-is. Shifting it a
63
+ * second time would file every room on floor 1 under floor 0.
64
+ *
65
+ * @param {{ getStateAsync?: (id: string) => {val?: unknown} | null | undefined }} adapter
66
+ * @param {string} duid
67
+ * @returns {number | null}
68
+ */
69
+ function readCachedRoomFloor(adapter, duid) {
70
+ const cached = adapter.getStateAsync?.(
71
+ `Devices.${duid}.deviceStatus.map_status`
72
+ );
73
+ const floor = Number(cached?.val);
74
+
75
+ // `Number(null)` is 0 and `Number(undefined)` is NaN, so the emptiness test
76
+ // has to be on the value rather than on the conversion: a real floor of 0 is
77
+ // the common case for a home with one map.
78
+ if (cached?.val === undefined || cached?.val === null) {
79
+ return null;
80
+ }
81
+
82
+ return Number.isFinite(floor) ? floor : null;
83
+ }
84
+
58
85
  /**
59
86
  * Render a `get_status` value for a log line. The old per-attribute warning
60
87
  * interpolated the raw value, so an object arrived as the useless
@@ -833,9 +860,9 @@ class vacuum {
833
860
  // Room data on B01/Q7 robots travels over the protobuf map channel,
834
861
  // and `get_room_mapping` itself is answered from the dialect's neutral
835
862
  // table without touching the network — so this branch looked free.
836
- // It is not: it opens by fetching `get_status` to read `map_status`,
863
+ // It was not: it opened by fetching `get_status` to read `map_status`,
837
864
  // a v1-only field that Q7 status dictionaries have never carried, and
838
- // on B01 `get_status` translates to a real `prop.get`. That is one
865
+ // on B01 `get_status` translates to a real `prop.get`. That was one
839
866
  // cloud round-trip per poll cycle per robot spent on an answer this
840
867
  // code cannot read — reported under the caller's label, which is why
841
868
  // #14's log line names `get_room_mapping` but times out on `prop.get`.
@@ -843,12 +870,36 @@ class vacuum {
843
870
  return;
844
871
  }
845
872
 
846
- const deviceStatus = await sendParameterRequest("get_status", []);
847
- const mapStatus = Array.isArray(deviceStatus)
848
- ? deviceStatus[0]?.["map_status"]
849
- : undefined;
850
- // to get the currently selected map perform bitwise right shift
851
- const roomFloor = typeof mapStatus === "number" ? mapStatus >> 2 : -1;
873
+ // On classic robots the request is answerable, so 3.11.0 left it in
874
+ // place. It still should not be made: the status poll runs on its own
875
+ // interval and has already stored this exact number, and
876
+ // `app_segment_clean` has always read the floor from there rather
877
+ // than asking again.
878
+ //
879
+ // AND IT COST MORE THAN A ROUND-TRIP. The give-up register counts the
880
+ // method that goes on the WIRE, and only for pairs a caller claimed
881
+ // with `govern()`. `pollParameter` claims `get_room_mapping`; the
882
+ // wire saw `get_status`, which the register must never govern. So the
883
+ // claimed pair recorded nothing and could never reach six strikes —
884
+ // the one method #22 and #24 name most often was the one the new
885
+ // register could not see. Worse, when `get_status` was the request
886
+ // timing out, this threw before `get_room_mapping` was ever sent, so
887
+ // the robot was not even asked the question it was failing.
888
+ let roomFloor = readCachedRoomFloor(this.adapter, duid);
889
+
890
+ if (roomFloor === null) {
891
+ // No status has landed yet — the first cycle after a restart starts
892
+ // both intervals together. Ask, rather than file the rooms under a
893
+ // floor we guessed: `app_segment_clean` looks them up under the
894
+ // real one and would not find them.
895
+ const deviceStatus = await sendParameterRequest("get_status", []);
896
+ const mapStatus = Array.isArray(deviceStatus)
897
+ ? deviceStatus[0]?.["map_status"]
898
+ : undefined;
899
+ // to get the currently selected map perform bitwise right shift
900
+ roomFloor = typeof mapStatus === "number" ? mapStatus >> 2 : -1;
901
+ }
902
+
852
903
  const mappedRooms = await sendParameterRequest("get_room_mapping", []);
853
904
  if (typeof this.adapter.updateRoomMappingCache === "function") {
854
905
  this.adapter.updateRoomMappingCache(duid, roomFloor, mappedRooms);