homebridge-roborock-matter 3.2.0 → 3.3.1
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 +22 -0
- package/README.md +27 -13
- package/config.schema.json +6 -0
- package/dist/matter_vacuum_accessory.js +266 -5
- package/dist/matter_vacuum_accessory.js.map +1 -1
- package/homebridge-ui/public/index.html +15 -1
- package/homebridge-ui/public/index.js +39 -0
- package/package.json +1 -1
- package/roborockLib/lib/b01Q7Adapter.js +45 -4
- package/roborockLib/lib/deviceFeatures.js +1 -0
- package/roborockLib/roborockAPI.js +124 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.3.1
|
|
4
|
+
|
|
5
|
+
Two field reports arrived within an hour of 3.3.0 and both came down to the same thing: the log and the diagnostics report were answering questions nobody had asked while staying silent on the one that mattered. This release is almost entirely about making the plugin legible.
|
|
6
|
+
|
|
7
|
+
- **The live-room log said "the robot may be between rooms" for four different problems.** The resolver returns nothing when the map payload has no header, when it carries no robot position, when it carries no room outlines, or when the position genuinely falls outside every outline — and only the last of those is "between rooms". A day of field logs produced 51 of these messages, every one of them asserting a cause that may not have been the cause. Each case is now named, with the number of outlines in the map and the computed position cell, so a coordinate problem is visible in the log instead of needing a debug build.
|
|
8
|
+
- **The message identifying a misbehaving robot was the one you could not read.** The failure line printed a raw 22-character duid while the success line beside it printed the robot's name. In a three-robot house that is the difference between a usable log and a wall of identifiers. The live-room, B01 status and B01 room lines all use the name now.
|
|
9
|
+
- **The attempt counter appeared to reset at random.** A robot resolving back into the room it was already in silently zeroed the miss counter without logging anything, so "attempt 15" was followed by "attempt 5" with nothing in between. Re-entering a known room after a run of misses now says so.
|
|
10
|
+
- **The startup line announced a cadence the code stopped using.** 3.2.0 changed the at-rest B01 poll from 45 s to 25 s and left the message advertising 45 s — misleading in exactly the area it was meant to explain. The cadence values are named constants now and the message is derived from them, so they cannot drift apart again.
|
|
11
|
+
- **The diagnostics report now lists which Apple Home features are switched on.** A report that omits them cannot answer "why doesn't Apple Home show this?", which is the first question most of them are sent to answer — and it cost a full round-trip with a user who had run the test correctly.
|
|
12
|
+
- **The Matter publish line names the robot, and reports a fault when one is being published.** Previously it printed a duid and said nothing about faults, so there was no way to tell whether Apple Home was showing nothing because the plugin sent nothing.
|
|
13
|
+
- **`operationalError` is no longer part of the accessory's registration snapshot.** It is published on the first runtime update instead, seconds later. Matter commissions the endpoint from that snapshot, and 1.4.61 removed the plugin's fault write precisely because Apple Home reacted badly to it there — so a robot that happens to be faulted when Homebridge starts can no longer change what gets commissioned. The mandatory Matter default covers the gap.
|
|
14
|
+
|
|
15
|
+
## 3.3.0
|
|
16
|
+
|
|
17
|
+
A robot that has stopped because it is wedged under the sofa has always looked exactly like a robot that finished the job: **Ready**. This release lets the plugin say what is actually wrong — asked for by Wazza151 in [#5](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/5), whose previous Matter bridge showed him when the clean-water tank ran empty.
|
|
18
|
+
|
|
19
|
+
- **New setting: Report faults in Apple Home** (off by default). The robot's own faults — stuck, blocked brush or wheel, missing dust bin, flat battery, unreachable dock — are published as the Matter Error state with the Roborock description attached, instead of being flattened to Ready. Dock and tank conditions — clean-water tank empty, waste-water tank full, dust bag missing, air duct blocked, mop-wash tank full — are published as a Matter fault too, but deliberately do **not** force the Error state: a robot whose waste-water tank is full can still vacuum, and an accessory in Error may be refused a Start command by the controller.
|
|
20
|
+
- **The Error state was never gated for a reason.** `ERROR` (3) is a member of even the basic advertised operational state list, so publishing it was always legal — it was being rewritten to `STOPPED` alongside the states that genuinely did need a gate. That is why no released version has ever shown a Roborock fault in Apple Home.
|
|
21
|
+
- **A detached water tank or mop pad is not a fault.** Both are the normal, correct configuration for a vacuum-only run, so reporting them would leave a permanent warning on every dry robot's tile. They are read, and deliberately ignored.
|
|
22
|
+
- **The fault detail can never cost you the tile.** `operationalError` travels in the same cluster payload as the operational state, so a Matter build that refuses the attribute would otherwise freeze Cleaning/Docked along with it — the reason the explicit write was removed back in 1.4.61. If the write is rejected, the plugin immediately re-publishes without it, logs a warning naming the reason, and stops sending it for the rest of the session. An endpoint that is merely still starting up keeps its normal retry and does not disable the feature.
|
|
23
|
+
- **Diagnostics no longer truncate away the answer.** A Roborock status payload runs to about fifty fields and the export kept the first thirty — which are largely housekeeping, while the twenty it dropped included `dock_error_status`, the single field a question about the dock's water tanks turns on. The fields that matter for a fault report are now always kept, however far down the payload they sit, with the size cap otherwise unchanged and secret redaction untouched.
|
|
24
|
+
|
|
3
25
|
## 3.2.0
|
|
4
26
|
|
|
5
27
|
Field feedback on the Q7 series: the live room in Apple Home lagged badly behind the robot. One run took 90 seconds to name the first room; another took seven minutes.
|
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. 380 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
|
|
|
@@ -75,7 +75,7 @@ Progress stays honest: a room is only shown as _completed_ once the robot was ac
|
|
|
75
75
|
<details>
|
|
76
76
|
<summary>How it works under the hood</summary>
|
|
77
77
|
|
|
78
|
-
While a robot is actively cleaning, the plugin fetches its live position from the map channel (
|
|
78
|
+
While a robot is actively cleaning, the plugin fetches its live position from the map channel (the first room of a run goes out immediately, then ~10 s apart, active runs only, nothing while docked or paused) and publishes the room it is inside as the Matter Service Area `currentArea`. Both robot generations are covered: **B01/Q7** robots via the encrypted SCMap protobuf (position ray-cast against per-room boundary outlines), **classic S/Q-series** robots via the RRMap segment grid (position resolved against per-pixel room segments — a single-byte lookup on the raw map buffer, ~1 µs per check).
|
|
79
79
|
|
|
80
80
|
</details>
|
|
81
81
|
|
|
@@ -102,17 +102,31 @@ The clean mode follows the robot as well: start a vacuum+mop or mop-only clean f
|
|
|
102
102
|
|
|
103
103
|
Everything is configurable from the Homebridge UI. The essentials:
|
|
104
104
|
|
|
105
|
-
| Option | Default | What it does
|
|
106
|
-
| ------------------------------- | ------- |
|
|
107
|
-
| `email` / password | — | Your Roborock app account (2FA handled in the UI; the session token is stored encrypted)
|
|
108
|
-
| `skipDevices` | — | Comma-separated device IDs the plugin should ignore
|
|
109
|
-
| `enableMatterServiceArea` | `true` | Room/map selection in Apple Home
|
|
110
|
-
| `enableLiveRoomTracking` | `true` | Live current-room from the robot's map position while cleaning
|
|
111
|
-
| `enableMatterCleanMode` | `true` | Vacuum / Mop / Vacuum + Mop mode selection
|
|
112
|
-
| `enableFanPowerCleanModes` | `false` | Adds Quiet / Balanced / Turbo / Max (and Max+ on Q7) suction modes to the Matter mode list. **Re-pair the robot once after toggling** — Matter locks the mode list at pairing
|
|
113
|
-
| `enableMatterPowerSource` | `true` | Battery cluster
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
105
|
+
| Option | Default | What it does |
|
|
106
|
+
| ------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
107
|
+
| `email` / password | — | Your Roborock app account (2FA handled in the UI; the session token is stored encrypted) |
|
|
108
|
+
| `skipDevices` | — | Comma-separated device IDs the plugin should ignore |
|
|
109
|
+
| `enableMatterServiceArea` | `true` | Room/map selection in Apple Home |
|
|
110
|
+
| `enableLiveRoomTracking` | `true` | Live current-room from the robot's map position while cleaning |
|
|
111
|
+
| `enableMatterCleanMode` | `true` | Vacuum / Mop / Vacuum + Mop mode selection |
|
|
112
|
+
| `enableFanPowerCleanModes` | `false` | Adds Quiet / Balanced / Turbo / Max (and Max+ on Q7) suction modes to the Matter mode list. **Re-pair the robot once after toggling** — Matter locks the mode list at pairing |
|
|
113
|
+
| `enableMatterPowerSource` | `true` | Battery cluster |
|
|
114
|
+
| `enableMatterFaultReporting` | `false` | Say why the robot needs attention instead of showing Ready — stuck, empty clean-water tank, full waste-water tank, missing dust bag ([details](#why-the-robot-needs-attention)) |
|
|
115
|
+
| `cloudOnlyMode` | `false` | Skip local TCP entirely and use the cloud for everything |
|
|
116
|
+
| `transientWarningThrottleHours` | `6` | How often recurring transient-timeout warnings may repeat (0 = only in debug) |
|
|
117
|
+
|
|
118
|
+
## Why the robot needs attention
|
|
119
|
+
|
|
120
|
+
By default a robot that has stopped for any reason shows as **Ready** in Apple Home — whether it finished the job or is wedged under the sofa. Turning on **Report faults in Apple Home** changes that: the robot's own faults are published as the Matter Error state, and the condition behind them is published as a Matter fault so the Home app can say what is wrong rather than just that something is.
|
|
121
|
+
|
|
122
|
+
What gets reported:
|
|
123
|
+
|
|
124
|
+
- **The robot's own faults** — stuck, a blocked brush or wheel, a missing dust bin, a flat battery, a dock it cannot reach. These put the tile into Error, because the robot genuinely cannot work.
|
|
125
|
+
- **Dock and tank conditions** — an empty clean-water tank, a full waste-water tank, a missing dust bag, a blocked air duct, a mop-wash tank that needs emptying. These are reported as a fault but deliberately **do not** put the tile into Error: a robot whose waste-water tank is full can still vacuum, and an accessory in Error may be refused a Start command by the controller.
|
|
126
|
+
|
|
127
|
+
What is deliberately **not** reported: a detached water tank or mop pad. That is the normal, correct configuration for a vacuum-only run, so treating it as a fault would leave a permanent warning on every dry robot's tile.
|
|
128
|
+
|
|
129
|
+
It is off by default for two reasons: a robot reporting Error may be refused a Start command from Apple Home, and the fault attribute has a history with Apple Home commissioning. If Matter ever refuses the attribute, the plugin drops it by itself and logs a warning rather than letting it freeze the tile. How much of this Apple Home actually draws, and in what words, is Apple's decision.
|
|
116
130
|
|
|
117
131
|
## Battery percentage in Apple Home
|
|
118
132
|
|
package/config.schema.json
CHANGED
|
@@ -110,6 +110,12 @@
|
|
|
110
110
|
"type": "boolean",
|
|
111
111
|
"default": false
|
|
112
112
|
},
|
|
113
|
+
"enableMatterFaultReporting": {
|
|
114
|
+
"title": "Report Faults in Apple Home",
|
|
115
|
+
"description": "Show why the robot needs attention instead of leaving it as Ready: a stuck robot reports Error, and conditions such as an empty clean-water tank, a full waste-water tank, a missing dust bag or a blocked brush are published as a Matter fault. Which of them Apple Home renders, and how, is Apple's decision. Off by default because a robot reporting Error may refuse a Start command from Apple Home. Dock conditions never force the Error state, so a robot that can still vacuum stays startable. If Matter refuses the fault attribute, the plugin drops it automatically and logs a warning rather than freezing the tile.",
|
|
116
|
+
"type": "boolean",
|
|
117
|
+
"default": false
|
|
118
|
+
},
|
|
113
119
|
"matterChargedBatteryThreshold": {
|
|
114
120
|
"title": "Charged Battery Threshold (%)",
|
|
115
121
|
"description": "Battery percentage at which a docked robot switches from Charging to Docked on the Apple Home tile. Default 100. Lower it (e.g. 90) if a worn battery makes the robot report fully charged early.",
|
|
@@ -145,6 +145,102 @@ const RVC_CHARGING_DOCKED_STATE_LIST = [
|
|
|
145
145
|
RVC_OPERATIONAL_STATE.CHARGING,
|
|
146
146
|
RVC_OPERATIONAL_STATE.DOCKED,
|
|
147
147
|
];
|
|
148
|
+
// RVC Operational State ErrorStateEnum, from the Matter cluster definition
|
|
149
|
+
// (src/app/zap-templates/zcl/data-model/chip/operational-state-rvc-cluster.xml).
|
|
150
|
+
// Only the values that shipped with the original RVC device type are used:
|
|
151
|
+
// they are the ones controllers have understood the longest, and a fault the
|
|
152
|
+
// controller cannot name is worse than a generic one it can.
|
|
153
|
+
const RVC_ERROR_STATE = {
|
|
154
|
+
NO_ERROR: 0x00,
|
|
155
|
+
UNABLE_TO_START_OR_RESUME: 0x01,
|
|
156
|
+
UNABLE_TO_COMPLETE_OPERATION: 0x02,
|
|
157
|
+
FAILED_TO_FIND_CHARGING_DOCK: 0x40,
|
|
158
|
+
STUCK: 0x41,
|
|
159
|
+
DUST_BIN_MISSING: 0x42,
|
|
160
|
+
DUST_BIN_FULL: 0x43,
|
|
161
|
+
WATER_TANK_EMPTY: 0x44,
|
|
162
|
+
WATER_TANK_MISSING: 0x45,
|
|
163
|
+
MOP_CLEANING_PAD_MISSING: 0x47,
|
|
164
|
+
LOW_BATTERY: 0x48,
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Roborock `error_code` to Matter ErrorStateEnum. The Roborock names are the
|
|
168
|
+
* ones this plugin already shows in `deviceFeatures.js`; several distinct
|
|
169
|
+
* mechanical faults collapse onto STUCK because Matter has no finer-grained
|
|
170
|
+
* standard code for them, which is fine — the human-readable Roborock text
|
|
171
|
+
* rides along in `errorStateDetails`.
|
|
172
|
+
*
|
|
173
|
+
* Codes deliberately absent map to UNABLE_TO_COMPLETE_OPERATION via the
|
|
174
|
+
* fallback: inventing a wrong specific code is worse than a correct vague one.
|
|
175
|
+
*/
|
|
176
|
+
const ROBOROCK_ERROR_TO_MATTER = new Map([
|
|
177
|
+
[3, RVC_ERROR_STATE.STUCK], // Wheel floating
|
|
178
|
+
[5, RVC_ERROR_STATE.STUCK], // Main brush blocked
|
|
179
|
+
[6, RVC_ERROR_STATE.STUCK], // Side brush blocked
|
|
180
|
+
[7, RVC_ERROR_STATE.STUCK], // Wheel blocked
|
|
181
|
+
[8, RVC_ERROR_STATE.STUCK], // Device stuck
|
|
182
|
+
[9, RVC_ERROR_STATE.DUST_BIN_MISSING], // Dust bin missing
|
|
183
|
+
[12, RVC_ERROR_STATE.LOW_BATTERY], // Low battery
|
|
184
|
+
[16, RVC_ERROR_STATE.STUCK], // Uneven surface
|
|
185
|
+
[19, RVC_ERROR_STATE.FAILED_TO_FIND_CHARGING_DOCK], // Unpowered charging station
|
|
186
|
+
[22, RVC_ERROR_STATE.FAILED_TO_FIND_CHARGING_DOCK], // Charge sensor problem
|
|
187
|
+
[23, RVC_ERROR_STATE.FAILED_TO_FIND_CHARGING_DOCK], // Dock problem
|
|
188
|
+
[24, RVC_ERROR_STATE.STUCK], // No-go zone or invisible wall detected
|
|
189
|
+
[254, RVC_ERROR_STATE.DUST_BIN_FULL], // Bin full
|
|
190
|
+
]);
|
|
191
|
+
/**
|
|
192
|
+
* Roborock `dock_error_status` to Matter ErrorStateEnum, with the label the
|
|
193
|
+
* Roborock app uses. Values are the ones the reference implementations agree
|
|
194
|
+
* on; `0` means the dock is healthy.
|
|
195
|
+
*
|
|
196
|
+
* The dirty-water tank has no pre-1.4 standard code, so "waste water tank
|
|
197
|
+
* full" reports as DUST_BIN_FULL — the closest "a container needs emptying"
|
|
198
|
+
* signal every controller understands — with the true wording in the details.
|
|
199
|
+
*/
|
|
200
|
+
const ROBOROCK_DOCK_ERROR_TO_MATTER = new Map([
|
|
201
|
+
[
|
|
202
|
+
34,
|
|
203
|
+
{
|
|
204
|
+
errorStateId: RVC_ERROR_STATE.UNABLE_TO_COMPLETE_OPERATION,
|
|
205
|
+
details: "Dock air duct blocked",
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
[
|
|
209
|
+
38,
|
|
210
|
+
{
|
|
211
|
+
errorStateId: RVC_ERROR_STATE.WATER_TANK_EMPTY,
|
|
212
|
+
details: "Clean water tank empty",
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
[
|
|
216
|
+
39,
|
|
217
|
+
{
|
|
218
|
+
errorStateId: RVC_ERROR_STATE.DUST_BIN_FULL,
|
|
219
|
+
details: "Waste water tank full",
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
[
|
|
223
|
+
44,
|
|
224
|
+
{
|
|
225
|
+
errorStateId: RVC_ERROR_STATE.WATER_TANK_MISSING,
|
|
226
|
+
details: "Dirty water tank latch open",
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
[
|
|
230
|
+
46,
|
|
231
|
+
{
|
|
232
|
+
errorStateId: RVC_ERROR_STATE.DUST_BIN_MISSING,
|
|
233
|
+
details: "Dock dust bag missing",
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
[
|
|
237
|
+
53,
|
|
238
|
+
{
|
|
239
|
+
errorStateId: RVC_ERROR_STATE.UNABLE_TO_COMPLETE_OPERATION,
|
|
240
|
+
details: "Mop cleaning tank full or blocked",
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
]);
|
|
148
244
|
const POWER_SOURCE_STATUS = {
|
|
149
245
|
ACTIVE: 1,
|
|
150
246
|
UNAVAILABLE: 3,
|
|
@@ -252,6 +348,13 @@ class RoborockMatterVacuumAccessory {
|
|
|
252
348
|
this.lastWaterBoxMode = null;
|
|
253
349
|
this.matterInitializationRetryAttempt = 0;
|
|
254
350
|
this.matterInitializationRetryPending = false;
|
|
351
|
+
// Latched when a Matter write carrying `operationalError` is rejected.
|
|
352
|
+
// `operationalError` travels in the same cluster payload as
|
|
353
|
+
// `operationalState`, so a controller or matter.js build that refuses the
|
|
354
|
+
// attribute would otherwise freeze the whole tile — Cleaning/Docked
|
|
355
|
+
// included — for as long as the setting stays on. One rejection is enough
|
|
356
|
+
// to drop the attribute for the rest of this run.
|
|
357
|
+
this.faultReportingRejected = false;
|
|
255
358
|
this.returnToDockRetryPending = false;
|
|
256
359
|
this.matterStateHeartbeatTimer = null;
|
|
257
360
|
// Serializes every Matter publish so concurrent publishers (live messages,
|
|
@@ -359,7 +462,15 @@ class RoborockMatterVacuumAccessory {
|
|
|
359
462
|
this.accessory.context = {};
|
|
360
463
|
}
|
|
361
464
|
this.accessory.context.duid = duid;
|
|
362
|
-
|
|
465
|
+
// The registration snapshot is the payload Matter commissions the
|
|
466
|
+
// endpoint from, and 1.4.61 removed the plugin's `operationalError` write
|
|
467
|
+
// precisely because Apple Home reacted badly to it there. Keep the
|
|
468
|
+
// declaration byte-identical to a build without fault reporting and let
|
|
469
|
+
// the attribute arrive on the first runtime publish a few seconds later
|
|
470
|
+
// — matter.js supplies the mandatory NoError default in the meantime, so
|
|
471
|
+
// nothing is missing, and a robot that happens to be faulted at
|
|
472
|
+
// Homebridge start can no longer change what gets commissioned.
|
|
473
|
+
this.accessory.clusters = this.stripOperationalError(this.buildClusters());
|
|
363
474
|
this.accessory.handlers = this.buildHandlers();
|
|
364
475
|
this.accessory.getState = async (cluster, attribute) => {
|
|
365
476
|
const clusterState = this.buildCluster(cluster);
|
|
@@ -383,7 +494,7 @@ class RoborockMatterVacuumAccessory {
|
|
|
383
494
|
}
|
|
384
495
|
}
|
|
385
496
|
async updateMatterStateFromRoborock() {
|
|
386
|
-
var _a, _b, _c
|
|
497
|
+
var _a, _b, _c;
|
|
387
498
|
if (!this.registered) {
|
|
388
499
|
return;
|
|
389
500
|
}
|
|
@@ -406,7 +517,15 @@ class RoborockMatterVacuumAccessory {
|
|
|
406
517
|
const opState = clusters.rvcOperationalState;
|
|
407
518
|
const runMode = clusters.rvcRunMode;
|
|
408
519
|
const cleanMode = clusters.rvcCleanMode;
|
|
409
|
-
|
|
520
|
+
// A fault only appears here when one is actually being published, so
|
|
521
|
+
// an unremarkable line stays unremarkable — but when a user reports
|
|
522
|
+
// "Apple Home shows nothing", this is what says whether the plugin
|
|
523
|
+
// sent anything to show.
|
|
524
|
+
const fault = opState === null || opState === void 0 ? void 0 : opState.operationalError;
|
|
525
|
+
const faultSummary = fault && fault.errorStateId
|
|
526
|
+
? `, fault=${fault.errorStateId}${fault.errorStateDetails ? ` (${fault.errorStateDetails})` : ""}`
|
|
527
|
+
: "";
|
|
528
|
+
this.platform.log.info(`Matter publish for ${this.getVacuumName()}: battery=${typeof halfPercent === "number" ? halfPercent / 2 + "%" : "n/a"}, operationalState=${(_a = opState === null || opState === void 0 ? void 0 : opState.operationalState) !== null && _a !== void 0 ? _a : "n/a"}, runMode=${(_b = runMode === null || runMode === void 0 ? void 0 : runMode.currentMode) !== null && _b !== void 0 ? _b : "n/a"}, cleanMode=${(_c = cleanMode === null || cleanMode === void 0 ? void 0 : cleanMode.currentMode) !== null && _c !== void 0 ? _c : "n/a"}${faultSummary}.`);
|
|
410
529
|
}
|
|
411
530
|
this.ensureMatterStateHeartbeat();
|
|
412
531
|
}
|
|
@@ -690,6 +809,14 @@ class RoborockMatterVacuumAccessory {
|
|
|
690
809
|
// Drop the record so the cluster is retried on the next snapshot
|
|
691
810
|
// even if its payload is unchanged.
|
|
692
811
|
this.lastPublishedClusterJson.delete(cluster);
|
|
812
|
+
// Opt-in fault reporting must never cost the user their tile. If
|
|
813
|
+
// the payload carried `operationalError` and the write was
|
|
814
|
+
// refused for any reason other than an endpoint that is still
|
|
815
|
+
// coming up, retry immediately without it and stop sending it.
|
|
816
|
+
// The operational state itself then still lands.
|
|
817
|
+
if (await this.retryWithoutOperationalError(matter, cluster, attributes, error)) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
693
820
|
failures.push(error);
|
|
694
821
|
this.platform.log.debug(`Matter publish for cluster ${cluster} on ${this.accessory.UUID} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
695
822
|
}
|
|
@@ -722,6 +849,38 @@ class RoborockMatterVacuumAccessory {
|
|
|
722
849
|
throw error;
|
|
723
850
|
}
|
|
724
851
|
}
|
|
852
|
+
/**
|
|
853
|
+
* Recovery for a rejected `operationalError` write.
|
|
854
|
+
*
|
|
855
|
+
* Returns true when the cluster was successfully re-published without the
|
|
856
|
+
* attribute, in which case the caller must treat the write as a success —
|
|
857
|
+
* the operational state landed, only the optional fault detail was dropped.
|
|
858
|
+
* Returns false for payloads that never carried the attribute, for an
|
|
859
|
+
* endpoint that is merely still initializing (that has its own retry, and
|
|
860
|
+
* disabling a feature over a transient startup race would be wrong), and
|
|
861
|
+
* when the retry fails too.
|
|
862
|
+
*/
|
|
863
|
+
async retryWithoutOperationalError(matter, cluster, attributes, error) {
|
|
864
|
+
if (!("operationalError" in attributes) ||
|
|
865
|
+
this.isMatterInitializingError(error)) {
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
const { operationalError: _rejected, ...withoutError } = attributes;
|
|
869
|
+
try {
|
|
870
|
+
await matter.updateAccessoryState(this.accessory.UUID, cluster, withoutError);
|
|
871
|
+
}
|
|
872
|
+
catch (_a) {
|
|
873
|
+
// The write was failing for some other reason. Leave the feature alone
|
|
874
|
+
// and let the caller record a normal failure.
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
if (!this.faultReportingRejected) {
|
|
878
|
+
this.faultReportingRejected = true;
|
|
879
|
+
this.platform.log.warn(`Matter rejected the fault detail (operationalError) for ${this.getVacuumName()}, so fault reporting has been switched off for this session; the operational state itself is unaffected. Turn off "Report Faults in Apple Home" to silence this. Reason: ${this.getErrorMessage(error)}`);
|
|
880
|
+
}
|
|
881
|
+
this.lastPublishedClusterJson.set(cluster, JSON.stringify(withoutError));
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
725
884
|
/**
|
|
726
885
|
* Publish a full Roborock cluster snapshot, performing a one-time battery
|
|
727
886
|
* resync per boot first. Matter controllers filter attribute reports by
|
|
@@ -869,6 +1028,19 @@ class RoborockMatterVacuumAccessory {
|
|
|
869
1028
|
}
|
|
870
1029
|
return this.applyOptimisticState(clusters);
|
|
871
1030
|
}
|
|
1031
|
+
/**
|
|
1032
|
+
* A copy of a cluster snapshot with the optional fault attribute removed,
|
|
1033
|
+
* for the one payload where its presence is a commissioning risk rather
|
|
1034
|
+
* than useful information.
|
|
1035
|
+
*/
|
|
1036
|
+
stripOperationalError(clusters) {
|
|
1037
|
+
const operationalState = clusters.rvcOperationalState;
|
|
1038
|
+
if (!operationalState || !("operationalError" in operationalState)) {
|
|
1039
|
+
return clusters;
|
|
1040
|
+
}
|
|
1041
|
+
const { operationalError: _omitted, ...rest } = operationalState;
|
|
1042
|
+
return { ...clusters, rvcOperationalState: rest };
|
|
1043
|
+
}
|
|
872
1044
|
buildCluster(cluster) {
|
|
873
1045
|
var _a;
|
|
874
1046
|
let clusterState;
|
|
@@ -1194,7 +1366,7 @@ class RoborockMatterVacuumAccessory {
|
|
|
1194
1366
|
}
|
|
1195
1367
|
buildOperationalStateCluster() {
|
|
1196
1368
|
const operationalState = this.getOperationalState();
|
|
1197
|
-
|
|
1369
|
+
const cluster = {
|
|
1198
1370
|
// RVC Operational State requires PhaseList and CurrentPhase to be null.
|
|
1199
1371
|
phaseList: null,
|
|
1200
1372
|
currentPhase: null,
|
|
@@ -1204,6 +1376,20 @@ class RoborockMatterVacuumAccessory {
|
|
|
1204
1376
|
operationalStateList: this.getOperationalStateList().map((operationalStateId) => ({ operationalStateId })),
|
|
1205
1377
|
operationalState,
|
|
1206
1378
|
};
|
|
1379
|
+
if (this.isFaultReportingEnabled()) {
|
|
1380
|
+
// Always write the attribute while the feature is on, never just when
|
|
1381
|
+
// something is wrong: an error that is published but never cleared
|
|
1382
|
+
// leaves the tile complaining about a tank the user refilled an hour
|
|
1383
|
+
// ago. NO_ERROR is the "all clear".
|
|
1384
|
+
const operationalError = this.buildOperationalError();
|
|
1385
|
+
cluster.operationalError =
|
|
1386
|
+
operationalError !== null && operationalError !== void 0 ? operationalError : (operationalState === RVC_OPERATIONAL_STATE.ERROR
|
|
1387
|
+
? // The robot says it has halted but named no code. Saying
|
|
1388
|
+
// "error, but no error" is worse than a truthful vague answer.
|
|
1389
|
+
{ errorStateId: RVC_ERROR_STATE.UNABLE_TO_COMPLETE_OPERATION }
|
|
1390
|
+
: { errorStateId: RVC_ERROR_STATE.NO_ERROR });
|
|
1391
|
+
}
|
|
1392
|
+
return cluster;
|
|
1207
1393
|
}
|
|
1208
1394
|
buildPowerSourceCluster(batteryValue, chargeStatusValue, stateValue) {
|
|
1209
1395
|
const battery = batteryValue === undefined
|
|
@@ -1776,6 +1962,68 @@ class RoborockMatterVacuumAccessory {
|
|
|
1776
1962
|
isChargingDockedStateEnabled() {
|
|
1777
1963
|
return (this.platform.platformConfig.enableMatterChargingDockedStates === true);
|
|
1778
1964
|
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Opt-in fault reporting. Off by default: publishing `operationalError` is
|
|
1967
|
+
* the one thing 1.4.61 removed for Apple Home commissioning safety, and a
|
|
1968
|
+
* robot that reports ERROR may be refused a Start command by the
|
|
1969
|
+
* controller — so a spurious fault would cost the user their tile. The
|
|
1970
|
+
* write also self-disables at runtime if Matter rejects it
|
|
1971
|
+
* (`faultReportingRejected`), which keeps a bad payload from taking the
|
|
1972
|
+
* rest of the operational-state cluster down with it.
|
|
1973
|
+
*/
|
|
1974
|
+
isFaultReportingEnabled() {
|
|
1975
|
+
return (this.platform.platformConfig.enableMatterFaultReporting === true &&
|
|
1976
|
+
!this.faultReportingRejected);
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Map the robot's current condition onto a Matter ErrorStateStruct, or null
|
|
1980
|
+
* when nothing is wrong.
|
|
1981
|
+
*
|
|
1982
|
+
* `errorStateLabel` is deliberately never set. The Matter spec only allows
|
|
1983
|
+
* it for manufacturer-range IDs, and labels on standard IDs are exactly
|
|
1984
|
+
* what wedged Apple Home at "Connecting" in 1.4.40 — `errorStateDetails` is
|
|
1985
|
+
* the field that may carry free text alongside a standard ID.
|
|
1986
|
+
*/
|
|
1987
|
+
buildOperationalError() {
|
|
1988
|
+
var _a, _b, _c;
|
|
1989
|
+
// The robot's own fault takes precedence over a dock consumable: if it is
|
|
1990
|
+
// stuck under the sofa, that is what the user needs to be told.
|
|
1991
|
+
const errorCode = this.getNumberStatus("error_code");
|
|
1992
|
+
if (errorCode !== null && errorCode !== 0) {
|
|
1993
|
+
return {
|
|
1994
|
+
errorStateId: (_a = ROBOROCK_ERROR_TO_MATTER.get(errorCode)) !== null && _a !== void 0 ? _a : RVC_ERROR_STATE.UNABLE_TO_COMPLETE_OPERATION,
|
|
1995
|
+
errorStateDetails: this.describeRoborockError(errorCode),
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
const dockErrorStatus = this.getNumberStatus("dock_error_status");
|
|
1999
|
+
if (dockErrorStatus !== null && dockErrorStatus !== 0) {
|
|
2000
|
+
const mapped = ROBOROCK_DOCK_ERROR_TO_MATTER.get(dockErrorStatus);
|
|
2001
|
+
return {
|
|
2002
|
+
errorStateId: (_b = mapped === null || mapped === void 0 ? void 0 : mapped.errorStateId) !== null && _b !== void 0 ? _b : RVC_ERROR_STATE.UNABLE_TO_COMPLETE_OPERATION,
|
|
2003
|
+
errorStateDetails: (_c = mapped === null || mapped === void 0 ? void 0 : mapped.details) !== null && _c !== void 0 ? _c : `Dock error ${dockErrorStatus}`,
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
// Onboard tank ran dry mid-mop. This flag is set by the robot only when
|
|
2007
|
+
// water is actually wanted, so it does not fire on a vacuum-only run.
|
|
2008
|
+
if (this.getNumberStatus("water_shortage_status") === 1) {
|
|
2009
|
+
return {
|
|
2010
|
+
errorStateId: RVC_ERROR_STATE.WATER_TANK_EMPTY,
|
|
2011
|
+
errorStateDetails: "Water tank empty",
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
// Deliberately NOT reported as faults: `water_box_status` and
|
|
2015
|
+
// `water_box_carriage_status` being 0. A detached tank or mop pad is the
|
|
2016
|
+
// normal, correct configuration for a vacuum-only run, so treating them
|
|
2017
|
+
// as errors would put a permanent warning on every dry robot's tile.
|
|
2018
|
+
return null;
|
|
2019
|
+
}
|
|
2020
|
+
describeRoborockError(errorCode) {
|
|
2021
|
+
var _a, _b;
|
|
2022
|
+
const description = (_b = (_a = this.api).getErrorCodeDescription) === null || _b === void 0 ? void 0 : _b.call(_a, errorCode);
|
|
2023
|
+
return typeof description === "string" && description.length > 0
|
|
2024
|
+
? description
|
|
2025
|
+
: `Roborock error ${errorCode}`;
|
|
2026
|
+
}
|
|
1779
2027
|
/**
|
|
1780
2028
|
* Battery percentage at which a docked robot switches from Charging to
|
|
1781
2029
|
* Docked on the Matter tile. Defaults to 100 (charging until full); users
|
|
@@ -1913,7 +2161,20 @@ class RoborockMatterVacuumAccessory {
|
|
|
1913
2161
|
}
|
|
1914
2162
|
switch (operationalState) {
|
|
1915
2163
|
case RVC_OPERATIONAL_STATE.ERROR:
|
|
1916
|
-
|
|
2164
|
+
// ERROR (3) is a member of even the basic advertised list, so
|
|
2165
|
+
// publishing it was always legal — it was downgraded to STOPPED
|
|
2166
|
+
// alongside the states that genuinely needed a gate, which is why a
|
|
2167
|
+
// robot stuck under the sofa has always read as "Ready" in Apple
|
|
2168
|
+
// Home. Report it for real once the user has opted in.
|
|
2169
|
+
//
|
|
2170
|
+
// Only Roborock states 9 (Charging Error) and 12 (In Error) reach
|
|
2171
|
+
// here, i.e. the robot's own claim that it has halted. Dock
|
|
2172
|
+
// consumables arrive through `dock_error_status` instead and never
|
|
2173
|
+
// touch the state, so a full waste-water tank does not make a robot
|
|
2174
|
+
// that can still vacuum look unstartable in Apple Home.
|
|
2175
|
+
return this.isFaultReportingEnabled()
|
|
2176
|
+
? RVC_OPERATIONAL_STATE.ERROR
|
|
2177
|
+
: RVC_OPERATIONAL_STATE.STOPPED;
|
|
1917
2178
|
case RVC_OPERATIONAL_STATE.SEEKING_CHARGER:
|
|
1918
2179
|
return RVC_OPERATIONAL_STATE.STOPPED;
|
|
1919
2180
|
case RVC_OPERATIONAL_STATE.EMPTYING_DUST_BIN:
|