homebridge-roborock-matter 3.4.1 → 3.4.3

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.4.3
4
+
5
+ **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.
6
+
7
+ - **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.
8
+ - **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.
9
+ - **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.
10
+ - **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.
11
+ - **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.
12
+
13
+ ## 3.4.2
14
+
15
+ **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.
16
+
17
+ - **The periodic poller now consults the dialect before asking.** For a B01 robot it skips exactly the requests that have neither a Q7 translation nor a neutral placeholder, and says so once per robot at debug level instead of once per robot as a notice. Classic S- and Q-series robots poll precisely as before.
18
+ - **The check derives from the translation table itself**, not from a hand-written list of method names — a second list would drift the first time a translation was added, and the drift would only ever show up as noise in somebody's log.
19
+ - **The poll-profile line stops promising a water-box probe it will not perform** on a robot whose water tank is filled by hand and has no electronic level to read.
20
+ - **A test enumerates the rule rather than the three reported methods:** for a B01 robot, no periodic poll may be one the dialect cannot answer. Probes added later are covered without anyone having to remember this.
21
+ - Also fixed: the B01 full-chain simulation ran under Jest's 5-second default, which quietly made suite-wide CPU load an implicit assertion — it began failing on an unrelated new test file. Its wall-clock time was never what it set out to verify.
22
+
3
23
  ## 3.4.1
4
24
 
5
25
  **The Matter fault attribute is withdrawn.** Wazza151 ran three controlled tests on an S8 Pro Ultra with a genuinely empty clean water tank ([#5](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/5)) and the result was unambiguous: Apple Home drew no warning with the fault published beside a Charging state, drew no warning with it published beside a forced Error state either, and the tile went into a stuck "Updating…" that needed a manual poke to clear. Everything off, and the tile behaved perfectly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-roborock-matter",
3
- "version": "3.4.1",
3
+ "version": "3.4.3",
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": {
@@ -714,6 +714,31 @@ function neutralResponse(method) {
714
714
  return factory ? { value: factory() } : undefined;
715
715
  }
716
716
 
717
+ /**
718
+ * True when a v1-shaped request has *any* answer on a Q7 robot — a real
719
+ * translation or a neutral placeholder. Everything else is rejected by the
720
+ * send choke point in messageQueueHandler with a B01_METHOD_UNSUPPORTED
721
+ * error, so the periodic poller can consult this instead of asking for
722
+ * something the plugin already knows will fail.
723
+ *
724
+ * Derived from the same two sources the choke point uses, deliberately: a
725
+ * separate hand-written list would drift the first time a translation is
726
+ * added, and the drift would only show up as noise in a user's log.
727
+ * @param {string} method
728
+ * @returns {boolean}
729
+ */
730
+ function canAnswerV1Method(method) {
731
+ if (NEUTRAL_RESPONSES.has(method)) {
732
+ return true;
733
+ }
734
+
735
+ try {
736
+ return Boolean(translateOutgoing(method, []));
737
+ } catch {
738
+ return false;
739
+ }
740
+ }
741
+
717
742
  /**
718
743
  * Map a Q7 `prop.get` status payload to v1-shaped status fields.
719
744
  * Fixture reference: {"status":4,"quantity":87,"fault":0,...}
@@ -796,5 +821,6 @@ module.exports = {
796
821
  createB01MessageId,
797
822
  translateOutgoing,
798
823
  neutralResponse,
824
+ canAnswerV1Method,
799
825
  mapStatusToV1,
800
826
  };
@@ -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 };
@@ -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 device ${duid}: no localKey available.`
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. The device ${duid} is offline.`
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(`Device ${duid} is online.`);
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
- `Device ${duid} firmware update status: ${otaStatus}`
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
- `Device ${duid} firmware update progress: ${otaProgress}%`
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.warn(
417
- `Unsupported attribute: ${attribute} of get_status with value ${deviceStatus[0][attribute]}. Please contact the dev to add the newly found attribute of your robot. Model: ${this.robotModel}`
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
  }
@@ -201,6 +201,7 @@ class Roborock {
201
201
  // repeated warnings for requests they will never answer.
202
202
  this.unsupportedPollCommands = new Set();
203
203
  this.loggedPollProfiles = new Set();
204
+ this.skippedDialectPolls = new Set();
204
205
  this.baseURL = options.baseURL || "usiot.roborock.com";
205
206
 
206
207
  this.userData = options.userData || null;
@@ -2470,6 +2471,29 @@ class Roborock {
2470
2471
  return true;
2471
2472
  }
2472
2473
 
2474
+ /**
2475
+ * Poll one v1 parameter, unless the robot speaks a dialect that has no
2476
+ * answer for it. Every periodic probe goes through here so the rule holds
2477
+ * for probes added later, not just the three that were reported.
2478
+ * @param {string} duid @param {any} vacuum @param {string} method
2479
+ * @param {boolean} isB01
2480
+ * @returns {Promise<any>}
2481
+ */
2482
+ async pollParameter(duid, vacuum, method, isB01) {
2483
+ if (isB01 && !b01Q7Adapter.canAnswerV1Method(method)) {
2484
+ const key = `${duid}:${method}`;
2485
+ if (!this.skippedDialectPolls.has(key)) {
2486
+ this.skippedDialectPolls.add(key);
2487
+ this.log.debug(
2488
+ `Not polling '${method}' for ${this.describeDevice(duid)}: the Q7/B01 dialect has no equivalent request, so the robot could only ever reject it.`
2489
+ );
2490
+ }
2491
+ return undefined;
2492
+ }
2493
+
2494
+ return vacuum.getParameter(duid, method);
2495
+ }
2496
+
2473
2497
  startMainUpdateInterval(duid, online) {
2474
2498
  if (!this.hasInitializedVacuum(duid)) {
2475
2499
  return;
@@ -2628,18 +2652,26 @@ class Roborock {
2628
2652
  this.log.debug(`Latest data requested`);
2629
2653
 
2630
2654
  if (this.isSupportedVacuumModel(robotModel)) {
2655
+ // Q7-series robots speak the B01 dialect, where a good half of the v1
2656
+ // poll chain has no equivalent request at all. Asking anyway produced
2657
+ // an "unsupported" notice per robot per restart for a request the
2658
+ // plugin itself rejected before it ever reached the robot.
2659
+ const isB01 = b01Q7Adapter.isB01Protocol(
2660
+ await this.getRobotVersion(duid)
2661
+ );
2662
+
2631
2663
  const refreshedServiceAreaRooms =
2632
2664
  await this.refreshMatterServiceAreaRoomMappings(duid, vacuum);
2633
2665
 
2634
2666
  if (!refreshedServiceAreaRooms) {
2635
- await vacuum.getParameter(duid, "get_room_mapping");
2667
+ await this.pollParameter(duid, vacuum, "get_room_mapping", isB01);
2636
2668
  }
2637
2669
 
2638
- await vacuum.getParameter(duid, "get_consumable");
2670
+ await this.pollParameter(duid, vacuum, "get_consumable", isB01);
2639
2671
 
2640
- await vacuum.getParameter(duid, "get_server_timer");
2672
+ await this.pollParameter(duid, vacuum, "get_server_timer", isB01);
2641
2673
 
2642
- await vacuum.getParameter(duid, "get_timer");
2674
+ await this.pollParameter(duid, vacuum, "get_timer", isB01);
2643
2675
 
2644
2676
  await this.checkForNewFirmware(duid);
2645
2677
 
@@ -2657,13 +2689,28 @@ class Roborock {
2657
2689
  //do nothing
2658
2690
  break;
2659
2691
  case "roborock.vacuum.s6":
2660
- await vacuum.getParameter(duid, "get_carpet_mode");
2692
+ await this.pollParameter(duid, vacuum, "get_carpet_mode", isB01);
2661
2693
  break;
2662
2694
  case "roborock.vacuum.a27":
2663
- await vacuum.getParameter(duid, "get_dust_collection_switch_status");
2664
- await vacuum.getParameter(duid, "get_wash_towel_mode");
2665
- await vacuum.getParameter(duid, "get_smart_wash_params");
2666
- await vacuum.getParameter(duid, "app_get_dryer_setting");
2695
+ await this.pollParameter(
2696
+ duid,
2697
+ vacuum,
2698
+ "get_dust_collection_switch_status",
2699
+ isB01
2700
+ );
2701
+ await this.pollParameter(duid, vacuum, "get_wash_towel_mode", isB01);
2702
+ await this.pollParameter(
2703
+ duid,
2704
+ vacuum,
2705
+ "get_smart_wash_params",
2706
+ isB01
2707
+ );
2708
+ await this.pollParameter(
2709
+ duid,
2710
+ vacuum,
2711
+ "app_get_dryer_setting",
2712
+ isB01
2713
+ );
2667
2714
  break;
2668
2715
  default: {
2669
2716
  // No dedicated poll profile for this model: derive it from the
@@ -2675,18 +2722,31 @@ class Roborock {
2675
2722
  const carpetSupported = featureList
2676
2723
  ? Boolean(featureList.isCarpetSupported)
2677
2724
  : true;
2725
+ const waterBoxProbe =
2726
+ !isB01 ||
2727
+ b01Q7Adapter.canAnswerV1Method("get_water_box_custom_mode");
2678
2728
  const profileKey = `${duid}:${robotModel}`;
2679
2729
  if (!this.loggedPollProfiles.has(profileKey)) {
2680
2730
  this.loggedPollProfiles.add(profileKey);
2681
2731
  this.log.info(
2682
- `No dedicated poll profile for model '${robotModel}'; using ${featureList ? "capability-derived" : "generic"} polls (carpet=${carpetSupported ? "yes" : "no"}, water-box probe=yes). Requests the robot reports as unsupported are disabled automatically. If states look wrong for this model, please open a model report issue on GitHub.`
2732
+ `No dedicated poll profile for model '${robotModel}'; using ${featureList ? "capability-derived" : "generic"} polls (carpet=${carpetSupported ? "yes" : "no"}, water-box probe=${waterBoxProbe ? "yes" : "no, the Q7/B01 dialect has no such request"}). Requests the robot reports as unsupported are disabled automatically. If states look wrong for this model, please open a model report issue on GitHub.`
2683
2733
  );
2684
2734
  }
2685
2735
  if (carpetSupported) {
2686
- await vacuum.getParameter(duid, "get_carpet_mode");
2687
- await vacuum.getParameter(duid, "get_carpet_clean_mode");
2736
+ await this.pollParameter(duid, vacuum, "get_carpet_mode", isB01);
2737
+ await this.pollParameter(
2738
+ duid,
2739
+ vacuum,
2740
+ "get_carpet_clean_mode",
2741
+ isB01
2742
+ );
2688
2743
  }
2689
- await vacuum.getParameter(duid, "get_water_box_custom_mode");
2744
+ await this.pollParameter(
2745
+ duid,
2746
+ vacuum,
2747
+ "get_water_box_custom_mode",
2748
+ isB01
2749
+ );
2690
2750
  }
2691
2751
  }
2692
2752
  } else {