homebridge-roborock-matter 3.22.0 → 3.23.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 CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.23.1
4
+
5
+ **A Q7 that finished cleaning normally asked its owner to report a fault.**
6
+
7
+ The maintainer's own `roborock.vacuum.sc05` logged six distinct unexplained `error_code`s in a single day — 2110, 2108, 501, 2102, 2103 and the long-familiar 2105 — every one of them while it was running. It then finished its run and docked at 100%. Nothing was wrong with it at any point.
8
+
9
+ Read against python-roborock's own per-family fault tables, two of those six are not faults at all:
10
+
11
+ - **2102** — "Cleaning completed. Returning to the dock." It fires after **every** task.
12
+ - **2100** — "Low battery. Resume cleaning after recharging." The robot announcing normal auto-recharge-and-resume.
13
+
14
+ The plugin already treats the Q10 family's equivalents this way: upstream marks that family's 501 as hardware-confirmed and firing per completed task, and its 502 as a low-battery resume, and both have been informational here since the families were split. The Q7 family was simply never given its own two. The asymmetry ran the other way too — the Q7 has always silenced 407 ("cleaning in progress, scheduled clean ignored") while the Q10 did not, though upstream marks it hardware-confirmed and "lifecycle, not an error" on that family as well. All three are informational now.
15
+
16
+ Apple Home was never affected: no B01 fault number appears in the plugin's v1 error table, so an unrecognised one has always published nothing rather than drawing a fault on a healthy tile. What it reached was the log, which named the code once per run and asked the owner to report the number "if the robot really is in trouble right now" — asked, in these two cases, after a clean that had just completed successfully.
17
+
18
+ **Restraint is the other half of this.** Only a healthy robot's lifecycle notifications are silenced. A scheduled clean that did not run (2003, "Battery level below 20%. Scheduled task canceled") and a clean that ended without reaching its target (2007, 2012) are outcomes an owner may want to know about, so they still surface. And the codes upstream itself cannot explain — 2103, 2105, 2108 and 2110 are bare `fault_NNNN` entries there too — stay exactly as they were. Silencing a number nobody has explained would be a guess, not a translation.
19
+
20
+ ## 3.23.0
21
+
22
+ **Where a newer robot actually keeps its schedules: a read-only measurement, on request.**
23
+
24
+ Some newer robots decline the device-side schedule method outright. A Saros 10R (`roborock.vacuum.a144`) answers `-10007 "Not FCC robot"` to every `get_server_timer`, while the legacy `get_timer` honestly answers `[]`. Both answers are true — that robot holds no _device-side_ timers — and yet its owner has three daily schedules, which he showed running under the robot's own Schedule screen in the Roborock app. They are held server-side, on cloud routes the device protocol never touches, and this plugin has only ever asked the robot.
25
+
26
+ Rather than map a payload nobody here has seen, this release measures it. With debug logging on, the plugin now asks the two candidate cloud routes for each robot once and prints what came back:
27
+
28
+ - `user/devices/{duid}/jobs` — schedules
29
+ - `user/scene/device/{duid}` — the app's Routines
30
+
31
+ The answer is also filed under `lastCloudScheduleProbe` in the plugin's diagnostics.
32
+
33
+ **This is a diagnostic, not a feature, and it is built to stay that way.** It is silent unless debug logging is on, so no installation pays for it uninvited. It only ever issues GETs, so it cannot alter a schedule. It runs once per robot per session, so no poll cadence can turn it into traffic. It cannot throw, because it rides along on a live poll. And credential-shaped fields in the answer are redacted before anything is logged.
34
+
35
+ It does not yet expose these schedules in HomeKit. It establishes their shape, which is what the next step needs.
36
+
3
37
  ## 3.22.0
4
38
 
5
39
  **Schedule reads cost far fewer cloud calls, and the queue that makes that possible could deadlock itself.**
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. 1760 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. 1787 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
 
@@ -257,7 +257,7 @@ The complete path — robot → plugin → Homebridge → matter.js store — wa
257
257
 
258
258
  ## Contributing
259
259
 
260
- Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 1760 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.
260
+ Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 1787 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.
261
261
 
262
262
  ## Support the project
263
263
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-roborock-matter",
3
- "version": "3.22.0",
3
+ "version": "3.23.1",
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": {
@@ -197,13 +197,40 @@ function v1FanPowerToWind(family) {
197
197
  // Fault codes that are informational rather than active errors — per family,
198
198
  // because the 2 families reuse the same numbers for different things.
199
199
  //
200
- // Q7: 407 = "Cleaning in progress. Scheduled cleanup ignored".
201
- // Q10: 400 = scheduled clean starting, 501 = cleaning completed and returning
200
+ // Q7: 407 = "Cleaning in progress. Scheduled cleanup ignored", 2100 = "Low
201
+ // battery. Resume cleaning after recharging", 2102 = "Cleaning completed.
202
+ // Returning to the dock".
203
+ // Q10: 400 = scheduled clean starting, 407 = cleaning in progress and a due
204
+ // scheduled clean ignored, 501 = cleaning completed and returning
202
205
  // (hardware-confirmed upstream, and it fires after EVERY task, so treating it
203
206
  // as a fault would leave a Q10 permanently in error), 502 = low-battery
204
207
  // resume.
205
- const INFORMATIONAL_Q7_FAULTS = new Set([0, 407]);
206
- const INFORMATIONAL_Q10_FAULTS = new Set([0, 400, 501, 502]);
208
+ //
209
+ // EACH FAMILY NEEDS ITS OWN PER-TASK COMPLETION CODE, AND THE Q7 WAS MISSING
210
+ // ITS ONE UNTIL 3.23.1. The 2 sets started from the codes each family had
211
+ // actually been measured emitting, which left them asymmetric in both
212
+ // directions: the Q10 had its after-every-task code (501) while the Q7 did
213
+ // not, and the Q7 had 407 while the Q10 did not — even though upstream marks
214
+ // `YXFault` 407 hardware-confirmed on a physical ss07 and "lifecycle, not an
215
+ // error".
216
+ //
217
+ // The Q7 gap surfaced on the maintainer's own `sc05`, which logged 6 distinct
218
+ // unmapped codes in a single day (2110, 2108, 501, 2102, 2103, 2105), every
219
+ // one of them mid-run, and then finished and docked at 100 % with nothing
220
+ // wrong. Read against python-roborock's `B01Fault`, 2 of the 6 are not faults:
221
+ // 2102 fires after every task, and 2100 is the robot announcing normal
222
+ // auto-recharge-and-resume — the Q7 analogue of the Q10's 502.
223
+ //
224
+ // WHAT IS DELIBERATELY LEFT SURFACING, BECAUSE SILENCE IS NOT FREE. Only a
225
+ // healthy robot's lifecycle notifications belong here. Q7's 2003 ("Battery
226
+ // level below 20%. Scheduled task canceled") and 2007/2012 ("Unable to reach
227
+ // the target. Cleaning ended") are outcomes a user may want to know about — a
228
+ // scheduled clean that did not run is not noise. And the codes upstream has no
229
+ // description for at all (2103, 2105, 2108, 2110 are bare `fault_NNNN` there)
230
+ // stay put: silencing a number nobody has explained would be a guess, not a
231
+ // translation.
232
+ const INFORMATIONAL_Q7_FAULTS = new Set([0, 407, 2100, 2102]);
233
+ const INFORMATIONAL_Q10_FAULTS = new Set([0, 400, 407, 501, 502]);
207
234
 
208
235
  /** @param {string} family */
209
236
  function informationalFaults(family) {
@@ -980,6 +980,19 @@ class vacuum {
980
980
  this.adapter.log.debug(
981
981
  `Roborock ${parameter} diagnostic for ${duid}: ${JSON.stringify(this.adapter.compactDiagnosticPayload(timers))}`
982
982
  );
983
+
984
+ // Both device-side timer methods have now answered for this robot,
985
+ // so this is the one moment where their answers can be compared with
986
+ // what the cloud holds (#22). The probe is read-only, debug-only and
987
+ // runs once per robot per session; it must not be able to fail the
988
+ // poll it is riding along on.
989
+ try {
990
+ await this.adapter.probeCloudScheduleRoutes?.(duid);
991
+ } catch (error) {
992
+ this.adapter.log.debug(
993
+ `Roborock cloud schedule probe for ${duid} could not run: ${error instanceof Error ? error.message : String(error)}`
994
+ );
995
+ }
983
996
  }
984
997
  } else if (parameter == "get_photo") {
985
998
  const photoresponse = await sendParameterRequest(
@@ -4540,6 +4540,113 @@ class Roborock {
4540
4540
  return await this.vacuums[duid].getServerTimers(duid, options);
4541
4541
  }
4542
4542
 
4543
+ /**
4544
+ * Ask the Roborock CLOUD where a robot's schedules live (#22).
4545
+ *
4546
+ * Some newer robots decline the device-side `get_server_timer` outright —
4547
+ * a Saros 10R (`roborock.vacuum.a144`) answers `-10007 "Not FCC robot"` on
4548
+ * every attempt, while the legacy `get_timer` honestly answers `[]`. Both
4549
+ * answers are true: that robot has no DEVICE-side timers. Its owner
4550
+ * demonstrably has three daily schedules in the app, under the robot's own
4551
+ * Schedule screen, so they are held server-side on routes the device
4552
+ * protocol knows nothing about.
4553
+ *
4554
+ * This is a MEASUREMENT, not a feature. It reads the two candidate routes
4555
+ * and prints what came back, so the next release can map a real payload
4556
+ * instead of a guess. Deliberately constrained:
4557
+ *
4558
+ * - **Debug only.** Silent for every installation that has not asked for it.
4559
+ * - **GET only.** Nothing here can change a schedule. The Hawk interceptor
4560
+ * signs an empty body (`roborockAPI.js` request interceptor), so a
4561
+ * body-bearing write would not authenticate anyway — a read does.
4562
+ * - **Once per robot per session,** so a poll cadence cannot turn it into
4563
+ * traffic.
4564
+ * - **Never throws.** A probe that breaks startup would be worse than the
4565
+ * missing feature it investigates.
4566
+ *
4567
+ * @param {string} duid Robot to probe.
4568
+ * @returns {Promise<Record<string, unknown> | undefined>} Per-route outcome,
4569
+ * or `undefined` when the probe did not run.
4570
+ */
4571
+ async probeCloudScheduleRoutes(duid) {
4572
+ if (!duid || !this.config?.debug || !this.api) {
4573
+ return undefined;
4574
+ }
4575
+
4576
+ if (!this._probedCloudScheduleRoutes) {
4577
+ this._probedCloudScheduleRoutes = new Set();
4578
+ }
4579
+
4580
+ if (this._probedCloudScheduleRoutes.has(duid)) {
4581
+ return undefined;
4582
+ }
4583
+
4584
+ this._probedCloudScheduleRoutes.add(duid);
4585
+
4586
+ // Route names and shapes cross-checked against python-roborock's
4587
+ // `get_schedules` and `get_scenes`. Our own `executeScene` already talks to
4588
+ // `user/scene/{id}/execute` on this same client, which is what makes the
4589
+ // base URL and the leading-slash convention here a measured fact rather
4590
+ // than a hope.
4591
+ const routes = [
4592
+ { label: "schedules", path: `user/devices/${duid}/jobs` },
4593
+ { label: "scenes", path: `user/scene/device/${duid}` },
4594
+ ];
4595
+
4596
+ /** @type {Record<string, unknown>} */
4597
+ const results = {};
4598
+
4599
+ for (const route of routes) {
4600
+ try {
4601
+ const response = await this.api.get(route.path);
4602
+ // Roborock wraps most answers in `{api,result,status,success}`. Keep
4603
+ // the envelope only when there is no `result` to unwrap, so the log
4604
+ // shows the payload rather than the wrapper.
4605
+ const payload =
4606
+ response?.data?.result === undefined
4607
+ ? response?.data
4608
+ : response.data.result;
4609
+
4610
+ results[route.label] = {
4611
+ path: route.path,
4612
+ ok: true,
4613
+ response: payload,
4614
+ };
4615
+
4616
+ this.log.debug(
4617
+ `Roborock cloud schedule probe for ${this.describeDevice(duid)} — GET ${route.path} answered: ${JSON.stringify(
4618
+ this.compactDiagnosticPayload(payload)
4619
+ )}`
4620
+ );
4621
+ } catch (error) {
4622
+ const status = error?.response?.status;
4623
+ const message =
4624
+ error instanceof Error ? error.message : String(error ?? "");
4625
+
4626
+ results[route.label] = {
4627
+ path: route.path,
4628
+ ok: false,
4629
+ status: status ?? null,
4630
+ error: message,
4631
+ };
4632
+
4633
+ this.log.debug(
4634
+ `Roborock cloud schedule probe for ${this.describeDevice(duid)} — GET ${route.path} failed${
4635
+ status ? ` with HTTP ${status}` : ""
4636
+ }: ${message}`
4637
+ );
4638
+ }
4639
+ }
4640
+
4641
+ await this.updateRoborockDiagnostics(
4642
+ String(duid),
4643
+ "lastCloudScheduleProbe",
4644
+ results
4645
+ );
4646
+
4647
+ return results;
4648
+ }
4649
+
4543
4650
  async updateServerTimer(duid, timerId, enabled, options = {}) {
4544
4651
  if (!this.vacuums[duid]) {
4545
4652
  throw new Error(