obico.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. """API routes for Obico AI failure detection."""
  2. import logging
  3. from fastapi import APIRouter, HTTPException, Response
  4. from pydantic import BaseModel
  5. from backend.app.core.auth import RequestPrinterScope, RequirePermissionIfAuthEnabled
  6. from backend.app.core.permissions import Permission
  7. from backend.app.core.printer_scope import PrinterScope
  8. from backend.app.models.user import User
  9. from backend.app.services.obico_detection import obico_detection_service, pop_frame
  10. logger = logging.getLogger(__name__)
  11. router = APIRouter(prefix="/obico", tags=["obico"])
  12. class TestConnectionRequest(BaseModel):
  13. url: str
  14. # Omitted entirely = test with the saved token; "" = test with no token.
  15. token: str | None = None
  16. @router.get("/status")
  17. async def get_status(
  18. _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ),
  19. ):
  20. """Scheduler status, per-printer classification, and recent detection history."""
  21. settings = await obico_detection_service._load_settings()
  22. status = obico_detection_service.get_status(settings["sensitivity"])
  23. return {
  24. **status,
  25. "enabled": settings["enabled"],
  26. "ml_url": settings["ml_url"],
  27. "sensitivity": settings["sensitivity"],
  28. "action": settings["action"],
  29. "poll_interval": settings["poll_interval"],
  30. "external_url_configured": bool(settings["external_url"]),
  31. }
  32. @router.get("/printer-status")
  33. async def get_printer_status(
  34. user: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTERS_READ),
  35. printer_scope: PrinterScope = RequestPrinterScope,
  36. ):
  37. """Per-printer live classification for the printer cards (#1546).
  38. Deliberately excludes configuration (ML URL, action, history) so users
  39. with printers:read but no settings:read can still render the badge.
  40. """
  41. settings = await obico_detection_service._load_settings()
  42. enabled_printers = settings["enabled_printers"]
  43. # Error strings can embed configured URLs (ML API base, external URL), so
  44. # they stay behind settings:read like the rest of the configuration.
  45. can_see_error = user is None or user.has_permission(Permission.SETTINGS_READ.value)
  46. per_printer = obico_detection_service.get_per_printer()
  47. if not can_see_error:
  48. # The "error" *class* is not configuration — a printers:read user still
  49. # needs to know their print is not being watched. Only the reason, which
  50. # can name a URL, is withheld.
  51. per_printer = {pid: {**entry, "error": None} for pid, entry in per_printer.items()}
  52. # Only printers the caller may see (#1727)
  53. per_printer = {pid: entry for pid, entry in per_printer.items() if printer_scope.allows(int(pid))}
  54. if enabled_printers is not None:
  55. enabled_printers = [pid for pid in enabled_printers if printer_scope.allows(int(pid))]
  56. return {
  57. "enabled": settings["enabled"],
  58. # None = all printers are monitored
  59. "monitored_printers": sorted(enabled_printers) if enabled_printers is not None else None,
  60. "per_printer": per_printer,
  61. "last_error": obico_detection_service._last_error if can_see_error else None,
  62. }
  63. @router.post("/test-connection")
  64. async def test_connection(
  65. req: TestConnectionRequest,
  66. _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
  67. ):
  68. """Ping the Obico ML API health endpoint and check the token. Returns ok + raw body."""
  69. if not req.url:
  70. return {"ok": False, "status_code": None, "body": None, "error": "URL is empty", "auth_ok": None}
  71. token = req.token
  72. if token is None:
  73. # Field omitted entirely — test what the service actually uses.
  74. settings = await obico_detection_service._load_settings()
  75. token = settings.get("ml_token") or ""
  76. return await obico_detection_service.test_connection(req.url, token)
  77. @router.get("/cached-frame/{nonce}")
  78. async def cached_frame(nonce: str):
  79. """Serve a pre-captured JPEG to the Obico ML API.
  80. The detection loop captures a snapshot locally (where we control the timeout),
  81. stashes the bytes under a one-shot random nonce, then hands this URL to Obico's
  82. ML API. Obico's hardcoded 5s read timeout never races our snapshot pipeline.
  83. Unauthenticated: the unguessable 32-byte nonce is single-use and expires in
  84. seconds, so exposing this path doesn't widen the camera access surface.
  85. """
  86. data = await pop_frame(nonce)
  87. if data is None:
  88. raise HTTPException(status_code=404, detail="Frame not found or expired")
  89. return Response(
  90. content=data,
  91. media_type="image/jpeg",
  92. headers={"Cache-Control": "no-store"},
  93. )