system.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. """System information API routes."""
  2. import platform
  3. from datetime import datetime
  4. from pathlib import Path
  5. import psutil
  6. from fastapi import APIRouter, Depends
  7. from sqlalchemy import func, select
  8. from sqlalchemy.ext.asyncio import AsyncSession
  9. from backend.app.core.config import APP_VERSION, settings
  10. from backend.app.core.database import get_db
  11. from backend.app.models.archive import PrintArchive
  12. from backend.app.models.filament import Filament
  13. from backend.app.models.printer import Printer
  14. from backend.app.models.project import Project
  15. from backend.app.models.smart_plug import SmartPlug
  16. from backend.app.services.printer_manager import printer_manager
  17. router = APIRouter(prefix="/system", tags=["system"])
  18. def get_directory_size(path: Path) -> int:
  19. """Calculate total size of a directory in bytes."""
  20. total = 0
  21. try:
  22. for entry in path.rglob("*"):
  23. if entry.is_file():
  24. total += entry.stat().st_size
  25. except (PermissionError, OSError):
  26. pass
  27. return total
  28. def format_bytes(bytes_value: int) -> str:
  29. """Format bytes to human-readable string."""
  30. for unit in ["B", "KB", "MB", "GB", "TB"]:
  31. if bytes_value < 1024:
  32. return f"{bytes_value:.1f} {unit}"
  33. bytes_value /= 1024
  34. return f"{bytes_value:.1f} PB"
  35. def format_uptime(seconds: float) -> str:
  36. """Format uptime in seconds to human-readable string."""
  37. days = int(seconds // 86400)
  38. hours = int((seconds % 86400) // 3600)
  39. minutes = int((seconds % 3600) // 60)
  40. parts = []
  41. if days > 0:
  42. parts.append(f"{days}d")
  43. if hours > 0:
  44. parts.append(f"{hours}h")
  45. if minutes > 0:
  46. parts.append(f"{minutes}m")
  47. return " ".join(parts) if parts else "< 1m"
  48. @router.get("/info")
  49. async def get_system_info(db: AsyncSession = Depends(get_db)):
  50. """Get comprehensive system information."""
  51. # Database stats
  52. archive_count = await db.scalar(select(func.count(PrintArchive.id)))
  53. printer_count = await db.scalar(select(func.count(Printer.id)))
  54. filament_count = await db.scalar(select(func.count(Filament.id)))
  55. project_count = await db.scalar(select(func.count(Project.id)))
  56. smart_plug_count = await db.scalar(select(func.count(SmartPlug.id)))
  57. # Archive stats by status
  58. completed_count = await db.scalar(select(func.count(PrintArchive.id)).where(PrintArchive.status == "completed"))
  59. failed_count = await db.scalar(select(func.count(PrintArchive.id)).where(PrintArchive.status == "failed"))
  60. printing_count = await db.scalar(select(func.count(PrintArchive.id)).where(PrintArchive.status == "printing"))
  61. # Total print time
  62. total_print_time = (
  63. await db.scalar(
  64. select(func.sum(PrintArchive.print_time_seconds)).where(PrintArchive.print_time_seconds.isnot(None))
  65. )
  66. or 0
  67. )
  68. # Total filament used
  69. total_filament = (
  70. await db.scalar(
  71. select(func.sum(PrintArchive.filament_used_grams)).where(PrintArchive.filament_used_grams.isnot(None))
  72. )
  73. or 0
  74. )
  75. # Connected printers
  76. connected_printers = []
  77. for printer_id, client in printer_manager._clients.items():
  78. state = client.state
  79. if state and state.connected:
  80. # Get printer name and model from database
  81. result = await db.execute(select(Printer.name, Printer.model).where(Printer.id == printer_id))
  82. row = result.first()
  83. name = row[0] if row else f"Printer {printer_id}"
  84. model = row[1] if row else "unknown"
  85. connected_printers.append(
  86. {
  87. "id": printer_id,
  88. "name": name,
  89. "state": state.state,
  90. "model": model,
  91. }
  92. )
  93. # Storage info
  94. archive_dir = settings.archive_dir
  95. archive_size = get_directory_size(archive_dir) if archive_dir.exists() else 0
  96. # Database file size
  97. db_path = settings.base_dir / "bambuddy.db"
  98. db_size = db_path.stat().st_size if db_path.exists() else 0
  99. # Disk usage
  100. disk = psutil.disk_usage(str(settings.base_dir))
  101. # System info
  102. memory = psutil.virtual_memory()
  103. boot_time = datetime.fromtimestamp(psutil.boot_time())
  104. uptime_seconds = (datetime.now() - boot_time).total_seconds()
  105. # Python and system info
  106. import sys
  107. return {
  108. "app": {
  109. "version": APP_VERSION,
  110. "base_dir": str(settings.base_dir),
  111. "archive_dir": str(archive_dir),
  112. },
  113. "database": {
  114. "archives": archive_count,
  115. "archives_completed": completed_count,
  116. "archives_failed": failed_count,
  117. "archives_printing": printing_count,
  118. "printers": printer_count,
  119. "filaments": filament_count,
  120. "projects": project_count,
  121. "smart_plugs": smart_plug_count,
  122. "total_print_time_seconds": total_print_time,
  123. "total_print_time_formatted": format_uptime(total_print_time),
  124. "total_filament_grams": round(total_filament, 1),
  125. "total_filament_kg": round(total_filament / 1000, 2),
  126. },
  127. "printers": {
  128. "total": printer_count,
  129. "connected": len(connected_printers),
  130. "connected_list": connected_printers,
  131. },
  132. "storage": {
  133. "archive_size_bytes": archive_size,
  134. "archive_size_formatted": format_bytes(archive_size),
  135. "database_size_bytes": db_size,
  136. "database_size_formatted": format_bytes(db_size),
  137. "disk_total_bytes": disk.total,
  138. "disk_total_formatted": format_bytes(disk.total),
  139. "disk_used_bytes": disk.used,
  140. "disk_used_formatted": format_bytes(disk.used),
  141. "disk_free_bytes": disk.free,
  142. "disk_free_formatted": format_bytes(disk.free),
  143. "disk_percent_used": disk.percent,
  144. },
  145. "system": {
  146. "platform": platform.system(),
  147. "platform_release": platform.release(),
  148. "platform_version": platform.version(),
  149. "architecture": platform.machine(),
  150. "hostname": platform.node(),
  151. "python_version": sys.version.split()[0],
  152. "uptime_seconds": uptime_seconds,
  153. "uptime_formatted": format_uptime(uptime_seconds),
  154. "boot_time": boot_time.isoformat(),
  155. },
  156. "memory": {
  157. "total_bytes": memory.total,
  158. "total_formatted": format_bytes(memory.total),
  159. "available_bytes": memory.available,
  160. "available_formatted": format_bytes(memory.available),
  161. "used_bytes": memory.used,
  162. "used_formatted": format_bytes(memory.used),
  163. "percent_used": memory.percent,
  164. },
  165. "cpu": {
  166. "count": psutil.cpu_count(),
  167. "count_logical": psutil.cpu_count(logical=True),
  168. "percent": psutil.cpu_percent(interval=0.1),
  169. },
  170. }