homebridge-roborock-matter 3.4.2 → 3.4.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 +14 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/roborockLib/lib/describeDevice.js +33 -0
- package/roborockLib/lib/deviceFeatures.js +21 -0
- package/roborockLib/lib/message.js +3 -2
- package/roborockLib/lib/roborock_mqtt_connector.js +7 -6
- package/roborockLib/lib/vacuum.js +93 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.4.4
|
|
4
|
+
|
|
5
|
+
- **Nine Saros 10 status fields are now mapped.** 3.4.3 started naming unknown `get_status` fields once instead of once a minute, and asked owners to paste that line into a model report. skmzwanke did exactly that for his `roborock.vacuum.a144` ([#8](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/8)), so `home_sec_status`, `voice_chat_status`, `home_sec_enable_password`, `extra_time`, `sterilize_status`, `rst`, `cleaning_info`, `exit_dock` and `seq_type` are known from now on and his log is quiet. None of them drive behaviour — control, battery, rooms and state come from a model-agnostic path — so this is purely about not pestering the owner of a new model about fields the plugin had simply never met.
|
|
6
|
+
|
|
7
|
+
## 3.4.3
|
|
8
|
+
|
|
9
|
+
**A robot with no model profile no longer fills your log with the same request forever.** Models the plugin has no dedicated profile for — the Saros 10 in [#8](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/8), the Qrevo CurvX in [#6](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/6) — run on the capability-derived path, which works, but every status field the profile did not name produced its own `Unsupported attribute … Please contact the dev` warning on every status poll. For the Saros 10 that is eight warnings a minute: about 11,500 requests a day to report the same eight fields. Both issues were promised this would be quietened.
|
|
10
|
+
|
|
11
|
+
- **Each unmapped field is now reported once per robot, in one line, and then never again.** A firmware's set of unmapped fields is fixed, so a repeat says nothing a user can act on. Repeats go to debug. A time-based throttle was the wrong shape: it would bring the message back forever, which is the complaint rather than the fix.
|
|
12
|
+
- **A field nobody has seen before still gets through.** If a robot starts sending something new after hours of uptime, it is reported on its own — quietening the noise must not also hide the signal, since these lines are the raw material for a model profile.
|
|
13
|
+
- **The line is now usable in a model report.** It names the robot and model, lists every field with its value, and says plainly that control, battery, rooms and state do not depend on them. Object values are serialised instead of arriving as `[object Object]`, which is what `cleaning_info` looked like in #8 — the one field where the shape was the interesting part.
|
|
14
|
+
- **Two startup tests no longer assert on the clock.** Both checked that per-robot probes run concurrently by timing them against a 180 ms budget — and on a quiet machine they finish in ~65 ms, so the assertion could only ever fail for a reason it was not testing. A scheduling hiccup was enough to fail a build with the concurrency perfectly correct. The check was also redundant: serialized probes give a peak concurrency of 1, which the neighbouring assertion already catches exactly. They now assert the property directly — every probe started before the first one finished — which holds on any machine under any load. Same defect 3.4.2 removed from the B01 full-chain simulation.
|
|
15
|
+
- **The log-naming rule from 3.3.2 was itself only half enumerated.** It listed three files by hand, and the files it left out held twelve log lines still printing a bare 22-character duid — including `Device <duid> is offline.`, which is exactly the line someone quotes when asking why a robot dropped out. A hand-written file list is the same mistake as a hand-written line list, one level up. The rule now discovers the file list from the source tree, so a new file is covered the moment it exists, and all twelve lines now name the robot.
|
|
16
|
+
|
|
3
17
|
## 3.4.2
|
|
4
18
|
|
|
5
19
|
**Q7-series robots are no longer asked for things they cannot answer.** Every restart, each Q7-generation robot (`roborock.vacuum.sc05`, `ss07`, and the rest of the B01 family) logged an unsupported-method notice — most visibly for `get_water_box_custom_mode`, and also for `get_timer` and `get_carpet_clean_mode`. The message blamed the robot, and the robot was never involved: the plugin's own send path rejects v1-only requests for B01 devices before anything reaches the network, and the poller then recorded that self-rejection as though the robot had answered 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. 419 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.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": {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Friendly name for a robot, for use in user-visible log lines.
|
|
5
|
+
*
|
|
6
|
+
* `roborockAPI` has had its own `describeDevice` for a while, but the library
|
|
7
|
+
* modules underneath it only have the adapter reference, so their log lines
|
|
8
|
+
* kept printing the raw duid — a 22-character opaque string that tells the
|
|
9
|
+
* reader nothing, in exactly the messages people are asked to paste into an
|
|
10
|
+
* issue. This is the shared wrapper so the answer to "how is a robot named to
|
|
11
|
+
* a human" lives in one place rather than being re-decided per module.
|
|
12
|
+
*
|
|
13
|
+
* The fallback matters: these lines are reachable before HomeData has been
|
|
14
|
+
* fetched, and a log line is worth more than a crash, so an adapter without
|
|
15
|
+
* the helper (or a robot with no cached name) degrades to the duid.
|
|
16
|
+
*
|
|
17
|
+
* @param {{ describeDevice?: (duid: string) => string } | null | undefined} adapter
|
|
18
|
+
* @param {string} duid
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
function describeDevice(adapter, duid) {
|
|
22
|
+
if (adapter && typeof adapter.describeDevice === "function") {
|
|
23
|
+
const name = adapter.describeDevice(duid);
|
|
24
|
+
|
|
25
|
+
if (typeof name === "string" && name.length > 0) {
|
|
26
|
+
return name;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return String(duid);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { describeDevice };
|
|
@@ -153,6 +153,27 @@ const deviceStates = {
|
|
|
153
153
|
rdt: "number",
|
|
154
154
|
camera_status: "number",
|
|
155
155
|
distance_off: "number",
|
|
156
|
+
// Reported by the Saros 10 (roborock.vacuum.a144) and siblings. Added from
|
|
157
|
+
// skmzwanke's field report in issue #8, which is exactly the round trip the
|
|
158
|
+
// "no mapping for" log line asks for: 3.4.3 named the nine fields once, he
|
|
159
|
+
// pasted the line, and they are now known.
|
|
160
|
+
//
|
|
161
|
+
// None of them drive behaviour — control, battery, rooms and state come
|
|
162
|
+
// from a model-agnostic path — so mapping them is about not pestering the
|
|
163
|
+
// owner of a brand-new model about fields the plugin simply had not met
|
|
164
|
+
// yet.
|
|
165
|
+
home_sec_status: "number",
|
|
166
|
+
home_sec_enable_password: "number",
|
|
167
|
+
voice_chat_status: "number",
|
|
168
|
+
sterilize_status: "number",
|
|
169
|
+
exit_dock: "number",
|
|
170
|
+
extra_time: "number",
|
|
171
|
+
seq_type: "number",
|
|
172
|
+
rst: "number",
|
|
173
|
+
// An object: {target_segment_id, segment_id, fan_power, water_box_mode, ...}
|
|
174
|
+
// — the same convention as adbumper_status and events, which are also
|
|
175
|
+
// non-scalar.
|
|
176
|
+
cleaning_info: "string",
|
|
156
177
|
};
|
|
157
178
|
|
|
158
179
|
// Shared shape for the work-time consumables that both consumablesInt and
|
|
@@ -4,6 +4,7 @@ const crypto = require("crypto");
|
|
|
4
4
|
const CRC32 = require("crc-32");
|
|
5
5
|
const Parser = require("binary-parser").Parser;
|
|
6
6
|
const roborockCrypto = require("./roborockCrypto");
|
|
7
|
+
const { describeDevice } = require("./describeDevice");
|
|
7
8
|
|
|
8
9
|
let seq = 1;
|
|
9
10
|
let random = 4711; // Should be initialized with a number 0 - 1999?
|
|
@@ -243,7 +244,7 @@ class message {
|
|
|
243
244
|
if (!this.missingLocalKeyWarnings.has(duid)) {
|
|
244
245
|
this.missingLocalKeyWarnings.add(duid);
|
|
245
246
|
this.adapter.log.warn(
|
|
246
|
-
`Skipping MQTT message for
|
|
247
|
+
`Skipping MQTT message for ${describeDevice(this.adapter, duid)}: no localKey available.`
|
|
247
248
|
);
|
|
248
249
|
}
|
|
249
250
|
|
|
@@ -321,7 +322,7 @@ class message {
|
|
|
321
322
|
.toString("hex");
|
|
322
323
|
const reason = error && error.message ? error.message : String(error);
|
|
323
324
|
this.adapter.log.error(
|
|
324
|
-
`failed to _decodeMsg for ${duid}: ${reason} (len=${message.length}, preview=${preview})`
|
|
325
|
+
`failed to _decodeMsg for ${describeDevice(this.adapter, duid)}: ${reason} (len=${message.length}, preview=${preview})`
|
|
325
326
|
);
|
|
326
327
|
// this.adapter.catchError(error, "_decodeMessage", "none");
|
|
327
328
|
return null;
|
|
@@ -5,6 +5,7 @@ const crypto = require("crypto");
|
|
|
5
5
|
const Parser = require("binary-parser").Parser;
|
|
6
6
|
const zlib = require("zlib");
|
|
7
7
|
const roborockCrypto = require("./roborockCrypto");
|
|
8
|
+
const { describeDevice } = require("./describeDevice");
|
|
8
9
|
|
|
9
10
|
const PHOTO_MAGIC = "ROBOROCK";
|
|
10
11
|
const PHOTO_HEADER_MIN_LENGTH = 9;
|
|
@@ -499,7 +500,7 @@ class roborock_mqtt_connector {
|
|
|
499
500
|
} catch (error) {
|
|
500
501
|
// If parsing fails, the data might be corrupted or in an unexpected format
|
|
501
502
|
this.adapter.log.warn(
|
|
502
|
-
`Unable to parse message for ${duid}. Error: ${error.message}. Data: ${dataString}`
|
|
503
|
+
`Unable to parse message for ${describeDevice(this.adapter, duid)}. Error: ${error.message}. Data: ${dataString}`
|
|
503
504
|
);
|
|
504
505
|
return;
|
|
505
506
|
}
|
|
@@ -507,10 +508,10 @@ class roborock_mqtt_connector {
|
|
|
507
508
|
// Check if the device is online
|
|
508
509
|
if (parsedData.online == false) {
|
|
509
510
|
this.adapter.log.info(
|
|
510
|
-
`Couldn't process message.
|
|
511
|
+
`Couldn't process message. ${describeDevice(this.adapter, duid)} is offline.`
|
|
511
512
|
);
|
|
512
513
|
} else if (parsedData.online == true) {
|
|
513
|
-
// this.adapter.log.info(
|
|
514
|
+
// this.adapter.log.info(`${describeDevice(this.adapter, duid)} is online.`);
|
|
514
515
|
} else if (
|
|
515
516
|
// Check for firmware update information
|
|
516
517
|
parsedData.mqttOtaData
|
|
@@ -521,19 +522,19 @@ class roborock_mqtt_connector {
|
|
|
521
522
|
|
|
522
523
|
if (otaStatus) {
|
|
523
524
|
this.adapter.log.info(
|
|
524
|
-
|
|
525
|
+
`${describeDevice(this.adapter, duid)} firmware update status: ${otaStatus}`
|
|
525
526
|
);
|
|
526
527
|
}
|
|
527
528
|
|
|
528
529
|
if (otaProgress !== undefined) {
|
|
529
530
|
this.adapter.log.info(
|
|
530
|
-
|
|
531
|
+
`${describeDevice(this.adapter, duid)} firmware update progress: ${otaProgress}%`
|
|
531
532
|
);
|
|
532
533
|
}
|
|
533
534
|
} else {
|
|
534
535
|
// Received an unrecognized message
|
|
535
536
|
this.adapter.log.warn(
|
|
536
|
-
`Received an unrecognized message for ${duid}. Data: ${dataString}`
|
|
537
|
+
`Received an unrecognized message for ${describeDevice(this.adapter, duid)}. Data: ${dataString}`
|
|
537
538
|
);
|
|
538
539
|
}
|
|
539
540
|
} else {
|
|
@@ -4,12 +4,45 @@ const rrMessage = require("./message").message;
|
|
|
4
4
|
const RRMapParser = require("./RRMapParser");
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const zlib = require("zlib");
|
|
7
|
+
const { describeDevice } = require("./describeDevice");
|
|
7
8
|
|
|
8
9
|
// Minimum spacing between periodic (non-forced) get_status polls per robot.
|
|
9
10
|
// MQTT push remains the primary live channel; this is the safety net that
|
|
10
11
|
// catches a dropped push long before the 3-minute full refresh would.
|
|
11
12
|
const STATUS_POLL_MIN_INTERVAL_MS = 60 * 1000;
|
|
12
13
|
|
|
14
|
+
// Longest rendering of a single unmapped attribute value in the report line
|
|
15
|
+
// below. `cleaning_info` is an object, and one robot's status can carry a
|
|
16
|
+
// nested blob big enough to bury the rest of the message.
|
|
17
|
+
const MAX_REPORTED_STATUS_VALUE_LENGTH = 60;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Render a `get_status` value for a log line. The old per-attribute warning
|
|
21
|
+
* interpolated the raw value, so an object arrived as the useless
|
|
22
|
+
* `[object Object]` — visible in skmzwanke's log for `cleaning_info`, which is
|
|
23
|
+
* the one field where the shape was the interesting part.
|
|
24
|
+
*
|
|
25
|
+
* @param {unknown} value
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function describeStatusValue(value) {
|
|
29
|
+
let text;
|
|
30
|
+
|
|
31
|
+
if (value === null || typeof value !== "object") {
|
|
32
|
+
text = String(value);
|
|
33
|
+
} else {
|
|
34
|
+
try {
|
|
35
|
+
text = JSON.stringify(value);
|
|
36
|
+
} catch {
|
|
37
|
+
text = "[unserialisable]";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return text.length > MAX_REPORTED_STATUS_VALUE_LENGTH
|
|
42
|
+
? `${text.slice(0, MAX_REPORTED_STATUS_VALUE_LENGTH)}…`
|
|
43
|
+
: text;
|
|
44
|
+
}
|
|
45
|
+
|
|
13
46
|
const mappedCleanSummary = {
|
|
14
47
|
0: "clean_time",
|
|
15
48
|
1: "clean_area",
|
|
@@ -55,6 +88,41 @@ class vacuum {
|
|
|
55
88
|
|
|
56
89
|
/** @type {Map<string, number>} last periodic status poll, per duid */
|
|
57
90
|
this.lastStatusPollAt = new Map();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* `get_status` attributes with no mapping in the robot's feature profile
|
|
94
|
+
* that have already been reported, per duid. The set of unmapped fields a
|
|
95
|
+
* given robot sends is fixed by its firmware, so reporting it once says
|
|
96
|
+
* everything a repeat would; a time-based throttle would still bring the
|
|
97
|
+
* message back forever, which is the complaint, not the fix.
|
|
98
|
+
*
|
|
99
|
+
* @type {Map<string, Set<string>>}
|
|
100
|
+
*/
|
|
101
|
+
this.reportedUnmappedStatusAttributes = new Map();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Record that an unmapped `get_status` attribute is about to be reported for
|
|
106
|
+
* a robot, and say whether this is the initial sighting.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} duid
|
|
109
|
+
* @param {string} attribute
|
|
110
|
+
* @returns {boolean} true only the one time the pair has not been seen before
|
|
111
|
+
*/
|
|
112
|
+
rememberUnmappedStatusAttribute(duid, attribute) {
|
|
113
|
+
let reported = this.reportedUnmappedStatusAttributes.get(duid);
|
|
114
|
+
|
|
115
|
+
if (!reported) {
|
|
116
|
+
reported = new Set();
|
|
117
|
+
this.reportedUnmappedStatusAttributes.set(duid, reported);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (reported.has(attribute)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
reported.add(attribute);
|
|
125
|
+
return true;
|
|
58
126
|
}
|
|
59
127
|
|
|
60
128
|
/**
|
|
@@ -182,7 +250,7 @@ class vacuum {
|
|
|
182
250
|
|
|
183
251
|
if (roomList.segments.length === 0) {
|
|
184
252
|
this.adapter.log.warn(
|
|
185
|
-
`No room segments supplied for app_segment_clean_by_ids on ${duid}.`
|
|
253
|
+
`No room segments supplied for app_segment_clean_by_ids on ${describeDevice(this.adapter, duid)}.`
|
|
186
254
|
);
|
|
187
255
|
break;
|
|
188
256
|
}
|
|
@@ -221,7 +289,7 @@ class vacuum {
|
|
|
221
289
|
const mapId = Number(value);
|
|
222
290
|
if (!Number.isInteger(mapId) || mapId < 0) {
|
|
223
291
|
this.adapter.log.warn(
|
|
224
|
-
`Invalid map id '${value}' supplied for load_multi_map on ${duid}.`
|
|
292
|
+
`Invalid map id '${value}' supplied for load_multi_map on ${describeDevice(this.adapter, duid)}.`
|
|
225
293
|
);
|
|
226
294
|
break;
|
|
227
295
|
}
|
|
@@ -391,6 +459,12 @@ class vacuum {
|
|
|
391
459
|
status: deviceStatus[0] || null,
|
|
392
460
|
});
|
|
393
461
|
|
|
462
|
+
// Collected across the whole poll and reported as one line. Eight
|
|
463
|
+
// separate warnings, once a minute, was ~11,500 identical requests a
|
|
464
|
+
// day to contact the dev about the same eight fields (#8).
|
|
465
|
+
/** @type {string[]} */
|
|
466
|
+
const newlyUnmappedAttributes = [];
|
|
467
|
+
|
|
394
468
|
for (const attribute in deviceStatus[0]) {
|
|
395
469
|
const isCleaning = this.adapter.isCleaning(
|
|
396
470
|
deviceStatus[0]["state"]
|
|
@@ -412,9 +486,15 @@ class vacuum {
|
|
|
412
486
|
this.adapter.log.debug(
|
|
413
487
|
`Skipping known get_status attribute without a Homebridge state object: ${attribute}. Model: ${this.robotModel}`
|
|
414
488
|
);
|
|
489
|
+
} else if (
|
|
490
|
+
this.rememberUnmappedStatusAttribute(duid, attribute)
|
|
491
|
+
) {
|
|
492
|
+
newlyUnmappedAttributes.push(
|
|
493
|
+
`${attribute}=${describeStatusValue(deviceStatus[0][attribute])}`
|
|
494
|
+
);
|
|
415
495
|
} else {
|
|
416
|
-
this.adapter.log.
|
|
417
|
-
`
|
|
496
|
+
this.adapter.log.debug(
|
|
497
|
+
`Unmapped get_status attribute ${attribute}=${describeStatusValue(deviceStatus[0][attribute])} for ${describeDevice(this.adapter, duid)}; already reported, not repeating.`
|
|
418
498
|
);
|
|
419
499
|
}
|
|
420
500
|
continue; // skip unsupported attributes
|
|
@@ -511,6 +591,13 @@ class vacuum {
|
|
|
511
591
|
{ val: deviceStatus[0][attribute], ack: true }
|
|
512
592
|
);
|
|
513
593
|
}
|
|
594
|
+
|
|
595
|
+
if (newlyUnmappedAttributes.length > 0) {
|
|
596
|
+
this.adapter.log.warn(
|
|
597
|
+
`${describeDevice(this.adapter, duid)} (${this.robotModel}) sends ${newlyUnmappedAttributes.length} get_status field(s) this plugin has no mapping for: ${newlyUnmappedAttributes.join(", ")}. Control, battery, rooms and state come from a model-agnostic path and do not depend on them, so nothing is broken — but a model report issue on GitHub quoting this line is how they get added. Logged once per field per robot, so it will not repeat.`
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
514
601
|
this.adapter.manageDeviceIntervals(duid);
|
|
515
602
|
}
|
|
516
603
|
} else if (parameter == "get_room_mapping") {
|
|
@@ -536,7 +623,7 @@ class vacuum {
|
|
|
536
623
|
// if no rooms have been named, processing them can't work
|
|
537
624
|
if (!Array.isArray(mappedRooms) || mappedRooms.length < 1) {
|
|
538
625
|
this.adapter.log.info(
|
|
539
|
-
`No room mappings returned for ${duid}. Room-based controls will stay unavailable until the Roborock app exposes named rooms.`
|
|
626
|
+
`No room mappings returned for ${describeDevice(this.adapter, duid)}. Room-based controls will stay unavailable until the Roborock app exposes named rooms.`
|
|
540
627
|
);
|
|
541
628
|
} else {
|
|
542
629
|
let unnamedRooms = 0;
|
|
@@ -566,7 +653,7 @@ class vacuum {
|
|
|
566
653
|
|
|
567
654
|
if (unnamedRooms > 0) {
|
|
568
655
|
this.adapter.log.info(
|
|
569
|
-
`${unnamedRooms} room(s) for ${duid} were missing names from HomeData. Using fallback labels like 'Room <id>' until the Roborock app syncs names.`
|
|
656
|
+
`${unnamedRooms} room(s) for ${describeDevice(this.adapter, duid)} were missing names from HomeData. Using fallback labels like 'Room <id>' until the Roborock app syncs names.`
|
|
570
657
|
);
|
|
571
658
|
}
|
|
572
659
|
}
|