diff --git a/backend/app/api/routes/printers.py b/backend/app/api/routes/printers.py index 7f4ad3ad5..efc289a68 100644 --- a/backend/app/api/routes/printers.py +++ b/backend/app/api/routes/printers.py @@ -1299,3 +1299,36 @@ async def refresh_ams_slot( raise HTTPException(400, message) return {"success": True, "message": message} + + +@router.get("/{printer_id}/runtime-debug") +async def get_runtime_debug( + printer_id: int, + db: AsyncSession = Depends(get_db), +): + """Debug endpoint: Get runtime tracking status for a printer.""" + result = await db.execute(select(Printer).where(Printer.id == printer_id)) + printer = result.scalar_one_or_none() + if not printer: + raise HTTPException(404, "Printer not found") + + state = printer_manager.get_status(printer_id) + + return { + "printer_name": printer.name, + "runtime_seconds": printer.runtime_seconds, + "runtime_hours": printer.runtime_seconds / 3600.0 if printer.runtime_seconds else 0, + "print_hours_offset": printer.print_hours_offset, + "total_hours": (printer.runtime_seconds / 3600.0 if printer.runtime_seconds else 0) + + (printer.print_hours_offset or 0), + "last_runtime_update": printer.last_runtime_update.isoformat() if printer.last_runtime_update else None, + "mqtt_state": { + "connected": state.connected if state else False, + "state": state.state if state else None, + "progress": state.progress if state else None, + "gcode_file": state.gcode_file if state else None, + } + if state + else None, + "is_active": printer.is_active, + } diff --git a/backend/app/main.py b/backend/app/main.py index 6154637bf..4e6479898 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1597,7 +1597,11 @@ async def track_printer_runtime(): for printer in printers: # Get current state from printer manager state = printer_manager.get_status(printer.id) - if not state or not state.connected: + if not state: + logger.debug(f"[{printer.name}] Runtime tracking: no state available") + continue + if not state.connected: + logger.debug(f"[{printer.name}] Runtime tracking: not connected") continue # Check if printer is in an active state (RUNNING or PAUSE) @@ -1610,14 +1614,27 @@ async def track_printer_runtime(): printer.runtime_seconds += int(elapsed) updated_count += 1 needs_commit = True + logger.debug( + f"[{printer.name}] Runtime tracking: added {int(elapsed)}s, " + f"total={printer.runtime_seconds}s ({printer.runtime_seconds/3600:.2f}h)" + ) + else: + logger.warning( + f"[{printer.name}] Runtime tracking: skipped elapsed={elapsed:.1f}s " + f"(outside valid range 0-{RUNTIME_TRACKING_INTERVAL * 2}s)" + ) else: # First time seeing printer active - need to commit to save timestamp needs_commit = True + logger.debug(f"[{printer.name}] Runtime tracking: first active detection") printer.last_runtime_update = now else: # Printer is idle/offline - clear last_runtime_update if printer.last_runtime_update is not None: + logger.debug( + f"[{printer.name}] Runtime tracking: state={state.state}, clearing last_runtime_update" + ) printer.last_runtime_update = None needs_commit = True diff --git a/backend/app/services/bambu_mqtt.py b/backend/app/services/bambu_mqtt.py index d0eac3705..55045a49d 100644 --- a/backend/app/services/bambu_mqtt.py +++ b/backend/app/services/bambu_mqtt.py @@ -862,11 +862,24 @@ class BambuMQTTClient: logger.warning(f"[{self.serial_number}] Unexpected AMS data format: {type(ams_data)}") return - # Store AMS data in raw_data so it's accessible via API - self.state.raw_data["ams"] = ams_list + # Merge AMS data instead of replacing, to handle partial updates + # During prints, the printer may only send updates for active AMS units + existing_ams = self.state.raw_data.get("ams", []) + existing_by_id = {ams.get("id"): ams for ams in existing_ams if ams.get("id") is not None} + + # Update existing units with new data, add new units + for ams_unit in ams_list: + ams_id = ams_unit.get("id") + if ams_id is not None: + existing_by_id[ams_id] = ams_unit + + # Convert back to list, sorted by ID for consistent ordering + merged_ams = sorted(existing_by_id.values(), key=lambda x: x.get("id", 0)) + self.state.raw_data["ams"] = merged_ams + # Update timestamp for RFID refresh detection (frontend can detect "new data arrived") self.state.last_ams_update = time.time() - logger.debug(f"[{self.serial_number}] Stored AMS data with {len(ams_list)} units") + logger.debug(f"[{self.serial_number}] Merged AMS data: {len(ams_list)} new units, {len(merged_ams)} total") # Extract ams_extruder_map from each AMS unit's info field # According to OpenBambuAPI: info field bit 8 indicates which extruder (0=right, 1=left) diff --git a/icons/5f21bc794a4e4521b72c6564029ed5d9.svg b/icons/5f21bc794a4e4521b72c6564029ed5d9.svg new file mode 100644 index 000000000..9d0321d94 --- /dev/null +++ b/icons/5f21bc794a4e4521b72c6564029ed5d9.svg @@ -0,0 +1 @@ + diff --git a/icons/fc3b0d1fbe8b4815b0f7dbd92f3b68fd.svg b/icons/fc3b0d1fbe8b4815b0f7dbd92f3b68fd.svg new file mode 100644 index 000000000..9d0321d94 --- /dev/null +++ b/icons/fc3b0d1fbe8b4815b0f7dbd92f3b68fd.svg @@ -0,0 +1 @@ +