mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
Backend changes:
1. bambu_mqtt.py: Added hms_errors to the completion callback data, which includes the HMS error codes when a print fails
2. archive.py: Updated update_archive_status() to accept an optional failure_reason parameter
3. main.py: Added auto-detection of failure reasons:
- status == "aborted" → failure_reason = "User cancelled"
- status == "failed" with HMS errors → maps module codes to reasons:
- Module 0x07 (Filament) → "Filament runout"
- Module 0x0C (Motion Controller) → "Layer shift"
- Module 0x05 (Nozzle) → "Clogged nozzle"
Frontend changes:
1. ArchivesPage.tsx:
- Badge now shows "cancelled" for aborted prints, "failed" for failed prints
- "Failed Prints" collection and "Hide Failed" filter now include both failed and aborted statuses
2. EditArchiveModal.tsx: Failure reason field now shows for both failed and aborted prints
New tests added:
- test_hms_errors_included_in_failed_completion_callback
- test_aborted_status_when_cancelled
The HMS error module mapping is basic and can be expanded as you observe more failure types. The actual Bambu HMS error codes are documented in their wiki - we can add
more mappings as needed.
This commit is contained in:
@@ -13,10 +13,10 @@ Full end-to-end tests require the actual database setup.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
|
||||
@@ -26,11 +26,12 @@ class TestPrintStartLogic:
|
||||
@pytest.mark.asyncio
|
||||
async def test_print_start_calls_notification_service(self, capture_logs):
|
||||
"""Verify on_print_start triggers notification service."""
|
||||
with patch('backend.app.main.async_session') as mock_session_maker, \
|
||||
patch('backend.app.main.notification_service') as mock_notif, \
|
||||
patch('backend.app.main.smart_plug_manager') as mock_plug, \
|
||||
patch('backend.app.main.ws_manager') as mock_ws:
|
||||
|
||||
with (
|
||||
patch("backend.app.main.async_session") as mock_session_maker,
|
||||
patch("backend.app.main.notification_service") as mock_notif,
|
||||
patch("backend.app.main.smart_plug_manager") as mock_plug,
|
||||
patch("backend.app.main.ws_manager") as mock_ws,
|
||||
):
|
||||
mock_notif.on_print_start = AsyncMock()
|
||||
mock_plug.on_print_start = AsyncMock()
|
||||
mock_ws.send_print_start = AsyncMock()
|
||||
@@ -44,17 +45,19 @@ class TestPrintStartLogic:
|
||||
|
||||
from backend.app.main import on_print_start
|
||||
|
||||
await on_print_start(1, {
|
||||
"filename": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
})
|
||||
await on_print_start(
|
||||
1,
|
||||
{
|
||||
"filename": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
},
|
||||
)
|
||||
|
||||
# Verify WebSocket notification was sent
|
||||
mock_ws.send_print_start.assert_called_once()
|
||||
|
||||
# Verify no import shadowing errors
|
||||
errors = [r for r in capture_logs.get_errors()
|
||||
if "cannot access local variable" in str(r.message)]
|
||||
errors = [r for r in capture_logs.get_errors() if "cannot access local variable" in str(r.message)]
|
||||
assert not errors, f"Import shadowing error: {capture_logs.format_errors()}"
|
||||
|
||||
|
||||
@@ -64,11 +67,12 @@ class TestPrintCompleteLogic:
|
||||
@pytest.mark.asyncio
|
||||
async def test_print_complete_no_import_errors(self, capture_logs):
|
||||
"""Verify on_print_complete doesn't have import shadowing issues."""
|
||||
with patch('backend.app.main.async_session') as mock_session_maker, \
|
||||
patch('backend.app.main.notification_service') as mock_notif, \
|
||||
patch('backend.app.main.smart_plug_manager') as mock_plug, \
|
||||
patch('backend.app.main.ws_manager') as mock_ws:
|
||||
|
||||
with (
|
||||
patch("backend.app.main.async_session") as mock_session_maker,
|
||||
patch("backend.app.main.notification_service") as mock_notif,
|
||||
patch("backend.app.main.smart_plug_manager") as mock_plug,
|
||||
patch("backend.app.main.ws_manager") as mock_ws,
|
||||
):
|
||||
mock_notif.on_print_complete = AsyncMock()
|
||||
mock_plug.on_print_complete = AsyncMock()
|
||||
mock_ws.send_print_complete = AsyncMock()
|
||||
@@ -82,16 +86,18 @@ class TestPrintCompleteLogic:
|
||||
|
||||
from backend.app.main import on_print_complete
|
||||
|
||||
await on_print_complete(1, {
|
||||
"status": "completed",
|
||||
"filename": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
"timelapse_was_active": False,
|
||||
})
|
||||
await on_print_complete(
|
||||
1,
|
||||
{
|
||||
"status": "completed",
|
||||
"filename": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
"timelapse_was_active": False,
|
||||
},
|
||||
)
|
||||
|
||||
# Verify no import shadowing errors - this would have caught the ArchiveService bug
|
||||
errors = [r for r in capture_logs.get_errors()
|
||||
if "cannot access local variable" in str(r.message)]
|
||||
errors = [r for r in capture_logs.get_errors() if "cannot access local variable" in str(r.message)]
|
||||
assert not errors, f"Import shadowing error: {capture_logs.format_errors()}"
|
||||
|
||||
|
||||
@@ -115,18 +121,21 @@ class TestTimelapseTracking:
|
||||
client._timelapse_during_print = False
|
||||
|
||||
# Message with both state and timelapse
|
||||
client._process_message({
|
||||
"print": {
|
||||
"gcode_state": "RUNNING",
|
||||
"gcode_file": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
"xcam": {"timelapse": "enable"},
|
||||
client._process_message(
|
||||
{
|
||||
"print": {
|
||||
"gcode_state": "RUNNING",
|
||||
"gcode_file": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
"xcam": {"timelapse": "enable"},
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
assert client._was_running is True
|
||||
assert client._timelapse_during_print is True, \
|
||||
"Timelapse should be detected even when xcam is parsed before state"
|
||||
assert (
|
||||
client._timelapse_during_print is True
|
||||
), "Timelapse should be detected even when xcam is parsed before state"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timelapse_flag_included_in_completion_callback(self):
|
||||
@@ -148,27 +157,131 @@ class TestTimelapseTracking:
|
||||
client.on_print_complete = on_complete
|
||||
|
||||
# Start with timelapse
|
||||
client._process_message({
|
||||
"print": {
|
||||
"gcode_state": "RUNNING",
|
||||
"gcode_file": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
"xcam": {"timelapse": "enable"},
|
||||
client._process_message(
|
||||
{
|
||||
"print": {
|
||||
"gcode_state": "RUNNING",
|
||||
"gcode_file": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
"xcam": {"timelapse": "enable"},
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
# Complete print
|
||||
client._process_message({
|
||||
"print": {
|
||||
"gcode_state": "FINISH",
|
||||
"gcode_file": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
client._process_message(
|
||||
{
|
||||
"print": {
|
||||
"gcode_state": "FINISH",
|
||||
"gcode_file": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
assert "timelapse_was_active" in completion_data
|
||||
assert completion_data["timelapse_was_active"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hms_errors_included_in_failed_completion_callback(self):
|
||||
"""Verify completion callback receives hms_errors for failed prints."""
|
||||
from backend.app.services.bambu_mqtt import BambuMQTTClient
|
||||
|
||||
client = BambuMQTTClient(
|
||||
ip_address="192.168.1.100",
|
||||
serial_number="TEST123",
|
||||
access_code="12345678",
|
||||
)
|
||||
|
||||
completion_data = {}
|
||||
|
||||
def on_complete(data):
|
||||
completion_data.update(data)
|
||||
|
||||
client.on_print_start = lambda data: None
|
||||
client.on_print_complete = on_complete
|
||||
|
||||
# Start print
|
||||
client._process_message(
|
||||
{
|
||||
"print": {
|
||||
"gcode_state": "RUNNING",
|
||||
"gcode_file": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Add HMS error during print
|
||||
client._process_message(
|
||||
{
|
||||
"print": {
|
||||
"gcode_state": "RUNNING",
|
||||
"hms": [{"attr": 0x07000002, "code": 0x1234}], # Filament module error
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Fail print
|
||||
client._process_message(
|
||||
{
|
||||
"print": {
|
||||
"gcode_state": "FAILED",
|
||||
"gcode_file": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert "hms_errors" in completion_data
|
||||
assert len(completion_data["hms_errors"]) == 1
|
||||
assert completion_data["hms_errors"][0]["module"] == 0x07
|
||||
assert completion_data["status"] == "failed"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aborted_status_when_cancelled(self):
|
||||
"""Verify completion callback receives 'aborted' status when print is cancelled."""
|
||||
from backend.app.services.bambu_mqtt import BambuMQTTClient
|
||||
|
||||
client = BambuMQTTClient(
|
||||
ip_address="192.168.1.100",
|
||||
serial_number="TEST123",
|
||||
access_code="12345678",
|
||||
)
|
||||
|
||||
completion_data = {}
|
||||
|
||||
def on_complete(data):
|
||||
completion_data.update(data)
|
||||
|
||||
client.on_print_start = lambda data: None
|
||||
client.on_print_complete = on_complete
|
||||
|
||||
# Start print
|
||||
client._process_message(
|
||||
{
|
||||
"print": {
|
||||
"gcode_state": "RUNNING",
|
||||
"gcode_file": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# User cancels (goes to IDLE)
|
||||
client._process_message(
|
||||
{
|
||||
"print": {
|
||||
"gcode_state": "IDLE",
|
||||
"gcode_file": "/data/Metadata/test.gcode",
|
||||
"subtask_name": "Test",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert completion_data["status"] == "aborted"
|
||||
assert "hms_errors" in completion_data
|
||||
|
||||
|
||||
class TestCallbackErrorHandling:
|
||||
"""Test that callback errors are properly logged."""
|
||||
@@ -216,6 +329,7 @@ class TestNoImportShadowing:
|
||||
|
||||
# Check logs for any import-related errors
|
||||
errors = capture_logs.get_errors()
|
||||
import_errors = [e for e in errors if "import" in str(e.message).lower()
|
||||
or "local variable" in str(e.message).lower()]
|
||||
import_errors = [
|
||||
e for e in errors if "import" in str(e.message).lower() or "local variable" in str(e.message).lower()
|
||||
]
|
||||
assert not import_errors, f"Import errors found: {import_errors}"
|
||||
|
||||
Reference in New Issue
Block a user