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
This commit is contained in:
maziggy
2026-01-29 07:23:53 +01:00
parent c203d9bf5b
commit c123fa08c0
4 changed files with 113 additions and 4 deletions
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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)
@@ -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."""