|
@@ -260,6 +260,9 @@ _notified_hms_errors: dict[int, set[str]] = {}
|
|
|
# Used for snapshot-diff detection at print completion
|
|
# Used for snapshot-diff detection at print completion
|
|
|
_timelapse_baselines: dict[int, set[str]] = {}
|
|
_timelapse_baselines: dict[int, set[str]] = {}
|
|
|
|
|
|
|
|
|
|
+# Track active bed cooldown monitoring tasks: {printer_id: asyncio.Task}
|
|
|
|
|
+_bed_cooldown_tasks: dict[int, asyncio.Task] = {}
|
|
|
|
|
+
|
|
|
|
|
|
|
|
async def _get_plug_energy(plug, db) -> dict | None:
|
|
async def _get_plug_energy(plug, db) -> dict | None:
|
|
|
"""Get energy from plug regardless of type (Tasmota, Home Assistant, or MQTT).
|
|
"""Get energy from plug regardless of type (Tasmota, Home Assistant, or MQTT).
|
|
@@ -991,6 +994,12 @@ async def on_print_start(printer_id: int, data: dict):
|
|
|
|
|
|
|
|
logger.info("[CALLBACK] on_print_start called for printer %s, data keys: %s", printer_id, list(data.keys()))
|
|
logger.info("[CALLBACK] on_print_start called for printer %s, data keys: %s", printer_id, list(data.keys()))
|
|
|
|
|
|
|
|
|
|
+ # Cancel any active bed cooldown task for this printer
|
|
|
|
|
+ existing_task = _bed_cooldown_tasks.pop(printer_id, None)
|
|
|
|
|
+ if existing_task and not existing_task.done():
|
|
|
|
|
+ existing_task.cancel()
|
|
|
|
|
+ logger.info("[BED-COOL] Cancelled bed cooldown monitor for printer %s (new print started)", printer_id)
|
|
|
|
|
+
|
|
|
# Clear cached cover images so the new print's thumbnail is fetched fresh
|
|
# Clear cached cover images so the new print's thumbnail is fetched fresh
|
|
|
from backend.app.api.routes.printers import clear_cover_cache
|
|
from backend.app.api.routes.printers import clear_cover_cache
|
|
|
|
|
|
|
@@ -2519,6 +2528,87 @@ async def on_print_complete(printer_id: int, data: dict):
|
|
|
pass # Best-effort timelapse session cancellation on error
|
|
pass # Best-effort timelapse session cancellation on error
|
|
|
|
|
|
|
|
asyncio.create_task(_background_layer_timelapse())
|
|
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")
|
|
log_timing("All background tasks scheduled")
|
|
|
|
|
|
|
|
# Auto-scan for timelapse if recording was active during the print
|
|
# Auto-scan for timelapse if recording was active during the print
|