discovery.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. """
  2. Printer discovery API endpoints.
  3. Provides endpoints for discovering Bambu Lab printers on the local network.
  4. Supports both SSDP discovery (for native installs) and subnet scanning (for Docker).
  5. """
  6. import logging
  7. from fastapi import APIRouter
  8. from pydantic import BaseModel
  9. from backend.app.core.auth import RequirePermissionIfAuthEnabled
  10. from backend.app.core.permissions import Permission
  11. from backend.app.models.user import User
  12. from backend.app.services.discovery import (
  13. discovery_service,
  14. is_running_in_docker,
  15. subnet_scanner,
  16. )
  17. logger = logging.getLogger(__name__)
  18. router = APIRouter(prefix="/discovery", tags=["discovery"])
  19. class DiscoveryStatus(BaseModel):
  20. """Discovery status response."""
  21. running: bool
  22. class DiscoveryInfo(BaseModel):
  23. """Discovery environment info."""
  24. is_docker: bool
  25. ssdp_running: bool
  26. scan_running: bool
  27. class SubnetScanRequest(BaseModel):
  28. """Request to scan a subnet."""
  29. subnet: str # CIDR notation, e.g., "192.168.1.0/24"
  30. timeout: float = 1.0 # Connection timeout per host
  31. class SubnetScanStatus(BaseModel):
  32. """Subnet scan status response."""
  33. running: bool
  34. scanned: int
  35. total: int
  36. class DiscoveredPrinterResponse(BaseModel):
  37. """Discovered printer response."""
  38. serial: str
  39. name: str
  40. ip_address: str
  41. model: str | None = None
  42. discovered_at: str | None = None
  43. @router.get("/info", response_model=DiscoveryInfo)
  44. async def get_discovery_info(
  45. _: User | None = RequirePermissionIfAuthEnabled(Permission.DISCOVERY_SCAN),
  46. ):
  47. """Get discovery environment info (Docker detection, etc.)."""
  48. return DiscoveryInfo(
  49. is_docker=is_running_in_docker(),
  50. ssdp_running=discovery_service.is_running,
  51. scan_running=subnet_scanner.is_running,
  52. )
  53. @router.get("/status", response_model=DiscoveryStatus)
  54. async def get_discovery_status(
  55. _: User | None = RequirePermissionIfAuthEnabled(Permission.DISCOVERY_SCAN),
  56. ):
  57. """Get the current SSDP discovery status."""
  58. return DiscoveryStatus(running=discovery_service.is_running)
  59. @router.post("/start", response_model=DiscoveryStatus)
  60. async def start_discovery(
  61. duration: float = 10.0,
  62. _: User | None = RequirePermissionIfAuthEnabled(Permission.DISCOVERY_SCAN),
  63. ):
  64. """Start SSDP printer discovery.
  65. Args:
  66. duration: Discovery duration in seconds (default 10)
  67. """
  68. await discovery_service.start(duration=duration)
  69. return DiscoveryStatus(running=discovery_service.is_running)
  70. @router.post("/stop", response_model=DiscoveryStatus)
  71. async def stop_discovery(
  72. _: User | None = RequirePermissionIfAuthEnabled(Permission.DISCOVERY_SCAN),
  73. ):
  74. """Stop SSDP printer discovery."""
  75. await discovery_service.stop()
  76. return DiscoveryStatus(running=discovery_service.is_running)
  77. @router.get("/printers", response_model=list[DiscoveredPrinterResponse])
  78. async def get_discovered_printers(
  79. _: User | None = RequirePermissionIfAuthEnabled(Permission.DISCOVERY_SCAN),
  80. ):
  81. """Get list of discovered printers (from both SSDP and subnet scan)."""
  82. # Combine results from both discovery methods
  83. printers = {}
  84. # Add SSDP discovered printers
  85. for p in discovery_service.discovered_printers:
  86. printers[p.ip_address] = p
  87. # Add subnet scan discovered printers (may override if same IP)
  88. for p in subnet_scanner.discovered_printers:
  89. if p.ip_address not in printers:
  90. printers[p.ip_address] = p
  91. return [
  92. DiscoveredPrinterResponse(
  93. serial=p.serial,
  94. name=p.name,
  95. ip_address=p.ip_address,
  96. model=p.model,
  97. discovered_at=p.discovered_at,
  98. )
  99. for p in printers.values()
  100. ]
  101. # Subnet scanning endpoints (for Docker environments)
  102. @router.post("/scan", response_model=SubnetScanStatus)
  103. async def start_subnet_scan(
  104. request: SubnetScanRequest,
  105. _: User | None = RequirePermissionIfAuthEnabled(Permission.DISCOVERY_SCAN),
  106. ):
  107. """Start a subnet scan for Bambu printers.
  108. Use this when running in Docker where SSDP multicast doesn't work.
  109. Args:
  110. request: Subnet to scan in CIDR notation (e.g., "192.168.1.0/24")
  111. """
  112. # Start scan in background
  113. import asyncio
  114. asyncio.create_task(subnet_scanner.scan_subnet(request.subnet, request.timeout))
  115. # Return immediate status
  116. scanned, total = subnet_scanner.progress
  117. return SubnetScanStatus(
  118. running=subnet_scanner.is_running,
  119. scanned=scanned,
  120. total=total,
  121. )
  122. @router.get("/scan/status", response_model=SubnetScanStatus)
  123. async def get_scan_status(
  124. _: User | None = RequirePermissionIfAuthEnabled(Permission.DISCOVERY_SCAN),
  125. ):
  126. """Get the current subnet scan status."""
  127. scanned, total = subnet_scanner.progress
  128. return SubnetScanStatus(
  129. running=subnet_scanner.is_running,
  130. scanned=scanned,
  131. total=total,
  132. )
  133. @router.post("/scan/stop", response_model=SubnetScanStatus)
  134. async def stop_subnet_scan(
  135. _: User | None = RequirePermissionIfAuthEnabled(Permission.DISCOVERY_SCAN),
  136. ):
  137. """Stop the current subnet scan."""
  138. subnet_scanner.stop()
  139. scanned, total = subnet_scanner.progress
  140. return SubnetScanStatus(
  141. running=subnet_scanner.is_running,
  142. scanned=scanned,
  143. total=total,
  144. )