homebridge-roborock-matter 3.22.0 → 3.23.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 +17 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/roborockLib/lib/vacuum.js +13 -0
- package/roborockLib/roborockAPI.js +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.23.0
|
|
4
|
+
|
|
5
|
+
**Where a newer robot actually keeps its schedules: a read-only measurement, on request.**
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
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:
|
|
10
|
+
|
|
11
|
+
- `user/devices/{duid}/jobs` — schedules
|
|
12
|
+
- `user/scene/device/{duid}` — the app's Routines
|
|
13
|
+
|
|
14
|
+
The answer is also filed under `lastCloudScheduleProbe` in the plugin's diagnostics.
|
|
15
|
+
|
|
16
|
+
**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.
|
|
17
|
+
|
|
18
|
+
It does not yet expose these schedules in HomeKit. It establishes their shape, which is what the next step needs.
|
|
19
|
+
|
|
3
20
|
## 3.22.0
|
|
4
21
|
|
|
5
22
|
**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.
|
|
40
|
+
- 🛡️ **Verified by Homebridge.** Reviewed and endorsed by the Homebridge team. 1778 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
|
|
260
|
+
Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 1778 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.
|
|
3
|
+
"version": "3.23.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": {
|
|
@@ -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(
|