homebridge-roborock-matter 3.27.0 → 3.28.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 +31 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/roborockLib/roborockAPI.js +156 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.28.0
|
|
4
|
+
|
|
5
|
+
**Startup and network traffic, measured rather than felt.**
|
|
6
|
+
|
|
7
|
+
### The tiles no longer wait for readings no tile displays
|
|
8
|
+
|
|
9
|
+
`initializeDeviceUpdates` awaited the whole first parameter cycle for every robot — 8 serial round-trips per classic robot — and only then did startup set `bInited`, fire the Homebridge callback and register the Matter accessories. Those 8 requests read consumable hours, timers, carpet mode and water-box mode. Not one of them reaches the tile; the tile displays `get_status`, which was not among them.
|
|
10
|
+
|
|
11
|
+
Counted on the critical path between "Homebridge started the plugin" and "the vacuum exists in Apple Home": **up to 12 serial network waits before, 3 after.** The home snapshot was also fetched twice back to back — `v2/user/homes` to build the device list, then `user/homes` again inside `updateHomeData` — and the second one is now handed the first one's payload. The network probe and the first status run concurrently instead of one after the other.
|
|
12
|
+
|
|
13
|
+
On my own network this was a few seconds. On a robot that does not answer locally — the case in [#22](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/22), where every local request runs its full 10-second timeout before falling back to the cloud — it kept the vacuum out of Apple Home for over a minute after every restart.
|
|
14
|
+
|
|
15
|
+
### The first status is immediate, not 15 seconds away
|
|
16
|
+
|
|
17
|
+
The classic poller is a plain interval, so its first tick landed 15 seconds after start and the tile showed the cloud snapshot until then. The B01 loop already fired its first read immediately for exactly this reason; the classic path now does too. Each robot's first status is awaited before registration, in parallel across robots and capped at 4 seconds, so the tile registers with fresh state but never waits on a robot that will not answer.
|
|
18
|
+
|
|
19
|
+
### 160 requests an hour became 52
|
|
20
|
+
|
|
21
|
+
Measured per classic robot: the periodic cycle issued 8 requests every 180 seconds — `get_multi_maps_list`, `get_room_mapping`, `get_consumable`, `get_server_timer`, `get_timer`, `get_carpet_mode`, `get_carpet_clean_mode`, `get_water_box_custom_mode` — for values that change roughly once a week. 3 robots in one house: about 280 requests an hour, 6,700 a day, at the Roborock cloud and the robots' local ports.
|
|
22
|
+
|
|
23
|
+
The 6 slow-changing readings now move to a **30-minute lane**. Room mappings deliberately stay on every cycle: a room renamed in the Roborock app should reach Apple Home within minutes, and that refresh now serves from the persisted cache first — the same shape the B01 branch already had — so its 2 round-trips run behind the tile rather than in front of it.
|
|
24
|
+
|
|
25
|
+
| | before | after |
|
|
26
|
+
| ------------- | ---------- | ---------- |
|
|
27
|
+
| classic robot | 160 / hour | ~52 / hour |
|
|
28
|
+
| Q7 (B01) | 60 / hour | ~6 / hour |
|
|
29
|
+
|
|
30
|
+
`get_status` is untouched — still every 15 seconds for a classic robot, still the dedicated B01 loop — because that is the one reading the tile lives on. A schedule written from Apple Home or a diagnostics export that wants fresh numbers can bring the slow lane forward with `requestSlowParameterPoll`.
|
|
31
|
+
|
|
32
|
+
8 new tests, 6 of which fail against 3.27.0.
|
|
33
|
+
|
|
3
34
|
## 3.27.0
|
|
4
35
|
|
|
5
36
|
**The scene resource named its one write verb, and it is the one that deletes. So the search moves off that resource and onto its siblings — asked safely, and this time with controls, so the round either finds the route or proves there is nothing left to find.**
|
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. 1870 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
|
|
|
@@ -258,7 +258,7 @@ The complete path — robot → plugin → Homebridge → matter.js store — wa
|
|
|
258
258
|
|
|
259
259
|
## Contributing
|
|
260
260
|
|
|
261
|
-
Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with
|
|
261
|
+
Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 1870 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.
|
|
262
262
|
|
|
263
263
|
## Support the project
|
|
264
264
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-roborock-matter",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.28.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": {
|
|
@@ -340,6 +340,32 @@ const DIAGNOSTIC_PRIORITY_KEYS = new Set([
|
|
|
340
340
|
// ~86k wake-ups per robot per day to serve at most 1440 polls.
|
|
341
341
|
const CLASSIC_STATUS_TICK_MS = 15000;
|
|
342
342
|
|
|
343
|
+
// HOW OFTEN THE SLOW-CHANGING PARAMETERS ARE RE-READ.
|
|
344
|
+
//
|
|
345
|
+
// Measured before this existed: a classic robot's periodic cycle issued 8
|
|
346
|
+
// requests every 180 seconds - `get_multi_maps_list`, `get_room_mapping`,
|
|
347
|
+
// `get_consumable`, `get_server_timer`, `get_timer`, `get_carpet_mode`,
|
|
348
|
+
// `get_carpet_clean_mode`, `get_water_box_custom_mode` - 160 an hour, all
|
|
349
|
+
// serial, for values that change roughly once a week. Consumable hours tick
|
|
350
|
+
// over during a clean and nowhere else; timers change when the user edits a
|
|
351
|
+
// schedule; carpet and water-box modes change when the user changes a setting.
|
|
352
|
+
// None of them drives the Apple Home tile - that is `get_status`, on its own
|
|
353
|
+
// 15-second tick and untouched by this.
|
|
354
|
+
//
|
|
355
|
+
// Room mappings are deliberately NOT in the slow lane: a room the user renames
|
|
356
|
+
// or splits in the Roborock app should show up in Apple Home within minutes,
|
|
357
|
+
// and the refresh already serves from cache first. Everything else waits.
|
|
358
|
+
//
|
|
359
|
+
// 30 minutes cuts a classic robot from 160 to about 52 requests an hour and a
|
|
360
|
+
// Q7 from 60 to about 46. A diagnostics export still sees the last reading;
|
|
361
|
+
// anything that needs a fresh one can ask for it.
|
|
362
|
+
const SLOW_PARAMETER_POLL_INTERVAL_MS = 30 * 60 * 1000;
|
|
363
|
+
|
|
364
|
+
// The longest startup will wait for a classic robot's first status before the
|
|
365
|
+
// Matter accessories register. A robot that answers does so in well under a
|
|
366
|
+
// second; one that does not must not hold the others' tiles hostage.
|
|
367
|
+
const INITIAL_STATUS_WAIT_CAP_MS = 4000;
|
|
368
|
+
|
|
343
369
|
// Persisted states whose disk flush is debounced (see setStateAsync): they
|
|
344
370
|
// change on every received robot message, are served from memory, and only
|
|
345
371
|
// need the on-disk copy for restart survival.
|
|
@@ -474,6 +500,8 @@ class Roborock {
|
|
|
474
500
|
this.unsupportedPollCommands = new Set();
|
|
475
501
|
this.loggedPollProfiles = new Set();
|
|
476
502
|
this.skippedDialectPolls = new Set();
|
|
503
|
+
/** @type {Map<string, number>} duid -> when its slow lane last ran */
|
|
504
|
+
this.lastSlowParameterPollAt = new Map();
|
|
477
505
|
this.baseURL = options.baseURL || "usiot.roborock.com";
|
|
478
506
|
|
|
479
507
|
this.userData = options.userData || null;
|
|
@@ -2051,7 +2079,7 @@ class Roborock {
|
|
|
2051
2079
|
return {};
|
|
2052
2080
|
});
|
|
2053
2081
|
|
|
2054
|
-
await this.updateHomeData(homeId);
|
|
2082
|
+
await this.updateHomeData(homeId, homedataResult);
|
|
2055
2083
|
|
|
2056
2084
|
await this.createDevices();
|
|
2057
2085
|
|
|
@@ -2070,8 +2098,14 @@ class Roborock {
|
|
|
2070
2098
|
// createDevices() so the boot poll cannot precede the wait above.
|
|
2071
2099
|
this.startB01StatusLoop();
|
|
2072
2100
|
|
|
2073
|
-
|
|
2074
|
-
|
|
2101
|
+
// Independent, so concurrent: the network probe feeds the LAN
|
|
2102
|
+
// attach that runs later in the background, and the first status
|
|
2103
|
+
// feeds the tile. Neither needs the other, and serially they were
|
|
2104
|
+
// two full round-trip waits where one is enough.
|
|
2105
|
+
await Promise.allSettled([
|
|
2106
|
+
this.getNetworkInfo(),
|
|
2107
|
+
this.initializeDeviceUpdates(),
|
|
2108
|
+
]);
|
|
2075
2109
|
|
|
2076
2110
|
this.bInited = true;
|
|
2077
2111
|
// A success ends the retry chain and forgets the backoff, so a
|
|
@@ -2613,9 +2647,51 @@ class Roborock {
|
|
|
2613
2647
|
this.vacuums[duid].getStatusIntervall(); // actually start getStatusIntervall()
|
|
2614
2648
|
}
|
|
2615
2649
|
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2650
|
+
// THE TILE WAITS FOR NONE OF THIS, SO IT MUST NOT WAIT FOR ANY OF IT.
|
|
2651
|
+
//
|
|
2652
|
+
// `initializeDeviceUpdates` used to await the whole first parameter
|
|
2653
|
+
// cycle for every robot - 8 serial round-trips per classic robot, each
|
|
2654
|
+
// able to spend its full 10-second timeout on a robot that does not
|
|
2655
|
+
// answer locally - and only then did the caller set `bInited`, fire the
|
|
2656
|
+
// Homebridge callback, and register the Matter accessories. On a home
|
|
2657
|
+
// network that added a few seconds to startup. On a robot with a dead
|
|
2658
|
+
// local link it kept the vacuum out of Apple Home for over a minute
|
|
2659
|
+
// after every restart, for readings of consumable hours and carpet
|
|
2660
|
+
// mode that no tile displays.
|
|
2661
|
+
//
|
|
2662
|
+
// The cycle still runs once at start; it just runs behind the tiles.
|
|
2663
|
+
void this.updateDataMinimumData(
|
|
2664
|
+
duid,
|
|
2665
|
+
this.vacuums[duid],
|
|
2666
|
+
robotModel
|
|
2667
|
+
).catch((error) => {
|
|
2668
|
+
this.log.debug(
|
|
2669
|
+
`Initial parameter cycle failed for ${this.describeDevice(duid)}: ${
|
|
2670
|
+
/** @type {any} */ (error)?.message || error
|
|
2671
|
+
}`
|
|
2672
|
+
);
|
|
2673
|
+
});
|
|
2674
|
+
|
|
2675
|
+
// What the tile IS waiting for is one real status. The classic poller
|
|
2676
|
+
// is a plain interval, so its first tick used to land 15 seconds after
|
|
2677
|
+
// start and the tile showed the cloud snapshot until then; the B01 loop
|
|
2678
|
+
// already fires its first read immediately for exactly this reason.
|
|
2679
|
+
// Each robot's first status is awaited, in parallel across robots and
|
|
2680
|
+
// capped, so registration waits for fresh state but never for a robot
|
|
2681
|
+
// that will not answer.
|
|
2682
|
+
if (!this.isB01Device(duid)) {
|
|
2683
|
+
initialPolls.push(
|
|
2684
|
+
Promise.race([
|
|
2685
|
+
this.getStatus(duid),
|
|
2686
|
+
new Promise((resolve) => {
|
|
2687
|
+
const timer = setTimeout(resolve, INITIAL_STATUS_WAIT_CAP_MS);
|
|
2688
|
+
if (typeof timer?.unref === "function") {
|
|
2689
|
+
timer.unref();
|
|
2690
|
+
}
|
|
2691
|
+
}),
|
|
2692
|
+
])
|
|
2693
|
+
);
|
|
2694
|
+
}
|
|
2619
2695
|
}
|
|
2620
2696
|
|
|
2621
2697
|
await Promise.allSettled(initialPolls);
|
|
@@ -3238,6 +3314,37 @@ class Roborock {
|
|
|
3238
3314
|
* @param {boolean} isB01
|
|
3239
3315
|
* @returns {Promise<any>}
|
|
3240
3316
|
*/
|
|
3317
|
+
/**
|
|
3318
|
+
* Is it time to re-read a robot's slow-changing parameters?
|
|
3319
|
+
*
|
|
3320
|
+
* The first cycle after start always qualifies (nothing recorded yet), so a
|
|
3321
|
+
* fresh process still reads everything once - in the background, off the
|
|
3322
|
+
* startup critical path.
|
|
3323
|
+
*
|
|
3324
|
+
* @param {string} duid
|
|
3325
|
+
*/
|
|
3326
|
+
isSlowParameterPollDue(duid) {
|
|
3327
|
+
const last = this.lastSlowParameterPollAt.get(duid) ?? 0;
|
|
3328
|
+
return Date.now() - last >= SLOW_PARAMETER_POLL_INTERVAL_MS;
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
/** @param {string} duid */
|
|
3332
|
+
markSlowParameterPoll(duid) {
|
|
3333
|
+
this.lastSlowParameterPollAt.set(duid, Date.now());
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
/**
|
|
3337
|
+
* Forget a robot's slow-lane stamp so the next cycle re-reads everything.
|
|
3338
|
+
* Used when something the slow lane owns is known to have changed - a
|
|
3339
|
+
* schedule written from Apple Home, a diagnostics export asking for fresh
|
|
3340
|
+
* numbers.
|
|
3341
|
+
*
|
|
3342
|
+
* @param {string} duid
|
|
3343
|
+
*/
|
|
3344
|
+
requestSlowParameterPoll(duid) {
|
|
3345
|
+
this.lastSlowParameterPollAt.delete(duid);
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3241
3348
|
async pollParameter(duid, vacuum, method, isB01) {
|
|
3242
3349
|
if (isB01 && !b01Q7Adapter.canAnswerV1Method(method)) {
|
|
3243
3350
|
const key = `${duid}:${method}`;
|
|
@@ -3500,6 +3607,13 @@ class Roborock {
|
|
|
3500
3607
|
await this.pollParameter(duid, vacuum, "get_room_mapping", isB01);
|
|
3501
3608
|
}
|
|
3502
3609
|
|
|
3610
|
+
// Everything below this line is the slow lane. See
|
|
3611
|
+
// SLOW_PARAMETER_POLL_INTERVAL_MS for the measurement behind it.
|
|
3612
|
+
if (!this.isSlowParameterPollDue(duid)) {
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
this.markSlowParameterPoll(duid);
|
|
3616
|
+
|
|
3503
3617
|
await this.pollParameter(duid, vacuum, "get_consumable", isB01);
|
|
3504
3618
|
|
|
3505
3619
|
await this.pollParameter(duid, vacuum, "get_server_timer", isB01);
|
|
@@ -3631,10 +3745,28 @@ class Roborock {
|
|
|
3631
3745
|
}
|
|
3632
3746
|
}
|
|
3633
3747
|
|
|
3634
|
-
|
|
3748
|
+
// Same shape as the B01 branch above, for the same reason: with a room
|
|
3749
|
+
// list already persisted from an earlier run, Service Area can expose it
|
|
3750
|
+
// the moment the accessory registers, and the 2 round-trips that refresh
|
|
3751
|
+
// it run behind the tile instead of in front of it. Only a robot the
|
|
3752
|
+
// plugin has never seen rooms for is worth waiting on.
|
|
3753
|
+
const refresh = async () => {
|
|
3635
3754
|
await vacuum.getParameter(duid, "get_multi_maps_list");
|
|
3636
3755
|
await vacuum.getParameter(duid, "get_room_mapping");
|
|
3637
3756
|
await this.cacheMissingMatterServiceAreaRoomMappings(duid, vacuum);
|
|
3757
|
+
};
|
|
3758
|
+
|
|
3759
|
+
if (this.getRoomMappingsForDevice(duid).length > 0) {
|
|
3760
|
+
void refresh().catch((error) => {
|
|
3761
|
+
this.log.debug(
|
|
3762
|
+
`Background room refresh failed for ${duid}: ${error.message || error}`
|
|
3763
|
+
);
|
|
3764
|
+
});
|
|
3765
|
+
return true;
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
try {
|
|
3769
|
+
await refresh();
|
|
3638
3770
|
return true;
|
|
3639
3771
|
} catch (error) {
|
|
3640
3772
|
this.log.debug(
|
|
@@ -3891,12 +4023,26 @@ class Roborock {
|
|
|
3891
4023
|
this.log.debug(`Length of message queue: ${this.messageQueue.size}`);
|
|
3892
4024
|
}
|
|
3893
4025
|
|
|
3894
|
-
|
|
4026
|
+
/**
|
|
4027
|
+
* Refresh the home snapshot - devices, products, local keys - and let every
|
|
4028
|
+
* robot's polling intervals re-check themselves against it.
|
|
4029
|
+
*
|
|
4030
|
+
* @param {string | number} homeId
|
|
4031
|
+
* @param {Record<string, any>} [prefetched] a home payload already fetched
|
|
4032
|
+
* by the caller. Startup fetched `v2/user/homes/{id}` seconds earlier,
|
|
4033
|
+
* built the device list from it, and then called this, which fetched
|
|
4034
|
+
* `user/homes/{id}` again before the Matter accessories could register:
|
|
4035
|
+
* 2 cloud round-trips for one snapshot, back to back, on the critical
|
|
4036
|
+
* path. With the payload handed in, this consumes it. The periodic
|
|
4037
|
+
* caller passes nothing and fetches fresh, as before.
|
|
4038
|
+
*/
|
|
4039
|
+
async updateHomeData(homeId, prefetched) {
|
|
3895
4040
|
this.log.debug(`Updating HomeData with homeId: ${homeId}`);
|
|
3896
4041
|
if (this.api) {
|
|
3897
4042
|
try {
|
|
3898
|
-
const
|
|
3899
|
-
|
|
4043
|
+
const homedata = prefetched
|
|
4044
|
+
? prefetched
|
|
4045
|
+
: (await this.api.get(`user/homes/${homeId}`)).data.result;
|
|
3900
4046
|
|
|
3901
4047
|
if (homedata) {
|
|
3902
4048
|
this.superviseDeviceIntervals();
|