Browse Source

Fix A1/A1 Mini showing Printing instead of Idle (Issue #168)

Some A1/A1 Mini firmware versions incorrectly report stg_cur=0 (which
maps to "Printing") even when the printer is idle. This is a known
firmware bug also observed in the Home Assistant Bambu Lab integration.

- Add A1_MODELS constant listing affected model variants
- Add has_stg_cur_idle_bug() helper to identify affected models
- Update get_derived_status_name() to check gcode_state before stg_cur
  for A1 models: if IDLE + stg_cur=0, return None to show "Idle"
- Fix only applies when all conditions match (A1 model + IDLE + stg_cur=0)
- Non-A1 printers and A1 printers without the bug are unaffected

Closes #168
maziggy 3 months ago
parent
commit
c123fa08c0

+ 4 - 0
CHANGELOG.md

@@ -92,6 +92,10 @@ All notable changes to Bambuddy will be documented in this file.
 - **Multi-Plate Thumbnail in Queue** - Fixed queue items showing wrong thumbnail for multi-plate files (Issue #166):
   - Queue now displays the correct plate thumbnail based on selected plate
   - Previously always showed plate 1 thumbnail regardless of selection
+- **A1/A1 Mini Shows Printing Instead of Idle** - Fixed incorrect status display for A1 series printers (Issue #168):
+  - Some A1/A1 Mini firmware versions incorrectly report stage 0 ("Printing") when idle
+  - Now checks gcode_state to correctly display "Idle" for affected printers
+  - Fix only applies to A1 models with the specific buggy condition
 - **HMS Error Notifications** - Get notified when printer errors occur (Issue #84):
   - Automatic notifications for HMS errors (AMS issues, nozzle problems, etc.)
   - Human-readable error messages (853 error codes translated)

+ 1 - 1
backend/app/api/routes/printers.py

@@ -408,7 +408,7 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
         nozzles=nozzles,
         print_options=print_options,
         stg_cur=state.stg_cur,
-        stg_cur_name=get_derived_status_name(state),
+        stg_cur_name=get_derived_status_name(state, printer.model),
         stg=state.stg,
         airduct_mode=state.airduct_mode,
         speed_level=state.speed_level,

+ 40 - 2
backend/app/services/printer_manager.py

@@ -33,6 +33,22 @@ CHAMBER_TEMP_SUPPORTED_MODELS = frozenset(
     ]
 )
 
+# Models that may incorrectly report stg_cur=0 when idle (firmware bug)
+# Based on Home Assistant Bambu Lab integration observations
+# See: https://github.com/greghesp/ha-bambulab/blob/main/custom_components/bambu_lab/pybambu/models.py
+A1_MODELS = frozenset(
+    [
+        # Display names
+        "A1",
+        "A1 MINI",
+        "A1-MINI",
+        "A1MINI",
+        # Internal codes (from MQTT/SSDP)
+        "N1",  # A1 Mini
+        "N2S",  # A1
+    ]
+)
+
 
 def supports_chamber_temp(model: str | None) -> bool:
     """Check if a printer model has a real chamber temperature sensor.
@@ -47,6 +63,19 @@ def supports_chamber_temp(model: str | None) -> bool:
     return model_upper in CHAMBER_TEMP_SUPPORTED_MODELS
 
 
+def has_stg_cur_idle_bug(model: str | None) -> bool:
+    """Check if a printer model may incorrectly report stg_cur=0 when idle.
+
+    Some A1/A1 Mini firmware versions report stg_cur=0 (which maps to "Printing")
+    even when the printer is idle. This is a known firmware bug that was observed
+    in the Home Assistant Bambu Lab integration.
+    """
+    if not model:
+        return False
+    model_upper = model.strip().upper()
+    return model_upper in A1_MODELS
+
+
 class PrinterInfo:
     """Basic printer info for callbacks."""
 
@@ -373,13 +402,22 @@ class PrinterManager:
         return result
 
 
-def get_derived_status_name(state: PrinterState) -> str | None:
+def get_derived_status_name(state: PrinterState, model: str | None = None) -> str | None:
     """
     Compute a human-readable status name based on printer state.
 
     Uses stg_cur when available, otherwise derives status from temperature data
     when the printer is heating before a print starts.
+
+    Args:
+        state: The printer state to analyze
+        model: Optional printer model for model-specific workarounds
     """
+    # A1/A1 Mini firmware bug: some versions report stg_cur=0 when idle
+    # Only correct this specific case (IDLE + stg_cur=0) for affected models
+    if state.state == "IDLE" and state.stg_cur == 0 and has_stg_cur_idle_bug(model):
+        return None
+
     # If we have a valid calibration stage, use it
     # X1 models use -1 for idle, A1/P1 models use 255 for idle
     # Valid stage numbers are 0-254
@@ -581,7 +619,7 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None, mo
         "wifi_signal": state.wifi_signal,
         # Calibration stage tracking
         "stg_cur": state.stg_cur,
-        "stg_cur_name": get_derived_status_name(state),
+        "stg_cur_name": get_derived_status_name(state, model),
         # Printable objects count for skip objects feature
         "printable_objects_count": len(state.printable_objects),
         # Fan speeds (0-100 percentage, None if not available)

+ 68 - 1
backend/tests/unit/services/test_printer_manager.py

@@ -12,6 +12,7 @@ import pytest
 from backend.app.services.printer_manager import (
     PrinterManager,
     get_derived_status_name,
+    has_stg_cur_idle_bug,
     init_printer_connections,
     printer_state_to_dict,
     supports_chamber_temp,
@@ -901,7 +902,7 @@ class TestGetDerivedStatusName:
         assert result == "Auto bed leveling"
 
     def test_stg_cur_zero_returns_printing(self):
-        """Verify stg_cur=0 returns 'Printing'."""
+        """Verify stg_cur=0 returns 'Printing' when no model specified."""
         state = MagicMock()
         state.stg_cur = 0
 
@@ -909,6 +910,72 @@ class TestGetDerivedStatusName:
 
         assert result == "Printing"
 
+    def test_a1_idle_with_stg_cur_zero_returns_none(self):
+        """Verify A1 with IDLE state and stg_cur=0 returns None (bug workaround)."""
+        state = MagicMock()
+        state.stg_cur = 0
+        state.state = "IDLE"
+
+        # Test various A1 model names
+        for model in ["A1", "A1 Mini", "A1-Mini", "A1MINI", "N1", "N2S"]:
+            result = get_derived_status_name(state, model)
+            assert result is None, f"Expected None for model {model}"
+
+    def test_a1_running_with_stg_cur_zero_returns_printing(self):
+        """Verify A1 with RUNNING state and stg_cur=0 still returns 'Printing'."""
+        state = MagicMock()
+        state.stg_cur = 0
+        state.state = "RUNNING"
+
+        result = get_derived_status_name(state, "A1")
+
+        assert result == "Printing"
+
+    def test_non_a1_idle_with_stg_cur_zero_returns_printing(self):
+        """Verify non-A1 models with IDLE and stg_cur=0 still return 'Printing'."""
+        state = MagicMock()
+        state.stg_cur = 0
+        state.state = "IDLE"
+
+        # X1C should not get the workaround
+        result = get_derived_status_name(state, "X1C")
+
+        assert result == "Printing"
+
+
+class TestHasStgCurIdleBug:
+    """Tests for has_stg_cur_idle_bug function."""
+
+    def test_a1_models_return_true(self):
+        """Verify A1 model variants return True."""
+        assert has_stg_cur_idle_bug("A1") is True
+        assert has_stg_cur_idle_bug("A1 Mini") is True
+        assert has_stg_cur_idle_bug("A1-Mini") is True
+        assert has_stg_cur_idle_bug("A1MINI") is True
+        assert has_stg_cur_idle_bug("a1") is True  # case insensitive
+        assert has_stg_cur_idle_bug("a1 mini") is True
+
+    def test_a1_internal_codes_return_true(self):
+        """Verify A1 internal model codes return True."""
+        assert has_stg_cur_idle_bug("N1") is True  # A1 Mini
+        assert has_stg_cur_idle_bug("N2S") is True  # A1
+
+    def test_non_a1_models_return_false(self):
+        """Verify non-A1 models return False."""
+        assert has_stg_cur_idle_bug("X1C") is False
+        assert has_stg_cur_idle_bug("X1") is False
+        assert has_stg_cur_idle_bug("P1P") is False
+        assert has_stg_cur_idle_bug("P1S") is False
+        assert has_stg_cur_idle_bug("H2D") is False
+
+    def test_none_model_returns_false(self):
+        """Verify None model returns False."""
+        assert has_stg_cur_idle_bug(None) is False
+
+    def test_empty_model_returns_false(self):
+        """Verify empty model returns False."""
+        assert has_stg_cur_idle_bug("") is False
+
 
 class TestInitPrinterConnections:
     """Tests for init_printer_connections function."""