homebridge-roborock-matter 3.17.2 → 3.17.4
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 +2 -2
- package/package.json +1 -1
- package/roborockLib/lib/deviceFeatures.js +75 -0
- package/roborockLib/lib/vacuum.js +58 -14
- package/roborockLib/roborockAPI.js +20 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.17.4
|
|
4
|
+
|
|
5
|
+
**The "no mapping for these fields" warning no longer asks you to report fields this plugin already maps.** Raised by the log [@jcoz00](https://github.com/jcoz00) posted in [#6](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/6), whose Qrevo CurvX was told the plugin has no mapping for **eighteen** `get_status` fields and that a GitHub model report quoting the line is how they get added. Fifteen of those eighteen are named in `deviceFeatures.js` already. There was nothing for a report to add, and the three fields that genuinely were news sat buried in a list of fifteen that were not.
|
|
6
|
+
|
|
7
|
+
The message was asking the wrong question. A robot's status table starts as a copy of the plugin's baseline and capability detection adds to it, so "this field is not switched on for **this robot**" and "this plugin has never heard of this field" are different questions — and only the second one is worth a user's time. The warning asked the first and reported the answer as the second.
|
|
8
|
+
|
|
9
|
+
Each case now says what it means. A field no table anywhere names is still warned about once, by name and value, and still worth a model report; for the CurvX that is three fields rather than eighteen. A field the plugin maps but this robot's capability gate did not switch on is a debug line that says so, once per field per robot, and does not point anyone at GitHub. The repeat line that followed it on every subsequent poll is gone for that case — fifteen lines a minute saying nothing the first one did not.
|
|
10
|
+
|
|
11
|
+
**This quietens [#8](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/8) entirely as a side effect.** All nine fields [@skmzwanke](https://github.com/skmzwanke)'s Saros 10 was warned about have since been added, so that warning had been asking for work that was already done. It now says nothing at all.
|
|
12
|
+
|
|
13
|
+
Nothing about which fields are read, published or acted on changed — this release changes only what the log claims. The declared set of capability-installable fields is derived from the source by a test that scans for every writer, so a new capability cannot reintroduce the wrong warning without the suite failing.
|
|
14
|
+
|
|
15
|
+
## 3.17.3
|
|
16
|
+
|
|
17
|
+
**Q7- and Q10-series robots no longer spend a cloud request per poll on an answer the plugin cannot read.** Reported with a diagnostics export by [@niclasreich](https://github.com/niclasreich) in [#14](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/14), whose Q10 S5 (`roborock.vacuum.ss07`) logged `Failed to execute get_room_mapping … method prop.get timed out after 10 seconds` while the MQTT connection was reported as up.
|
|
18
|
+
|
|
19
|
+
The two method names in that line disagree, and that was the clue. `get_room_mapping` is the caller's label; `prop.get` is what actually went on the wire. The classic room-mapping routine opens by fetching `get_status` in order to read `map_status` and derive a floor number — and on these robots `get_status` translates to a real `prop.get`. `map_status` is a v1-only field that a Q7/Q10 status dictionary has never carried, so the reply could not have been used whatever it said. The request itself was already answered locally from the dialect's neutral table without touching the network, which is exactly why the existing skip did not catch this: the harmless call was making a second, expensive one.
|
|
20
|
+
|
|
21
|
+
The classic flow is now skipped outright for these robots, which is where their room data was never coming from in the first place — it arrives over the protobuf map channel. That removes one cloud round-trip per poll cycle per robot, along with the `No room mappings returned` notice and the empty room-list announcement that repeated at the same rate. Robots on the classic protocol are unaffected and still read `map_status` exactly as before.
|
|
22
|
+
|
|
23
|
+
**This does not by itself explain a robot that ignores commands from Apple Home**, which is the other half of that report; it removes a wasted request and the misleading error line it produced.
|
|
24
|
+
|
|
3
25
|
## 3.17.2
|
|
4
26
|
|
|
5
27
|
**The Qrevo CurvX's dock can now offer the Empty Bin switch.** Reported with a diagnostics export, and then settled by hand, by [@jcoz00](https://github.com/jcoz00) in [#6](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/6). His a185 reports `dock_type: 20`, and the dock table this plugin inherited stops at 9 — so the CurvX fell through to "unknown dock" and was treated as having no auto-empty capability, which kept the optional Empty Bin switch added in 3.17.0 from ever being offered for it. Dock type 20 is now a named, recognised auto-empty dock.
|
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. 1506 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
|
|
|
@@ -252,7 +252,7 @@ The complete path — robot → plugin → Homebridge → matter.js store — wa
|
|
|
252
252
|
|
|
253
253
|
## Contributing
|
|
254
254
|
|
|
255
|
-
Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with
|
|
255
|
+
Model reports, diagnostics exports, and pull requests are very welcome. The codebase ships with 1506 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.
|
|
256
256
|
|
|
257
257
|
## Support the project
|
|
258
258
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-roborock-matter",
|
|
3
|
-
"version": "3.17.
|
|
3
|
+
"version": "3.17.4",
|
|
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": {
|
|
@@ -182,6 +182,80 @@ const deviceStates = {
|
|
|
182
182
|
cleaning_info: "string",
|
|
183
183
|
};
|
|
184
184
|
|
|
185
|
+
// Every `get_status` field name a capability path can install into a robot's
|
|
186
|
+
// own `deviceStates`. A per-model table starts as a copy of the pristine
|
|
187
|
+
// baseline above and capability detection adds to it, so "absent from this
|
|
188
|
+
// robot's table" and "unknown to this plugin" are two different questions.
|
|
189
|
+
// `hasDeviceStatusAttribute()` only ever answered the first, and the unmapped-
|
|
190
|
+
// field warning in vacuum.js asked it the second: jcoz00's Qrevo CurvX (#6)
|
|
191
|
+
// was told the plugin has no mapping for eighteen fields and that a model
|
|
192
|
+
// report is how they get added, when fifteen of the eighteen are named right
|
|
193
|
+
// here and there was nothing for a report to add.
|
|
194
|
+
//
|
|
195
|
+
// This is a hand-written list, which is the defect shape the note at
|
|
196
|
+
// matter_vacuum_accessory.ts:185 warns about. It is held shut from outside:
|
|
197
|
+
// __tests__/a-known-field-is-not-called-unmapped.test.js scans this file for
|
|
198
|
+
// `deviceStates.<field> =` writers and fails if one of them is missing below,
|
|
199
|
+
// so a new capability cannot quietly reintroduce the wrong warning.
|
|
200
|
+
const CAPABILITY_STATUS_ATTRIBUTES = new Set([
|
|
201
|
+
"avoid_count",
|
|
202
|
+
"back_type",
|
|
203
|
+
"camera_status",
|
|
204
|
+
"carpet_clean_mode",
|
|
205
|
+
"carpet_mode",
|
|
206
|
+
"charge_status",
|
|
207
|
+
"clean_fluid",
|
|
208
|
+
"clean_percent",
|
|
209
|
+
"collision_avoid_status",
|
|
210
|
+
"common_status",
|
|
211
|
+
"corner_clean_mode",
|
|
212
|
+
"distance_off",
|
|
213
|
+
"dry_status",
|
|
214
|
+
"dss",
|
|
215
|
+
"home_sec_enable_password",
|
|
216
|
+
"home_sec_status",
|
|
217
|
+
"in_warmup",
|
|
218
|
+
"kct",
|
|
219
|
+
"last_clean_t",
|
|
220
|
+
"map_flag",
|
|
221
|
+
"monitor_status",
|
|
222
|
+
"mop_forbidden_enable",
|
|
223
|
+
"mop_mode",
|
|
224
|
+
"rdt",
|
|
225
|
+
"repeat",
|
|
226
|
+
"replenish_mode",
|
|
227
|
+
"rss",
|
|
228
|
+
"switch_map_mode",
|
|
229
|
+
"switch_status",
|
|
230
|
+
"voice_chat_status",
|
|
231
|
+
"wash_phase",
|
|
232
|
+
"wash_ready",
|
|
233
|
+
"wash_status",
|
|
234
|
+
"water_box_carriage_status",
|
|
235
|
+
"water_box_custom_mode",
|
|
236
|
+
"water_box_mode",
|
|
237
|
+
"water_box_status",
|
|
238
|
+
"water_shortage_status",
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Whether this plugin knows the named `get_status` field at all — in the
|
|
243
|
+
* pristine baseline table or behind any capability gate.
|
|
244
|
+
*
|
|
245
|
+
* Deliberately not a method on `deviceFeatures`: the question is about the
|
|
246
|
+
* plugin, not about one robot's instance, and asking an instance is what
|
|
247
|
+
* produced the wrong warning in the first place.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} attribute
|
|
250
|
+
* @returns {boolean}
|
|
251
|
+
*/
|
|
252
|
+
function isKnownStatusAttribute(attribute) {
|
|
253
|
+
return (
|
|
254
|
+
Object.prototype.hasOwnProperty.call(deviceStates, attribute) ||
|
|
255
|
+
CAPABILITY_STATUS_ATTRIBUTES.has(attribute)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
185
259
|
// Shared shape for the work-time consumables that both consumablesInt and
|
|
186
260
|
// consumablesString track identically.
|
|
187
261
|
const workTimeConsumable = {
|
|
@@ -1583,6 +1657,7 @@ function getModelNameWithoutBrand(model) {
|
|
|
1583
1657
|
module.exports = {
|
|
1584
1658
|
deviceFeatures,
|
|
1585
1659
|
errorCodes,
|
|
1660
|
+
isKnownStatusAttribute,
|
|
1586
1661
|
supportsMaxPlusFanPower,
|
|
1587
1662
|
getModelMarketingName,
|
|
1588
1663
|
getModelNameWithoutBrand,
|
|
@@ -5,6 +5,7 @@ const RRMapParser = require("./RRMapParser");
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const zlib = require("zlib");
|
|
7
7
|
const { describeDevice } = require("./describeDevice");
|
|
8
|
+
const { isKnownStatusAttribute } = require("./deviceFeatures");
|
|
8
9
|
|
|
9
10
|
// Minimum spacing between periodic (non-forced) get_status polls per robot.
|
|
10
11
|
// MQTT push remains the primary live channel; this is the safety net that
|
|
@@ -620,7 +621,12 @@ class vacuum {
|
|
|
620
621
|
`Devices.${duid}.deviceStatus.${attribute}`
|
|
621
622
|
))
|
|
622
623
|
) {
|
|
623
|
-
|
|
624
|
+
// Renamed from `isKnownStatusAttribute` on purpose: it is not
|
|
625
|
+
// what it used to be called. The feature profile answers "is
|
|
626
|
+
// this field switched on for THIS robot", which is a narrower
|
|
627
|
+
// question than "does this plugin know the field", and the old
|
|
628
|
+
// name invited the loop below to confuse the two.
|
|
629
|
+
const isEnabledForThisRobot =
|
|
624
630
|
typeof this.adapter.vacuums[duid].features
|
|
625
631
|
.hasDeviceStatusAttribute === "function" &&
|
|
626
632
|
this.adapter.vacuums[duid].features.hasDeviceStatusAttribute(
|
|
@@ -636,20 +642,45 @@ class vacuum {
|
|
|
636
642
|
// on: fifty lines a minute, and the log ring — the thing you
|
|
637
643
|
// need when something real goes wrong — held ninety minutes.
|
|
638
644
|
//
|
|
639
|
-
// The distinction below is the part that carries information
|
|
640
|
-
// and it
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
645
|
+
// The distinction below is the part that carries information,
|
|
646
|
+
// and it now has three cases rather than two. Asking only the
|
|
647
|
+
// per-robot question is what sent jcoz00 (#6) after a model
|
|
648
|
+
// report for fifteen fields that were already mapped: his Qrevo
|
|
649
|
+
// CurvX sends them, his robot's capability detection never
|
|
650
|
+
// switched them on, and the warning read that as "this plugin
|
|
651
|
+
// has no mapping for them". A warning that says "no mapping"
|
|
652
|
+
// and points at GitHub is only true of a field no table
|
|
653
|
+
// anywhere names — which for his robot was three of eighteen.
|
|
654
|
+
const isKnownToPlugin = isKnownStatusAttribute(attribute);
|
|
655
|
+
|
|
656
|
+
if (!isEnabledForThisRobot) {
|
|
657
|
+
const isFirstSighting = this.rememberUnmappedStatusAttribute(
|
|
658
|
+
duid,
|
|
659
|
+
attribute
|
|
652
660
|
);
|
|
661
|
+
|
|
662
|
+
if (isKnownToPlugin) {
|
|
663
|
+
// Worth seeing once when we go looking — a gate that did not
|
|
664
|
+
// fire for a field the robot demonstrably sends is a lead —
|
|
665
|
+
// but not worth waking a user for, and nothing they can act
|
|
666
|
+
// on. Repeats are silent rather than a debug line: the field
|
|
667
|
+
// is mapped and the gate is not going to change mid-run, so
|
|
668
|
+
// a per-poll line would be fifteen of them a minute on a
|
|
669
|
+
// Qrevo CurvX saying nothing the first one did not.
|
|
670
|
+
if (isFirstSighting) {
|
|
671
|
+
this.adapter.log.debug(
|
|
672
|
+
`get_status attribute ${attribute}=${describeStatusValue(deviceStatus[0][attribute])} arrived from ${describeDevice(this.adapter, duid)}, but this robot's capability detection did not switch it on. This plugin maps the field, so there is nothing to report.`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
} else if (isFirstSighting) {
|
|
676
|
+
newlyUnmappedAttributes.push(
|
|
677
|
+
`${attribute}=${describeStatusValue(deviceStatus[0][attribute])}`
|
|
678
|
+
);
|
|
679
|
+
} else {
|
|
680
|
+
this.adapter.log.debug(
|
|
681
|
+
`Unmapped get_status attribute ${attribute}=${describeStatusValue(deviceStatus[0][attribute])} for ${describeDevice(this.adapter, duid)}; already reported, not repeating.`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
653
684
|
}
|
|
654
685
|
continue; // skip unsupported attributes
|
|
655
686
|
}
|
|
@@ -765,6 +796,19 @@ class vacuum {
|
|
|
765
796
|
this.adapter.manageDeviceIntervals(duid);
|
|
766
797
|
}
|
|
767
798
|
} else if (parameter == "get_room_mapping") {
|
|
799
|
+
// Room data on B01/Q7 robots travels over the protobuf map channel,
|
|
800
|
+
// and `get_room_mapping` itself is answered from the dialect's neutral
|
|
801
|
+
// table without touching the network — so this branch looked free.
|
|
802
|
+
// It is not: it opens by fetching `get_status` to read `map_status`,
|
|
803
|
+
// a v1-only field that Q7 status dictionaries have never carried, and
|
|
804
|
+
// on B01 `get_status` translates to a real `prop.get`. That is one
|
|
805
|
+
// cloud round-trip per poll cycle per robot spent on an answer this
|
|
806
|
+
// code cannot read — reported under the caller's label, which is why
|
|
807
|
+
// #14's log line names `get_room_mapping` but times out on `prop.get`.
|
|
808
|
+
if (this.adapter.isB01Device?.(duid)) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
768
812
|
const deviceStatus = await sendParameterRequest("get_status", []);
|
|
769
813
|
const mapStatus = Array.isArray(deviceStatus)
|
|
770
814
|
? deviceStatus[0]?.["map_status"]
|
|
@@ -982,9 +982,7 @@ class Roborock {
|
|
|
982
982
|
}
|
|
983
983
|
|
|
984
984
|
getRoomMappingsForDevice(duid) {
|
|
985
|
-
if (
|
|
986
|
-
this.getVacuumDeviceInfo(duid, "pv") === b01Q7Adapter.B01_PROTOCOL_VERSION
|
|
987
|
-
) {
|
|
985
|
+
if (this.isB01Device(duid)) {
|
|
988
986
|
return this.getB01RoomCache(duid).map((room) => ({
|
|
989
987
|
segmentId: room.roomId,
|
|
990
988
|
mapId: 0,
|
|
@@ -1025,9 +1023,7 @@ class Roborock {
|
|
|
1025
1023
|
// the canonical mapId 0. Reporting 0 here keeps the Matter room-clean
|
|
1026
1024
|
// flow from attempting a map switch (load_multi_map has no Q7
|
|
1027
1025
|
// equivalent) before sending the segment command.
|
|
1028
|
-
if (
|
|
1029
|
-
this.getVacuumDeviceInfo(duid, "pv") === b01Q7Adapter.B01_PROTOCOL_VERSION
|
|
1030
|
-
) {
|
|
1026
|
+
if (this.isB01Device(duid)) {
|
|
1031
1027
|
return 0;
|
|
1032
1028
|
}
|
|
1033
1029
|
|
|
@@ -2308,10 +2304,7 @@ class Roborock {
|
|
|
2308
2304
|
this.vacuums[duid].getStatusIntervall = () => {
|
|
2309
2305
|
// B01/Q7 status is owned by the dedicated 15s loop; the per-device
|
|
2310
2306
|
// tick would only burn cycles hitting the attempt throttle.
|
|
2311
|
-
if (
|
|
2312
|
-
this.getVacuumDeviceInfo(duid, "pv") ===
|
|
2313
|
-
b01Q7Adapter.B01_PROTOCOL_VERSION
|
|
2314
|
-
) {
|
|
2307
|
+
if (this.isB01Device(duid)) {
|
|
2315
2308
|
return null;
|
|
2316
2309
|
}
|
|
2317
2310
|
this.clearInterval(this.vacuums[duid].getStatusIntervalHandle);
|
|
@@ -2396,9 +2389,7 @@ class Roborock {
|
|
|
2396
2389
|
// with no electronic mop/water control, so Matter must never expose mop
|
|
2397
2390
|
// modes for them regardless of what the generic cloud schema claims.
|
|
2398
2391
|
// Suction (Q7 "wind") is controllable via the B01 adapter.
|
|
2399
|
-
if (
|
|
2400
|
-
this.getVacuumDeviceInfo(duid, "pv") === b01Q7Adapter.B01_PROTOCOL_VERSION
|
|
2401
|
-
) {
|
|
2392
|
+
if (this.isB01Device(duid)) {
|
|
2402
2393
|
return {
|
|
2403
2394
|
// Q7 robots mop with a manually filled tank: expose the mop/vacuum
|
|
2404
2395
|
// mode switch, but never water-level status or control.
|
|
@@ -5070,6 +5061,22 @@ class Roborock {
|
|
|
5070
5061
|
}
|
|
5071
5062
|
}
|
|
5072
5063
|
|
|
5064
|
+
/**
|
|
5065
|
+
* True when this robot speaks the B01/Q7 dialect rather than classic v1.
|
|
5066
|
+
*
|
|
5067
|
+
* Four places wrote this comparison out by hand before it had a name, and
|
|
5068
|
+
* `vacuum.js` was about to need a fifth. It decides which wire protocol a
|
|
5069
|
+
* robot speaks, so it gets one spelling.
|
|
5070
|
+
*
|
|
5071
|
+
* @param {string} duid
|
|
5072
|
+
* @returns {boolean}
|
|
5073
|
+
*/
|
|
5074
|
+
isB01Device(duid) {
|
|
5075
|
+
return (
|
|
5076
|
+
this.getVacuumDeviceInfo(duid, "pv") === b01Q7Adapter.B01_PROTOCOL_VERSION
|
|
5077
|
+
);
|
|
5078
|
+
}
|
|
5079
|
+
|
|
5073
5080
|
/**
|
|
5074
5081
|
* A device's name for log messages, falling back to the duid.
|
|
5075
5082
|
*
|