Ver código fonte

Fix bed cooled notification never triggering (#497)

The bed cooldown monitor was defined at the end of on_print_complete,
after an early return that exits when no archive is found. Prints
started from BambuStudio or the printer's touchscreen have no archive,
so the function returned before the bed cooldown task was ever created.

Moved the bed cooldown block (function def + task creation) to before
the archive_id early-return so it fires for all completed prints.
Also hardened the temperature dict check from truthiness to isinstance.
maziggy 3 meses atrás
pai
commit
faff453dec
3 arquivos alterados com 89 adições e 80 exclusões
  1. 1 0
      CHANGELOG.md
  2. 82 80
      backend/app/main.py
  3. 6 0
      backend/tests/conftest.py

+ 1 - 0
CHANGELOG.md

@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
 ## [0.2.1b3] - Unreleased
 
 ### Fixed
+- **Print Bed Cooled Notification Never Triggers** ([#497](https://github.com/maziggy/bambuddy/issues/497)) — The bed cooldown monitor (which polls bed temperature after a print and sends a notification when it drops below the configured threshold) was defined at the end of the `on_print_complete` callback, after an early `return` that exits when no archive is found for the print. Prints started from BambuStudio or the printer's touchscreen typically have no archive in Bambuddy, so the function returned before the bed cooldown task was ever created. Moved the bed cooldown monitor to before the archive lookup early-return so it fires for all completed prints regardless of archive state. Also hardened the temperature dict check from truthiness (`if status.temperatures:`) to type check (`isinstance(status.temperatures, dict)`) to avoid false negatives on empty dicts.
 - **IP Addresses Not Redacted From Support Bundle Logs** — The `_sanitize_log_content()` function redacted emails, serials, and credentials but left raw IPv4 addresses in log output. Now adds known printer IPs to the sensitive string list for exact matching, and applies an IPv4 regex that replaces addresses with `[IP]` while preserving firmware version strings (which use leading-zero octets like `01.09.01.00`). Updated the system info page privacy disclaimer to list IP addresses as redacted.
 - **"Unknown stage (74)" on H2D During Print Preparation** — The H2D firmware reports `stg_cur=74` during print preparation, but this stage was not in the stage name lookup table (which went up to 66, sourced from BambuStudio). Now maps stage 74 to "Preparing". Also added stage 77 ("Preparing AMS") which was present in BambuStudio but missing from the lookup.
 - **Wrong Documentation Link for "Lubricate Carbon Rods" on P2S** ([#490](https://github.com/maziggy/bambuddy/issues/490)) — The "Lubricate Carbon Rods" maintenance task linked to the belt tension wiki page instead of the XYZ axis lubrication page for P2S printers.

+ 82 - 80
backend/app/main.py

@@ -2282,6 +2282,88 @@ async def on_print_complete(printer_id: int, data: dict):
 
     log_timing("Queue item update")
 
+    # Start bed cooldown monitor (polls bed temp until it drops below threshold)
+    # Must run before archive_id early-return so it fires for all prints (including
+    # prints started from BambuStudio/touchscreen that have no archive).
+    async def _background_bed_cooldown():
+        """Monitor bed temperature after print and notify when cooled."""
+        try:
+            from backend.app.api.routes.settings import get_setting
+
+            # Check threshold setting
+            async with async_session() as db:
+                threshold_str = await get_setting(db, "bed_cooled_threshold")
+            threshold = float(threshold_str) if threshold_str else 35.0
+
+            # Check if any provider has on_bed_cooled enabled (early exit if none)
+            async with async_session() as db:
+                providers = await notification_service._get_providers_for_event(db, "on_bed_cooled", printer_id)
+                if not providers:
+                    logger.debug("[BED-COOL] No providers enabled for bed_cooled on printer %s", printer_id)
+                    return
+
+            logger.info("[BED-COOL] Monitoring bed temp for printer %s (threshold: %.0f°C)", printer_id, threshold)
+
+            max_polls = 120  # 120 * 15s = 30 min timeout
+            for _ in range(max_polls):
+                await asyncio.sleep(15)
+
+                # Check if printer is still connected
+                status = printer_manager.get_status(printer_id)
+                if status is None:
+                    logger.info("[BED-COOL] Printer %s disconnected, stopping monitor", printer_id)
+                    return
+
+                # Check if a new print started (state == RUNNING)
+                if hasattr(status, "state") and status.state == "RUNNING":
+                    logger.info("[BED-COOL] New print started on printer %s, stopping monitor", printer_id)
+                    return
+
+                # Get bed temperature
+                bed_temp = None
+                if hasattr(status, "temperatures") and isinstance(status.temperatures, dict):
+                    bed_temp = status.temperatures.get("bed")
+
+                if bed_temp is None:
+                    continue
+
+                if bed_temp <= threshold:
+                    logger.info(
+                        "[BED-COOL] Bed cooled to %.1f°C on printer %s (threshold: %.0f°C)",
+                        bed_temp,
+                        printer_id,
+                        threshold,
+                    )
+                    printer_info = printer_manager.get_printer(printer_id)
+                    p_name = printer_info.name if printer_info else "Unknown"
+                    async with async_session() as db:
+                        await notification_service.on_bed_cooled(
+                            printer_id=printer_id,
+                            printer_name=p_name,
+                            bed_temp=bed_temp,
+                            threshold=threshold,
+                            filename=filename or subtask_name or "",
+                            db=db,
+                        )
+                    return
+
+            logger.info("[BED-COOL] Timeout waiting for bed to cool on printer %s", printer_id)
+        except asyncio.CancelledError:
+            logger.info("[BED-COOL] Bed cooldown monitor cancelled for printer %s", printer_id)
+        except Exception as e:
+            logger.warning("[BED-COOL] Failed: %s", e)
+        finally:
+            _bed_cooldown_tasks.pop(printer_id, None)
+
+    # Only start bed cooldown for completed prints
+    if data.get("status") == "completed":
+        # Cancel any existing task for this printer
+        existing_task = _bed_cooldown_tasks.pop(printer_id, None)
+        if existing_task and not existing_task.done():
+            existing_task.cancel()
+        task = asyncio.create_task(_background_bed_cooldown())
+        _bed_cooldown_tasks[printer_id] = task
+
     if not archive_id:
         logger.warning("Could not find archive for print complete: filename=%s, subtask=%s", filename, subtask_name)
         return
@@ -2791,86 +2873,6 @@ async def on_print_complete(printer_id: int, data: dict):
 
     asyncio.create_task(_background_layer_timelapse())
 
-    # Start bed cooldown monitor (polls bed temp until it drops below threshold)
-    async def _background_bed_cooldown():
-        """Monitor bed temperature after print and notify when cooled."""
-        try:
-            from backend.app.api.routes.settings import get_setting
-
-            # Check threshold setting
-            async with async_session() as db:
-                threshold_str = await get_setting(db, "bed_cooled_threshold")
-            threshold = float(threshold_str) if threshold_str else 35.0
-
-            # Check if any provider has on_bed_cooled enabled (early exit if none)
-            async with async_session() as db:
-                providers = await notification_service._get_providers_for_event(db, "on_bed_cooled", printer_id)
-                if not providers:
-                    logger.debug("[BED-COOL] No providers enabled for bed_cooled on printer %s", printer_id)
-                    return
-
-            logger.info("[BED-COOL] Monitoring bed temp for printer %s (threshold: %.0f°C)", printer_id, threshold)
-
-            max_polls = 120  # 120 * 15s = 30 min timeout
-            for _ in range(max_polls):
-                await asyncio.sleep(15)
-
-                # Check if printer is still connected
-                status = printer_manager.get_status(printer_id)
-                if status is None:
-                    logger.info("[BED-COOL] Printer %s disconnected, stopping monitor", printer_id)
-                    return
-
-                # Check if a new print started (state == RUNNING)
-                if hasattr(status, "state") and status.state == "RUNNING":
-                    logger.info("[BED-COOL] New print started on printer %s, stopping monitor", printer_id)
-                    return
-
-                # Get bed temperature
-                bed_temp = None
-                if hasattr(status, "temperatures") and status.temperatures:
-                    bed_temp = status.temperatures.get("bed")
-
-                if bed_temp is None:
-                    continue
-
-                if bed_temp <= threshold:
-                    logger.info(
-                        "[BED-COOL] Bed cooled to %.1f°C on printer %s (threshold: %.0f°C)",
-                        bed_temp,
-                        printer_id,
-                        threshold,
-                    )
-                    printer_info = printer_manager.get_printer(printer_id)
-                    p_name = printer_info.name if printer_info else "Unknown"
-                    async with async_session() as db:
-                        await notification_service.on_bed_cooled(
-                            printer_id=printer_id,
-                            printer_name=p_name,
-                            bed_temp=bed_temp,
-                            threshold=threshold,
-                            filename=filename or subtask_name or "",
-                            db=db,
-                        )
-                    return
-
-            logger.info("[BED-COOL] Timeout waiting for bed to cool on printer %s", printer_id)
-        except asyncio.CancelledError:
-            logger.info("[BED-COOL] Bed cooldown monitor cancelled for printer %s", printer_id)
-        except Exception as e:
-            logger.warning("[BED-COOL] Failed: %s", e)
-        finally:
-            _bed_cooldown_tasks.pop(printer_id, None)
-
-    # Only start bed cooldown for completed prints
-    if data.get("status") == "completed":
-        # Cancel any existing task for this printer
-        existing_task = _bed_cooldown_tasks.pop(printer_id, None)
-        if existing_task and not existing_task.done():
-            existing_task.cancel()
-        task = asyncio.create_task(_background_bed_cooldown())
-        _bed_cooldown_tasks[printer_id] = task
-
     log_timing("All background tasks scheduled")
 
     # Auto-scan for timelapse if recording was active during the print

+ 6 - 0
backend/tests/conftest.py

@@ -50,6 +50,8 @@ def event_loop():
     """Create an instance of the default event loop for each test session."""
     loop = asyncio.get_event_loop_policy().new_event_loop()
     yield loop
+    # Drain pending callbacks so aiosqlite threads can finish before loop closes
+    loop.run_until_complete(asyncio.sleep(0.05))
     loop.close()
 
 
@@ -89,6 +91,10 @@ async def test_engine():
     async with engine.begin() as conn:
         await conn.run_sync(Base.metadata.drop_all)
     await engine.dispose()
+    # Allow aiosqlite's background thread to finish processing the close
+    # response before the per-function event loop shuts down, preventing
+    # "RuntimeError: Event loop is closed" in call_soon_threadsafe.
+    await asyncio.sleep(0.01)
 
 
 @pytest.fixture