homebridge-roborock-matter 3.4.5 → 3.4.6

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.4.6
4
+
5
+ **Trying cloud-only mode once marked a robot "Cloud only" forever.** jawnlydon reported in [#7](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/7) that a "Cloud Only" instance of his robot "seems to have stuck around through several re-pairs" — surviving a bridge restart, repeated Apple Home re-pairings, and a complete uninstall and reinstall of the plugin. It was not a leftover accessory. It was one stale field.
6
+
7
+ - **The markers cloud-only mode writes are now retracted when it is switched off.** Enabling the mode stamps a robot's transport diagnostics to say local transport is disabled, and those diagnostics are persisted. `tcpConnectionState` is only ever rewritten when a LAN connection is actually attempted — and none is attempted for a robot no local IP was discovered for — so for such a robot the marker stayed on disk permanently, outliving the setting that wrote it. Startup now reconciles the markers in both directions, clearing only the fields that still hold the marker value so a live LAN connection is never stomped.
8
+ - **The report stopped contradicting itself.** Reading that stale marker back, the device card said `connectionStatus: Cloud only` with the hint "Cloud-only mode is enabled, so local LAN discovery and local TCP control are disabled" — two lines under the same report's `cloudOnlyMode: disabled`. The report was pointing at a setting that was off, and it cost its reader an evening.
9
+ - **Setting and clearing derive from one table**, so a marker added later is retracted later. A hand-written list of fields to put back is the same mistake as a hand-written list of files or log lines, one level down — the lesson 3.4.3 and 3.4.5 each learned in their own layer.
10
+ - **`cloudOnlyMode` in the diagnostic report now quotes the saved config, not the checkbox.** The `matterFeatures` line was fixed for exactly this reason and the fix stopped at that line, leaving the line directly above it still reading its form control — so a report could state a setting the running plugin did not have. A test now enumerates the rule over the source: nothing the report builder reaches may read the settings form, except the helpers whose whole job is to warn that the form and the saved config disagree. Unsaved edits to cloud-only mode now raise that warning too.
11
+
3
12
  ## 3.4.5
4
13
 
5
14
  **The one log line that exists to diagnose Apple Home display problems was hiding the transitions.** `Matter publish for <robot>: battery=…, operationalState=…, runMode=…, cleanMode=…` was added so an Apple Home display issue could be settled from a single log excerpt — and it was only ever emitted when the **battery** value changed. skmzwanke reported in [#8](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/8) that his Apple Home tile sat on "Traveling to Room"/"Preparing" for an entire cleaning run, and sent a log covering that whole run: every operational-state transition in it was invisible, because the line only appeared on the four polls where the battery happened to tick down. The log contained the answer and could not show it.
@@ -252,21 +252,64 @@ function getCloudOnlyMode() {
252
252
  *
253
253
  * @returns {Promise<string>}
254
254
  */
255
- async function describeEnabledMatterFeatures() {
256
- let config = null;
255
+ /**
256
+ * The plugin config as SAVED, or null when it cannot be read.
257
+ *
258
+ * Every settings-derived line in the diagnostic report goes through here. The
259
+ * form is only ever consulted afterwards, to warn that it disagrees.
260
+ *
261
+ * @returns {Promise<Record<string, any> | null>}
262
+ */
263
+ async function readSavedPluginConfig() {
257
264
  try {
258
265
  const configs =
259
266
  window.homebridge &&
260
267
  typeof window.homebridge.getPluginConfig === "function"
261
268
  ? await window.homebridge.getPluginConfig()
262
269
  : null;
263
- config = configs
264
- ? configs.find((entry) => entry.platform === "RoborockVacuumPlatform")
270
+ return configs
271
+ ? configs.find((entry) => entry.platform === "RoborockVacuumPlatform") ||
272
+ null
265
273
  : null;
266
274
  } catch {
275
+ return null;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Cloud-only mode as the RUNNING plugin has it.
281
+ *
282
+ * This read the checkbox until 3.4.6, which is the same trap the matterFeatures
283
+ * line was fixed for one line below — fixing the line in front of me instead of
284
+ * the rule. A user who had tried cloud-only mode and switched it off again got
285
+ * a report whose settings line said `disabled` while the device below it said
286
+ * "Cloud only", and spent an evening hunting a ghost setting.
287
+ *
288
+ * @returns {Promise<string>}
289
+ */
290
+ async function describeSavedCloudOnlyMode() {
291
+ const config = await readSavedPluginConfig();
292
+ if (!config) {
267
293
  return "unavailable";
268
294
  }
269
295
 
296
+ const label = config.cloudOnlyMode === true ? "enabled" : "disabled";
297
+ return hasUnsavedCloudOnlyEdit(config)
298
+ ? `${label} (WARNING: the settings form has unsaved changes; this is the value the plugin is running)`
299
+ : label;
300
+ }
301
+
302
+ /** True when the cloud-only checkbox differs from what is saved in the config. */
303
+ function hasUnsavedCloudOnlyEdit(config) {
304
+ return (
305
+ Boolean(elements.cloudOnlyMode && elements.cloudOnlyMode.checked) !==
306
+ (config.cloudOnlyMode === true)
307
+ );
308
+ }
309
+
310
+ async function describeEnabledMatterFeatures() {
311
+ const config = await readSavedPluginConfig();
312
+
270
313
  if (!config) {
271
314
  return "unavailable";
272
315
  }
@@ -1114,7 +1157,7 @@ async function buildDiagnosticsReport(result) {
1114
1157
  `nodeVersion: ${result.nodeVersion || "unknown"}`,
1115
1158
  `token: ${hasToken ? "present" : "missing"}`,
1116
1159
  `homeData: ${result.hasHomeData ? "present" : "missing"}`,
1117
- `cloudOnlyMode: ${getCloudOnlyMode() ? "enabled" : "disabled"}`,
1160
+ `cloudOnlyMode: ${await describeSavedCloudOnlyMode()}`,
1118
1161
  // Which Apple Home features are switched on decides what the plugin is
1119
1162
  // even allowed to publish, so a report that omits them cannot answer
1120
1163
  // "why doesn't Apple Home show X?" — the first question every one of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-roborock-matter",
3
- "version": "3.4.5",
3
+ "version": "3.4.6",
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": {
@@ -38,6 +38,27 @@ const B01_LIVE_ROOM_FETCH_V1_STATES = new Set([5, 11, 16, 17, 18]);
38
38
  // cached live room is cleared so a later run starts fresh.
39
39
  const B01_LIVE_ROOM_CLEAR_V1_STATES = new Set([3, 8]);
40
40
 
41
+ // The transport-diagnostics fields cloud-only mode owns, and the marker value
42
+ // it writes into each.
43
+ //
44
+ // These diagnostics are persisted, so the markers outlive the setting unless
45
+ // something puts them back. `tcpConnectionState` is the one that stuck: it is
46
+ // only rewritten when a LAN connection is actually attempted, and none is
47
+ // attempted for a robot no local IP was ever discovered for. A user who tried
48
+ // cloud-only mode once and switched it off again kept "Cloud only" on the
49
+ // device card through restarts, re-pairs and a full plugin reinstall, and the
50
+ // diagnostic report told them cloud-only mode was enabled two lines under its
51
+ // own `cloudOnlyMode: disabled`.
52
+ //
53
+ // Setting and clearing both derive from this table on purpose. A hand-written
54
+ // list of fields to clear is the same mistake as a hand-written list of files
55
+ // or log lines one level up: it is correct until someone adds a fourth marker.
56
+ const CLOUD_ONLY_TRANSPORT_MARKERS = Object.freeze({
57
+ lastTransportReason: "cloud-only-mode",
58
+ localDiscoveryState: "disabled",
59
+ tcpConnectionState: "disabled",
60
+ });
61
+
41
62
  // Minimum gap between live-room map fetch attempts while cleaning. The map
42
63
  // payload is an order of magnitude heavier than get_status, so it rides a
43
64
  // slower cadence than the active status polls — but 20s meant a robot could
@@ -896,6 +917,49 @@ class Roborock {
896
917
  });
897
918
  }
898
919
 
920
+ /**
921
+ * Reconcile one robot's cloud-only transport markers with the mode as it is
922
+ * configured right now.
923
+ *
924
+ * Enabling stamps every marker in CLOUD_ONLY_TRANSPORT_MARKERS. Disabling
925
+ * clears exactly those fields that still hold the marker value, so a LAN
926
+ * connection that came up in the meantime is never stomped, and a robot that
927
+ * has no diagnostics yet is not given any.
928
+ *
929
+ * @param {string} duid
930
+ * @param {boolean} cloudOnly
931
+ */
932
+ async syncCloudOnlyTransportMarkers(duid, cloudOnly) {
933
+ if (!duid) {
934
+ return;
935
+ }
936
+
937
+ if (cloudOnly) {
938
+ await this.updateTransportDiagnostics(duid, {
939
+ ...CLOUD_ONLY_TRANSPORT_MARKERS,
940
+ });
941
+ return;
942
+ }
943
+
944
+ const entry = this.getTransportDiagnostics()[duid];
945
+ if (!entry || typeof entry != "object") {
946
+ return;
947
+ }
948
+
949
+ const cleared = {};
950
+ for (const [field, marker] of Object.entries(
951
+ CLOUD_ONLY_TRANSPORT_MARKERS
952
+ )) {
953
+ if (entry[field] === marker) {
954
+ cleared[field] = null;
955
+ }
956
+ }
957
+
958
+ if (Object.keys(cleared).length) {
959
+ await this.updateTransportDiagnostics(duid, cleared);
960
+ }
961
+ }
962
+
899
963
  getTransportDiagnostics() {
900
964
  const diagnostics = this.getStateAsync("TransportDiagnostics");
901
965
  if (diagnostics && typeof diagnostics.val == "string") {
@@ -1518,29 +1582,30 @@ class Roborock {
1518
1582
  ignoredSet
1519
1583
  );
1520
1584
 
1521
- if (this.isCloudOnlyModeEnabled()) {
1585
+ const cloudOnly = this.isCloudOnlyModeEnabled();
1586
+ if (cloudOnly) {
1522
1587
  this.log.info(
1523
1588
  "Roborock cloud-only mode is enabled; local LAN discovery and TCP connections will be skipped."
1524
1589
  );
1590
+ }
1525
1591
 
1526
- for (const device of managedDevicesForDiagnostics) {
1592
+ for (const device of managedDevicesForDiagnostics) {
1593
+ if (cloudOnly) {
1527
1594
  await this.updateTransportDiagnostics(device.duid, {
1528
1595
  lastTransport: "cloud",
1529
- lastTransportReason: "cloud-only-mode",
1530
1596
  localIp: null,
1531
- localDiscoveryState: "disabled",
1532
- tcpConnectionState: "disabled",
1597
+ });
1598
+ } else if (!device.localKey) {
1599
+ await this.updateTransportDiagnostics(device.duid, {
1600
+ lastTransport: "cloud",
1601
+ lastTransportReason: "missing-local-key",
1533
1602
  });
1534
1603
  }
1535
- } else {
1536
- for (const device of managedDevicesForDiagnostics) {
1537
- if (!device.localKey) {
1538
- await this.updateTransportDiagnostics(device.duid, {
1539
- lastTransport: "cloud",
1540
- lastTransportReason: "missing-local-key",
1541
- });
1542
- }
1543
- }
1604
+
1605
+ // Runs in BOTH directions: switching the mode off has to retract
1606
+ // the markers it wrote, or they stay on disk for good and the
1607
+ // device keeps reporting itself as cloud-only.
1608
+ await this.syncCloudOnlyTransportMarkers(device.duid, cloudOnly);
1544
1609
  }
1545
1610
 
1546
1611
  // this.adapter.log.debug(`initUser test: ${JSON.stringify(Array.from(this.adapter.localKeys.entries()))}`);
@@ -4505,6 +4570,6 @@ class Roborock {
4505
4570
  }
4506
4571
  }
4507
4572
 
4508
- module.exports = { Roborock };
4573
+ module.exports = { Roborock, CLOUD_ONLY_TRANSPORT_MARKERS };
4509
4574
 
4510
4575
  ////////////////////////////////////////////////////////////////////////////////////////////////////