diff --git a/CHANGELOG.md b/CHANGELOG.md index cfb64f5ac..5339c97e2 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/backend/app/api/routes/printers.py b/backend/app/api/routes/printers.py index 6e8f86a92..811272c66 100644 --- a/backend/app/api/routes/printers.py +++ b/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, diff --git a/backend/app/services/printer_manager.py b/backend/app/services/printer_manager.py index ace0e8106..4457b5fb4 100644 --- a/backend/app/services/printer_manager.py +++ b/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) diff --git a/backend/tests/unit/services/test_printer_manager.py b/backend/tests/unit/services/test_printer_manager.py index dbff3358e..25547f08b 100644 --- a/backend/tests/unit/services/test_printer_manager.py +++ b/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."""