homebridge-roborock-matter 3.36.0 → 3.38.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 +70 -0
- package/README.md +2 -2
- package/dist/matter_vacuum_accessory.js +23 -2
- package/dist/matter_vacuum_accessory.js.map +1 -1
- package/dist/ui/index.js +3 -0
- package/dist/ui/index.js.map +1 -1
- package/docs/mqtt-session-diagnostics.md +78 -0
- package/homebridge-ui/public/index.js +7 -0
- package/package.json +1 -1
- package/roborockLib/lib/messageQueueHandler.js +22 -1
- package/roborockLib/lib/mqttSessionDiagnostics.js +178 -0
- package/roborockLib/lib/roborock_mqtt_connector.js +101 -8
- package/roborockLib/roborockAPI.js +29 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.38.0
|
|
4
|
+
|
|
5
|
+
**Two log lines that could not tell two causes apart, and one of them was accusing this plugin of a bug that was not there.**
|
|
6
|
+
|
|
7
|
+
Nothing about how the plugin talks to a robot changes in this release. What changes is what it says when something fails, in the two places where the sentence it printed was the reason a diagnosis stalled.
|
|
8
|
+
|
|
9
|
+
### "Please report this — it is a bug here" was wrong (#28)
|
|
10
|
+
|
|
11
|
+
[@CooperCGN](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/28)'s log says, about his S8 Pro Ultra:
|
|
12
|
+
|
|
13
|
+
> NOTE: 30 map reply frame(s) for this robot were received and then discarded by the plugin (addressed-elsewhere: 30), so the robot IS answering and this side is throwing it away. Please report this — it is a bug here, not on your robot.
|
|
14
|
+
|
|
15
|
+
It is not a bug here. `addressed-elsewhere` means the protocol-301 frame carried a different controller's endpoint, and the endpoint identifies a **controller on the account**, not this plugin: the Roborock app open on a phone, a second Homebridge, a voice assistant skill each have their own, and all of them share the robot's MQTT topic. Their replies reach this handler and are discarded, which is correct — they were never ours. His 30 frames arrived while a scheduled clean was being watched in the app.
|
|
16
|
+
|
|
17
|
+
One case genuinely is ours: our own endpoint being wrong. It is told apart by a measurement the counter was missing — the frames that **were** addressed to us. If the key were wrong, nothing would ever be, so one accepted frame acquits it and convicts the other controller instead.
|
|
18
|
+
|
|
19
|
+
The give-up line now distinguishes three outcomes instead of asserting one:
|
|
20
|
+
|
|
21
|
+
- Frames addressed to this plugin and still discarded → unchanged; that is a bug here and still says so.
|
|
22
|
+
- Frames for another controller, with frames of ours accepted → named as another controller's, with the Roborock app given as the usual cause, and no bug report asked for.
|
|
23
|
+
- Frames for another controller with **nothing** ever accepted → that is the real bug's shape, and the line now points at the account key rather than at the robot.
|
|
24
|
+
|
|
25
|
+
`accepted` rides along in the diagnostic report's `discardedReplyFrames`, so the next report settles it without a round trip.
|
|
26
|
+
|
|
27
|
+
### A refused room selection now says what it refused (#24)
|
|
28
|
+
|
|
29
|
+
[@Marrand](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/24) can add rooms one at a time and the selection holds; selecting all 8 reverts it to the 2 rooms from an earlier clean, repeatably. The only log line covering it read:
|
|
30
|
+
|
|
31
|
+
> Matter service area selection request for S8: none.
|
|
32
|
+
|
|
33
|
+
Three different causes print that: the controller really did send an empty list, the controller sent 8 ids and none of them could be read, or the set spanned 2 Roborock maps and was refused — and a refusal **is** a revert, because a controller that gets `InvalidSet` keeps the selection it last had accepted. At one run a week, that is a week per guess.
|
|
34
|
+
|
|
35
|
+
- The request line now names what arrived as well as what was understood: no list, an empty list, or _n_ entries of which _m_ could not be read as an area id.
|
|
36
|
+
- The multi-map refusal names the maps and the areas, and says in words that the selection is refused rather than cleared, so the controller will keep showing the previous one.
|
|
37
|
+
|
|
38
|
+
### Tests
|
|
39
|
+
|
|
40
|
+
2,201 tests, 14 more than 3.37.0, including the endpoint guard driven through the real protocol-301 handler for both an accepted frame and another controller's.
|
|
41
|
+
|
|
42
|
+
## 3.37.0
|
|
43
|
+
|
|
44
|
+
**When a cloud request times out, the diagnostics report can now say whether the MQTT session was actually carrying anything at the time.**
|
|
45
|
+
|
|
46
|
+
All of this release is [@pponce](https://github.com/mathiashornbek/homebridge-roborock-matter/pull/29)'s work, in [#29](https://github.com/mathiashornbek/homebridge-roborock-matter/pull/29), the first of a four-part series. It is deliberately passive: nothing here reconnects, gates a request, changes transport selection, or touches the unanswered-method breaker. It only writes down what was observed.
|
|
47
|
+
|
|
48
|
+
### Why
|
|
49
|
+
|
|
50
|
+
A cloud timeout has always been ambiguous. `MQTT connection state: true` says the link believes it is up, which is exactly what it says when the link is up and delivering nothing — the failure #24, #27 and #28 all run into. Until now there was no way to tell those apart after the fact.
|
|
51
|
+
|
|
52
|
+
The snapshot now separates things that used to be one word:
|
|
53
|
+
|
|
54
|
+
- **`connected` and `subscriptionAcknowledged` are different claims.** A session can be connected with no usable subscription behind it.
|
|
55
|
+
- **Inbound is measured in 4 stages** — the raw MQTT callback, attribution to a known robot, successful decoding, and correlation to a pending request. A link that delivers frames nobody can attribute looks very different from one that delivers nothing, and both used to read as silence.
|
|
56
|
+
- **`rawSilenceDuringRequest`** records whether _any_ raw MQTT callback arrived between sending an eligible read and its timeout. This is a claim about the session, not about one robot, so unlike the two-robot correlation rule it works on a single-robot account — which is what #22 and #24 both are.
|
|
57
|
+
|
|
58
|
+
The report carries ages and counts only: no robot IDs, no topics, no credentials, no payloads.
|
|
59
|
+
|
|
60
|
+
### Found in review
|
|
61
|
+
|
|
62
|
+
Two things were wrong when this was first proposed, and both were the same shape as faults in 3.30.0 and 3.31.0 — a value that is published but never reaches the disk the reader reads from:
|
|
63
|
+
|
|
64
|
+
- **The snapshot never persisted.** `MqttSessionDiagnostics` was published through `setStateAsync` but was in neither `PERSISTED_STATE_IDS` nor `DEBOUNCED_PERSIST_IDS`, while the settings page reads it from `storagePath`. The new block would have rendered in tests and never in production. The suite was green because the UI test wrote the file itself. Both sets now contain it, and the shutdown flush was moved to _after_ transport teardown so the final disconnected snapshot reaches disk.
|
|
65
|
+
- **A subscription could never be acknowledged after a reconnect.** mqtt.js resubscribes on its own, before our handler runs, so our `subscribe` callback comes back with no error and an empty grant list. Read as a result, that meant every reconnected session reported "subscription not acknowledged" forever. Acknowledgement is now taken from real SUBACK packets on the client's `packetreceive` event, and an empty grant list is treated as the no-op it is. A SUBACK whose grants are refusals still counts as unacknowledged.
|
|
66
|
+
|
|
67
|
+
The second one was only visible because the test was rewritten to drive the real mqtt.js client against a loopback broker instead of a mock. The mock had been asserting the behaviour it was built to expect.
|
|
68
|
+
|
|
69
|
+
### Tests
|
|
70
|
+
|
|
71
|
+
2,187 tests, 36 more than 3.36.0, including a regression that drops the socket mid-session, withholds the second SUBACK, and verifies the session becomes acknowledged only when that packet actually arrives.
|
|
72
|
+
|
|
3
73
|
## 3.36.0
|
|
4
74
|
|
|
5
75
|
**The LAN connection now asks the robot which protocol it speaks, the way python-roborock always has, and a cloud session that has gone quiet is restarted.**
|
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. 2201 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
|
|
267
|
+
Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 2201 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
|
|
|
@@ -3664,7 +3664,24 @@ class RoborockMatterVacuumAccessory {
|
|
|
3664
3664
|
const supportedAreas = new Map(this.getMatterServiceAreas().map((area) => [area.areaId, area]));
|
|
3665
3665
|
const selectedAreas = this.normalizeMatterAreaIds(newAreas);
|
|
3666
3666
|
const unsupportedArea = selectedAreas.find((areaId) => !supportedAreas.has(areaId));
|
|
3667
|
-
|
|
3667
|
+
// WHAT ARRIVED, NOT ONLY WHAT WAS UNDERSTOOD — #24.
|
|
3668
|
+
//
|
|
3669
|
+
// The line used to print the normalised ids alone, so "the controller
|
|
3670
|
+
// cleared the selection" and "the controller sent eight ids and this
|
|
3671
|
+
// plugin read none of them" were the same five characters: `none`. #24
|
|
3672
|
+
// reported a selection that reverts when all eight rooms are picked, and
|
|
3673
|
+
// the only log line covering it could not say which of the two had
|
|
3674
|
+
// happened — a week per guess, at one run a week.
|
|
3675
|
+
const requestedCount = Array.isArray(newAreas) ? newAreas.length : null;
|
|
3676
|
+
const unreadable = requestedCount === null ? 0 : requestedCount - selectedAreas.length;
|
|
3677
|
+
const requestDetail = requestedCount === null
|
|
3678
|
+
? " (the controller sent no area list)"
|
|
3679
|
+
: requestedCount === 0
|
|
3680
|
+
? " (the controller sent an empty list)"
|
|
3681
|
+
: unreadable > 0
|
|
3682
|
+
? ` (the controller sent ${requestedCount} entr${requestedCount === 1 ? "y" : "ies"}, ${unreadable} of which could not be read as an area id)`
|
|
3683
|
+
: "";
|
|
3684
|
+
this.platform.log.info(`Matter service area selection request for ${this.getVacuumName()}: ${selectedAreas.join(", ") || "none"}${requestDetail}.`);
|
|
3668
3685
|
if (unsupportedArea !== undefined) {
|
|
3669
3686
|
return {
|
|
3670
3687
|
status: SERVICE_AREA_SELECT_STATUS.UNSUPPORTED_AREA,
|
|
@@ -3675,7 +3692,11 @@ class RoborockMatterVacuumAccessory {
|
|
|
3675
3692
|
.map((areaId) => supportedAreas.get(areaId))
|
|
3676
3693
|
.filter((area) => area !== undefined));
|
|
3677
3694
|
if (selectedMapIds.length > 1) {
|
|
3678
|
-
|
|
3695
|
+
// A refused selection is not a cleared one: the controller keeps the set
|
|
3696
|
+
// it last had accepted. That is precisely the "it falls back to the two
|
|
3697
|
+
// rooms I cleaned this morning" of #24, and the old warning described
|
|
3698
|
+
// neither the cause (which maps) nor the visible effect (the revert).
|
|
3699
|
+
this.platform.log.warn(`Ignoring Matter service area selection spanning ${selectedMapIds.length} Roborock maps for ${this.getVacuumName()} (maps ${selectedMapIds.join(", ")}; areas ${selectedAreas.join(", ")}); select areas from one map at a time. The selection is refused, not cleared, so the controller will keep showing the previous selection.`);
|
|
3679
3700
|
return {
|
|
3680
3701
|
status: SERVICE_AREA_SELECT_STATUS.INVALID_SET,
|
|
3681
3702
|
statusText: "Select service areas from only one Roborock map at a time.",
|