scheduled_dryings.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. """Scheduled (delayed) manual AMS drying runs (#2638)."""
  2. from fastapi import APIRouter, Depends, HTTPException
  3. from sqlalchemy import select
  4. from sqlalchemy.ext.asyncio import AsyncSession
  5. from backend.app.core.auth import RequirePermissionIfAuthEnabled
  6. from backend.app.core.database import get_db
  7. from backend.app.core.permissions import Permission
  8. from backend.app.models.printer import Printer
  9. from backend.app.models.scheduled_drying import ScheduledDrying
  10. from backend.app.models.user import User
  11. from backend.app.schemas.scheduled_drying import ScheduledDryingCreate, ScheduledDryingResponse
  12. from backend.app.services import drying_preflight
  13. from backend.app.services.printer_manager import printer_manager
  14. from backend.app.utils.local_time import utcnow_naive
  15. router = APIRouter(prefix="/scheduled-dryings", tags=["scheduled-dryings"])
  16. ACTIVE_STATUSES = ("pending", "running")
  17. # Failed rows are listed too. A run can only fail at dispatch (the firmware
  18. # turns out too old, say), which is exactly the case a schedule-time check on
  19. # an offline printer cannot catch, so without this the run just disappears and
  20. # only the backend log knows why. The client dismisses the row to clear it.
  21. LISTED_STATUSES = (*ACTIVE_STATUSES, "failed")
  22. @router.post("", response_model=ScheduledDryingResponse)
  23. async def create_scheduled_drying(
  24. payload: ScheduledDryingCreate,
  25. user: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
  26. db: AsyncSession = Depends(get_db),
  27. ):
  28. result = await db.execute(select(Printer).where(Printer.id == payload.printer_id))
  29. printer = result.scalar_one_or_none()
  30. if not printer:
  31. raise HTTPException(404, "Printer not found")
  32. # Fail fast in the UI rather than hours later with nobody watching. An
  33. # offline printer is still schedulable: only its model is judged here.
  34. state = printer_manager.get_status(payload.printer_id)
  35. unsupported = drying_preflight.check_drying_supported(
  36. printer.model,
  37. state.firmware_version if state else None,
  38. require_firmware=state is not None,
  39. )
  40. if unsupported:
  41. raise HTTPException(400, unsupported)
  42. if payload.start_after is not None and payload.start_after <= utcnow_naive():
  43. raise HTTPException(400, "start_after must be in the future")
  44. row = ScheduledDrying(
  45. printer_id=payload.printer_id,
  46. ams_id=payload.ams_id,
  47. temp=payload.temp,
  48. duration_hours=payload.duration_hours,
  49. filament=payload.filament,
  50. rotate_tray=payload.rotate_tray,
  51. start_after=payload.start_after,
  52. created_by_id=user.id if user else None,
  53. )
  54. db.add(row)
  55. await db.commit()
  56. await db.refresh(row)
  57. return row
  58. @router.get("", response_model=list[ScheduledDryingResponse])
  59. async def list_scheduled_dryings(
  60. printer_id: int | None = None,
  61. _: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTERS_READ),
  62. db: AsyncSession = Depends(get_db),
  63. ):
  64. query = select(ScheduledDrying).where(ScheduledDrying.status.in_(LISTED_STATUSES))
  65. if printer_id is not None:
  66. query = query.where(ScheduledDrying.printer_id == printer_id)
  67. result = await db.execute(query.order_by(ScheduledDrying.start_after.asc().nullsfirst(), ScheduledDrying.id.asc()))
  68. return list(result.scalars().all())
  69. @router.delete("/{scheduled_drying_id}")
  70. async def cancel_scheduled_drying(
  71. scheduled_drying_id: int,
  72. _: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
  73. db: AsyncSession = Depends(get_db),
  74. ):
  75. result = await db.execute(select(ScheduledDrying).where(ScheduledDrying.id == scheduled_drying_id))
  76. row = result.scalar_one_or_none()
  77. if not row:
  78. raise HTTPException(404, "Scheduled drying not found")
  79. if row.status == "failed":
  80. # Terminal and now acknowledged; drop it so it stops being listed.
  81. await db.delete(row)
  82. await db.commit()
  83. return {"status": "dismissed", "id": scheduled_drying_id}
  84. if row.status not in ACTIVE_STATUSES:
  85. raise HTTPException(400, "Only pending, running or failed dryings can be cancelled")
  86. if row.status == "running":
  87. # Best effort; cancellation proceeds even if the printer is offline.
  88. printer_manager.send_drying_command(row.printer_id, row.ams_id, 0, 0, mode=0)
  89. row.status = "cancelled"
  90. row.completed_at = utcnow_naive()
  91. await db.commit()
  92. return {"status": "cancelled", "id": row.id}