homebridge-roborock-matter 3.4.7 → 3.4.9
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 +14 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/roborockLib/lib/b01Q7Adapter.js +38 -1
- package/roborockLib/roborockAPI.js +81 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.4.9
|
|
4
|
+
|
|
5
|
+
- **A live-room miss now says where the rooms actually are.** Two Q7s produced position cells around 22,000 while a Roborock map is a couple of thousand cells across at most — so those robots were never "between rooms", their computed position was nowhere near the map. One of them reported x exactly equal to y, which is arithmetic rather than a place a robot stood. The position on its own cannot separate a unit mismatch from a wrong origin, so the miss line now carries the range the room outlines occupy plus the map origin and resolution the transform used. This changes no behaviour; it turns the next log from a hypothesis into a measurement. The bounding box is computed only on the failure path, so a run that resolves every position pays nothing.
|
|
6
|
+
|
|
7
|
+
## 3.4.8
|
|
8
|
+
|
|
9
|
+
**Selecting "Vacuum" and getting a vacuum-and-mop was not a display bug — one timed-out command cancelled the one that mattered.** skmzwanke reported in [#8](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/8) that he selected Vacuum for a single room and the robot mopped it anyway, and his 3.4.5 log names the cause outright.
|
|
10
|
+
|
|
11
|
+
On a v1 robot the difference between "Vacuum" and "Vacuum and mop" **is** the water-box mode: choosing Vacuum sends water-box OFF. Fan power only picks a suction level within the chosen mode. The prep sequence sent fan power first and, if that command timed out, returned — so the water command was never sent at all. In his log, `set_custom_mode` timed out after two seconds, `set_water_box_custom_mode` never appeared, and the robot kept the mopping setting it already had from the Roborock app. A cosmetic command that did not answer in time cancelled the one carrying the user's actual choice.
|
|
12
|
+
|
|
13
|
+
- **The water command now goes out first, and no command in the sequence is cancelled by another's failure.** Dropping the early return cannot delay the start: the caller already races the whole prep against its own timeout, so the early return was buying latency protection that was paid for one level up.
|
|
14
|
+
- **A partial apply is now announced at warn level**, naming the robot and what was not confirmed. It was a debug line before, which meant that on a default log level the robot simply did the wrong job in silence while the Matter tile reported the mode that had been selected. That mismatch took two rounds of #8 to pin down.
|
|
15
|
+
- **The rule is enumerated over the sequence, not over the two commands in it today:** no clean-mode prep command may return out of the middle of the sequence. A third setting added later is covered by construction.
|
|
16
|
+
|
|
3
17
|
## 3.4.7
|
|
4
18
|
|
|
5
19
|
**The diagnostic report told Q7 owners their robot had tried to reach the LAN and failed. It never tried.** Following [#7](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/7), jawnlydon unpaired his robot from Apple Home, uninstalled the plugin, reinstalled it and paired fresh — and his report still read `markedRemote: true`, `remoteReason: marked-remote-after-connect-failure`, `connectionStatus: Cloud fallback`, "usually because LAN TCP was not connected at that moment". His `roborock.vacuum.sc05` speaks the B01 protocol, which has no LAN request surface at all: the plugin marks these robots cloud-only at startup precisely so it never opens a local socket to them. Every line above described a network fault that could not occur, and he spent an evening chasing it.
|
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. 463 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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-roborock-matter",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.9",
|
|
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": {
|
|
@@ -504,7 +504,9 @@ function resolveLiveRoomId(liveState) {
|
|
|
504
504
|
* @returns {{roomId: number | null,
|
|
505
505
|
* reason: "resolved" | "no-map-header" | "no-pose" | "no-room-outlines" | "pose-outside-outlines",
|
|
506
506
|
* outlineCount: number,
|
|
507
|
-
* cell: {x: number, y: number} | null
|
|
507
|
+
* cell: {x: number, y: number} | null,
|
|
508
|
+
* outlineBounds?: {minX: number, minY: number, maxX: number, maxY: number} | null,
|
|
509
|
+
* head?: {minX: number, minY: number, resolution: number}}}
|
|
508
510
|
*/
|
|
509
511
|
function describeLiveRoomResolution(liveState) {
|
|
510
512
|
const head = liveState?.head;
|
|
@@ -541,9 +543,44 @@ function describeLiveRoomResolution(liveState) {
|
|
|
541
543
|
reason: "pose-outside-outlines",
|
|
542
544
|
outlineCount,
|
|
543
545
|
cell,
|
|
546
|
+
outlineBounds: outlineBoundingBox(chains),
|
|
547
|
+
head: { minX: head.minX, minY: head.minY, resolution },
|
|
544
548
|
};
|
|
545
549
|
}
|
|
546
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Bounding box of every room outline, in the same cell space the
|
|
553
|
+
* point-in-polygon test uses.
|
|
554
|
+
*
|
|
555
|
+
* Field logs showed two Q7s reporting position cells around 22,000 while a
|
|
556
|
+
* Roborock map is at most a couple of thousand cells across — so the position
|
|
557
|
+
* is not "between rooms", it is nowhere near the map. Whether that is a unit
|
|
558
|
+
* mismatch (pose in millimetres against a resolution in metres) or a wrong
|
|
559
|
+
* origin cannot be told from the position alone: it needs the range the
|
|
560
|
+
* outlines actually occupy. Printing both next to each other turns a guess
|
|
561
|
+
* into a measurement.
|
|
562
|
+
*
|
|
563
|
+
* @param {Array<{points: Array<{x: number, y: number}>}>} chains
|
|
564
|
+
* @returns {{minX: number, minY: number, maxX: number, maxY: number} | null}
|
|
565
|
+
*/
|
|
566
|
+
function outlineBoundingBox(chains) {
|
|
567
|
+
let minX = Infinity;
|
|
568
|
+
let minY = Infinity;
|
|
569
|
+
let maxX = -Infinity;
|
|
570
|
+
let maxY = -Infinity;
|
|
571
|
+
|
|
572
|
+
for (const chain of chains) {
|
|
573
|
+
for (const point of chain.points || []) {
|
|
574
|
+
if (point.x < minX) minX = point.x;
|
|
575
|
+
if (point.y < minY) minY = point.y;
|
|
576
|
+
if (point.x > maxX) maxX = point.x;
|
|
577
|
+
if (point.y > maxY) maxY = point.y;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return Number.isFinite(minX) ? { minX, minY, maxX, maxY } : null;
|
|
582
|
+
}
|
|
583
|
+
|
|
547
584
|
/**
|
|
548
585
|
* Standard ray-casting point-in-polygon test over a room boundary chain.
|
|
549
586
|
* @param {number} x @param {number} y
|
|
@@ -95,6 +95,34 @@ const B01_LIVE_ROOM_MISS_REASONS = {
|
|
|
95
95
|
"the robot's position did not fall inside any known room outline (it may be between rooms, or the map may still be building)",
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The outline range and map origin, appended to a live-room miss.
|
|
100
|
+
*
|
|
101
|
+
* A position cell on its own cannot distinguish "the robot is between rooms"
|
|
102
|
+
* from "the position was computed in the wrong units" — and the field logs
|
|
103
|
+
* showed cells near 22,000 where a Roborock map is a couple of thousand cells
|
|
104
|
+
* at most. Printing the range the outlines occupy, plus the origin and
|
|
105
|
+
* resolution the transform used, makes the difference measurable from one log
|
|
106
|
+
* line instead of inferable from none.
|
|
107
|
+
*
|
|
108
|
+
* @param {{outlineBounds?: {minX: number, minY: number, maxX: number, maxY: number} | null,
|
|
109
|
+
* head?: {minX: number, minY: number, resolution: number}}} resolution
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
function describeOutlineBounds(resolution) {
|
|
113
|
+
const bounds = resolution?.outlineBounds;
|
|
114
|
+
if (!bounds) {
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const head = resolution.head;
|
|
119
|
+
const origin = head
|
|
120
|
+
? `, map origin ${head.minX},${head.minY} at ${head.resolution}/cell`
|
|
121
|
+
: "";
|
|
122
|
+
|
|
123
|
+
return `, outlines span ${Math.round(bounds.minX)}-${Math.round(bounds.maxX)} x ${Math.round(bounds.minY)}-${Math.round(bounds.maxY)}${origin}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
98
126
|
const B01_STATUS_TICK_MS = 15000;
|
|
99
127
|
const B01_STATUS_FORCED_GAP_MS = 1500;
|
|
100
128
|
const B01_STATUS_ACTIVE_GAP_MS = 12000;
|
|
@@ -2355,6 +2383,49 @@ class Roborock {
|
|
|
2355
2383
|
return;
|
|
2356
2384
|
}
|
|
2357
2385
|
|
|
2386
|
+
// The water command goes first, and no failure below cancels a later
|
|
2387
|
+
// command.
|
|
2388
|
+
//
|
|
2389
|
+
// On a v1 robot the difference between "Vacuum" and "Vacuum and mop" IS
|
|
2390
|
+
// the water-box mode: selecting Vacuum sends water-box OFF. Fan power is a
|
|
2391
|
+
// suction level *within* the chosen mode. The fan command used to run
|
|
2392
|
+
// first and, on timeout, return — skipping the water command entirely. So
|
|
2393
|
+
// skmzwanke selected Vacuum in Apple Home, the fan command timed out after
|
|
2394
|
+
// two seconds, the water command was never sent, and his Saros 10 ran a
|
|
2395
|
+
// vacuum-and-mop over the room he had asked to be vacuumed (#8). A
|
|
2396
|
+
// cosmetic command that did not answer in time cancelled the one that
|
|
2397
|
+
// carried the user's actual choice.
|
|
2398
|
+
//
|
|
2399
|
+
// Dropping the early return cannot run the start command late: the caller
|
|
2400
|
+
// races this whole sequence against its own prep timeout, which is what
|
|
2401
|
+
// bounds the delay. The early return was buying latency protection that
|
|
2402
|
+
// was already paid for one level up.
|
|
2403
|
+
const failedCommands = [];
|
|
2404
|
+
|
|
2405
|
+
if (Number.isInteger(settings?.waterBoxMode)) {
|
|
2406
|
+
const waterCommands = this.getMatterWaterModeCommandCandidates(duid);
|
|
2407
|
+
|
|
2408
|
+
if (waterCommands.length === 0) {
|
|
2409
|
+
this.log.debug(
|
|
2410
|
+
`Matter clean mode requested water mode ${settings.waterBoxMode} for ${duid}, but no supported Roborock water command was detected.`
|
|
2411
|
+
);
|
|
2412
|
+
} else {
|
|
2413
|
+
try {
|
|
2414
|
+
await this.runFirstMatterSettingCommand(
|
|
2415
|
+
duid,
|
|
2416
|
+
waterCommands,
|
|
2417
|
+
settings.waterBoxMode,
|
|
2418
|
+
commandOptions
|
|
2419
|
+
);
|
|
2420
|
+
} catch (error) {
|
|
2421
|
+
failedCommands.push("water mode");
|
|
2422
|
+
this.log.debug(
|
|
2423
|
+
`Matter clean mode water commands failed for ${duid}; continuing with start command. ${error.message || error}`
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2358
2429
|
if (
|
|
2359
2430
|
Number.isInteger(settings?.fanPower) &&
|
|
2360
2431
|
this.getMatterCleanModeCapabilities(duid).canControlFanPower
|
|
@@ -2372,40 +2443,21 @@ class Roborock {
|
|
|
2372
2443
|
"set_custom_mode",
|
|
2373
2444
|
error
|
|
2374
2445
|
);
|
|
2375
|
-
|
|
2376
|
-
this.log.debug(
|
|
2377
|
-
`Matter clean mode fan command timed out for ${duid}; skipping remaining clean-mode prep and continuing with start command. ${error.message || error}`
|
|
2378
|
-
);
|
|
2379
|
-
return;
|
|
2380
|
-
}
|
|
2446
|
+
failedCommands.push("suction level");
|
|
2381
2447
|
this.log.debug(
|
|
2382
2448
|
`Matter clean mode fan command failed for ${duid}; continuing with start command. ${error.message || error}`
|
|
2383
2449
|
);
|
|
2384
2450
|
}
|
|
2385
2451
|
}
|
|
2386
2452
|
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
if (
|
|
2393
|
-
this.log.
|
|
2394
|
-
`
|
|
2395
|
-
);
|
|
2396
|
-
return;
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
try {
|
|
2400
|
-
await this.runFirstMatterSettingCommand(
|
|
2401
|
-
duid,
|
|
2402
|
-
waterCommands,
|
|
2403
|
-
settings.waterBoxMode,
|
|
2404
|
-
commandOptions
|
|
2405
|
-
);
|
|
2406
|
-
} catch (error) {
|
|
2407
|
-
this.log.debug(
|
|
2408
|
-
`Matter clean mode water commands failed for ${duid}; continuing with start command. ${error.message || error}`
|
|
2453
|
+
// At warn level on purpose. The clean is about to start and the Matter
|
|
2454
|
+
// tile will report the mode the user selected, so a silent partial apply
|
|
2455
|
+
// leaves the tile stating something the robot is not doing. That mismatch
|
|
2456
|
+
// is exactly what took two rounds of #8 to pin down, and the log is where
|
|
2457
|
+
// the next person will look.
|
|
2458
|
+
if (failedCommands.length > 0) {
|
|
2459
|
+
this.log.warn(
|
|
2460
|
+
`Roborock did not confirm the ${failedCommands.join(" and ")} for ${this.describeDevice(duid)} before starting; the robot may keep its previous settings for this run, so the clean may not match the mode selected in your controller.`
|
|
2409
2461
|
);
|
|
2410
2462
|
}
|
|
2411
2463
|
}
|
|
@@ -2521,11 +2573,6 @@ class Roborock {
|
|
|
2521
2573
|
].some((pattern) => message.includes(pattern));
|
|
2522
2574
|
}
|
|
2523
2575
|
|
|
2524
|
-
isMatterSettingTimeoutError(error) {
|
|
2525
|
-
const message = `${error?.message || error || ""}`.toLowerCase();
|
|
2526
|
-
return message.includes("request") && message.includes("timed out after");
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
2576
|
/**
|
|
2530
2577
|
* Self-healing capability detection for periodic poll commands: once a
|
|
2531
2578
|
* robot definitively answers a request with an "unsupported"-class error,
|
|
@@ -4272,7 +4319,7 @@ class Roborock {
|
|
|
4272
4319
|
// because they call for different fixes.
|
|
4273
4320
|
liveState.unresolvedPoseCount =
|
|
4274
4321
|
(liveState.unresolvedPoseCount || 0) + 1;
|
|
4275
|
-
const message = `Live room for ${this.describeDevice(duid)}: ${B01_LIVE_ROOM_MISS_REASONS[resolution2.reason]} (attempt ${liveState.unresolvedPoseCount} this run, ${resolution2.outlineCount} room outline(s) in the map${resolution2.cell ? `, position cell ${Math.round(resolution2.cell.x)},${Math.round(resolution2.cell.y)}` : ""}).`;
|
|
4322
|
+
const message = `Live room for ${this.describeDevice(duid)}: ${B01_LIVE_ROOM_MISS_REASONS[resolution2.reason]} (attempt ${liveState.unresolvedPoseCount} this run, ${resolution2.outlineCount} room outline(s) in the map${resolution2.cell ? `, position cell ${Math.round(resolution2.cell.x)},${Math.round(resolution2.cell.y)}` : ""}${describeOutlineBounds(resolution2)}).`;
|
|
4276
4323
|
if (liveState.unresolvedPoseCount % 5 === 0) {
|
|
4277
4324
|
this.log.info(message);
|
|
4278
4325
|
} else {
|