printer_sensor_history.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. """API routes for printer heater (nozzle / bed / chamber) sensor history."""
  2. from datetime import datetime, timedelta, timezone
  3. from fastapi import APIRouter, Depends, Query
  4. from pydantic import BaseModel
  5. from sqlalchemy import and_, func, select
  6. from sqlalchemy.ext.asyncio import AsyncSession
  7. from backend.app.core.auth import RequirePermissionIfAuthEnabled
  8. from backend.app.core.database import get_db
  9. from backend.app.core.permissions import Permission
  10. from backend.app.models.printer_sensor_history import PrinterSensorHistory
  11. from backend.app.models.user import User
  12. router = APIRouter(prefix="/printer-sensor-history", tags=["printer-sensor-history"])
  13. VALID_KINDS = {"nozzle", "nozzle_2", "bed", "chamber"}
  14. class HeaterHistoryPoint(BaseModel):
  15. recorded_at: datetime
  16. value: float | None
  17. target: float | None
  18. class HeaterSeries(BaseModel):
  19. sensor_kind: str
  20. data: list[HeaterHistoryPoint]
  21. min_value: float | None
  22. max_value: float | None
  23. avg_value: float | None
  24. class PrinterSensorHistoryResponse(BaseModel):
  25. printer_id: int
  26. series: list[HeaterSeries]
  27. @router.get("/{printer_id}", response_model=PrinterSensorHistoryResponse)
  28. async def get_printer_sensor_history(
  29. printer_id: int,
  30. hours: int = Query(default=24, ge=1, le=168, description="Hours of history (1-168)"),
  31. kinds: str | None = Query(
  32. default=None,
  33. description="Comma-separated list of sensor kinds (nozzle, nozzle_2, bed, chamber). All by default.",
  34. ),
  35. db: AsyncSession = Depends(get_db),
  36. _: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTER_SENSOR_HISTORY_READ),
  37. ):
  38. """Return per-sensor heater history for a printer."""
  39. since = datetime.now(timezone.utc) - timedelta(hours=hours)
  40. if kinds:
  41. requested = {k.strip() for k in kinds.split(",") if k.strip()}
  42. kinds_to_fetch = sorted(requested & VALID_KINDS)
  43. else:
  44. kinds_to_fetch = sorted(VALID_KINDS)
  45. series_out: list[HeaterSeries] = []
  46. for kind in kinds_to_fetch:
  47. result = await db.execute(
  48. select(PrinterSensorHistory)
  49. .where(
  50. and_(
  51. PrinterSensorHistory.printer_id == printer_id,
  52. PrinterSensorHistory.sensor_kind == kind,
  53. PrinterSensorHistory.recorded_at >= since,
  54. )
  55. )
  56. .order_by(PrinterSensorHistory.recorded_at)
  57. )
  58. records = result.scalars().all()
  59. stats_result = await db.execute(
  60. select(
  61. func.min(PrinterSensorHistory.value).label("min_v"),
  62. func.max(PrinterSensorHistory.value).label("max_v"),
  63. func.avg(PrinterSensorHistory.value).label("avg_v"),
  64. ).where(
  65. and_(
  66. PrinterSensorHistory.printer_id == printer_id,
  67. PrinterSensorHistory.sensor_kind == kind,
  68. PrinterSensorHistory.recorded_at >= since,
  69. )
  70. )
  71. )
  72. stats = stats_result.one()
  73. series_out.append(
  74. HeaterSeries(
  75. sensor_kind=kind,
  76. data=[
  77. HeaterHistoryPoint(
  78. recorded_at=r.recorded_at,
  79. value=r.value,
  80. target=r.target,
  81. )
  82. for r in records
  83. ],
  84. min_value=stats.min_v,
  85. max_value=stats.max_v,
  86. avg_value=round(stats.avg_v, 1) if stats.avg_v is not None else None,
  87. )
  88. )
  89. return PrinterSensorHistoryResponse(printer_id=printer_id, series=series_out)
  90. @router.delete("/{printer_id}")
  91. async def delete_old_history(
  92. printer_id: int,
  93. days: int = Query(default=30, ge=1, le=365, description="Delete data older than X days"),
  94. db: AsyncSession = Depends(get_db),
  95. _: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTER_SENSOR_HISTORY_READ),
  96. ):
  97. """Delete old printer sensor history for a printer."""
  98. cutoff = datetime.now(timezone.utc) - timedelta(days=days)
  99. result = await db.execute(
  100. select(func.count(PrinterSensorHistory.id)).where(
  101. and_(
  102. PrinterSensorHistory.printer_id == printer_id,
  103. PrinterSensorHistory.recorded_at < cutoff,
  104. )
  105. )
  106. )
  107. count = result.scalar()
  108. await db.execute(
  109. PrinterSensorHistory.__table__.delete().where(
  110. and_(
  111. PrinterSensorHistory.printer_id == printer_id,
  112. PrinterSensorHistory.recorded_at < cutoff,
  113. )
  114. )
  115. )
  116. await db.commit()
  117. return {"deleted": count, "message": f"Deleted {count} records older than {days} days"}