system.py 6.9 KB

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