Browse Source

fix(drying): stop false "drying complete" from killing the printer via smart-plug auto-off (#1462)

  Reporter on X2D set a 1h AMS dry; the printer powered off seconds in.
  Support log: every "Sent drying command duration=1" was followed 3-9s
  later by "AMS 0 drying complete (dry_time 60 -> 0)" -- the completion
  callback fired right after drying started, arming smart-plug auto-off.

  Root cause: the tray-bearing branch of the AMS partial-update merge
  rebuilt the unit as {**ams_unit, "tray": merged_trays}, never spreading
  existing_unit. Tray-bearing partials carry no drying fields, so dry_time
  (and info) was dropped; the falling-edge detector read the absent field
  as 0 and saw a false 60->0 edge.

  - Merge: tray branch now spreads existing_unit first, preserving
    dry_time / info / humidity / temp across tray-only partials. Matches
    the no-tray branch. Also fixes dry_status/dry_sub_status UI flapping.
  - Detector: only evaluate the falling edge when dry_time is explicitly
    present and parseable; skip otherwise without updating state.
maziggy 6 days ago
parent
commit
76582298b5
3 changed files with 39 additions and 4 deletions
  1. 0 0
      CHANGELOG.md
  2. 19 4
      backend/app/services/bambu_mqtt.py
  3. 20 0
      backend/tests/unit/services/test_bambu_mqtt.py

File diff suppressed because it is too large
+ 0 - 0
CHANGELOG.md


+ 19 - 4
backend/app/services/bambu_mqtt.py

@@ -1714,8 +1714,15 @@ class BambuMQTTClient:
                             merged_trays.append(merged_tray)
                         else:
                             merged_trays.append(new_tray)
-                    # Update ams_unit with merged trays
-                    ams_unit = {**ams_unit, "tray": merged_trays}
+                    # Update ams_unit with merged trays. Spread existing_unit
+                    # FIRST so top-level fields the partial update omits —
+                    # dry_time, info (which drives dry_status / dry_sub_status),
+                    # humidity, temp — are preserved instead of dropped. The
+                    # printer sends tray-bearing partials that carry no drying
+                    # fields; without this, dry_time reads as absent → 0 and the
+                    # falling-edge detector below fires a false "drying complete"
+                    # (#1462). Mirrors the no-tray branch's merge semantics.
+                    ams_unit = {**existing_unit, **ams_unit, "tray": merged_trays}
                 elif existing_unit:
                     # Partial update without tray data: merge new fields into existing
                     # unit to preserve tray, sn, sw_ver, and other accumulated data.
@@ -1872,10 +1879,18 @@ class BambuMQTTClient:
                     continue
                 if ams_id < 0:
                     continue
+                # Only evaluate the edge when this update carries an explicit
+                # dry_time. An absent / unparseable value is NOT zero — treating
+                # it as 0 lets a tray-only partial fake a drying-complete edge
+                # (#1462). Skip without touching the remembered value so the
+                # next update that DOES carry dry_time sees the true previous.
+                raw_dry_time = ams_unit.get("dry_time")
+                if raw_dry_time is None:
+                    continue
                 try:
-                    current = int(ams_unit.get("dry_time") or 0)
+                    current = int(raw_dry_time)
                 except (TypeError, ValueError):
-                    current = 0
+                    continue
                 previous = self._previous_dry_times.get(ams_id, 0)
                 self._previous_dry_times[ams_id] = current
                 if previous > 0 and current == 0:

+ 20 - 0
backend/tests/unit/services/test_bambu_mqtt.py

@@ -5132,3 +5132,23 @@ class TestDryingCompleteCallback:
         # And finishes.
         mqtt_client._handle_ams_data({"ams": [{"id": "0", "dry_time": 0, "tray": []}]})
         assert mqtt_client._drying_events == [0, 0]
+
+    def test_tray_only_partial_does_not_fake_completion(self, mqtt_client):
+        """#1462 — a tray-bearing partial update that omits dry_time must not
+        be read as dry_time=0. The pre-fix merge dropped dry_time on such
+        partials, so the falling-edge detector saw a 60→0 edge and fired a
+        false 'drying complete' seconds after drying started — which armed
+        smart-plug auto-off and killed the printer mid-cycle."""
+        # Drying active, 60 minutes remaining.
+        mqtt_client._handle_ams_data({"ams": [{"id": "0", "dry_time": 60, "tray": []}]})
+        assert mqtt_client._drying_events == []
+
+        # Printer sends a tray-bearing partial carrying NO dry_time field.
+        mqtt_client._handle_ams_data({"ams": [{"id": "0", "tray": []}]})
+        assert mqtt_client._drying_events == []
+        # dry_time survived the partial in the merged AMS state.
+        assert mqtt_client.state.raw_data["ams"][0]["dry_time"] == 60
+
+        # Drying genuinely finishes → the real edge still fires exactly once.
+        mqtt_client._handle_ams_data({"ams": [{"id": "0", "dry_time": 0, "tray": []}]})
+        assert mqtt_client._drying_events == [0]

Some files were not shown because too many files changed in this diff