| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- """System information API routes."""
- import platform
- from datetime import datetime
- from pathlib import Path
- import psutil
- from fastapi import APIRouter, Depends
- from sqlalchemy import func, select
- from sqlalchemy.ext.asyncio import AsyncSession
- from backend.app.core.config import APP_VERSION, settings
- from backend.app.core.database import get_db
- from backend.app.models.archive import PrintArchive
- from backend.app.models.filament import Filament
- from backend.app.models.printer import Printer
- from backend.app.models.project import Project
- from backend.app.models.smart_plug import SmartPlug
- from backend.app.services.printer_manager import printer_manager
- router = APIRouter(prefix="/system", tags=["system"])
- def get_directory_size(path: Path) -> int:
- """Calculate total size of a directory in bytes."""
- total = 0
- try:
- for entry in path.rglob("*"):
- if entry.is_file():
- total += entry.stat().st_size
- except (PermissionError, OSError):
- pass
- return total
- def format_bytes(bytes_value: int) -> str:
- """Format bytes to human-readable string."""
- for unit in ["B", "KB", "MB", "GB", "TB"]:
- if bytes_value < 1024:
- return f"{bytes_value:.1f} {unit}"
- bytes_value /= 1024
- return f"{bytes_value:.1f} PB"
- def format_uptime(seconds: float) -> str:
- """Format uptime in seconds to human-readable string."""
- days = int(seconds // 86400)
- hours = int((seconds % 86400) // 3600)
- minutes = int((seconds % 3600) // 60)
- parts = []
- if days > 0:
- parts.append(f"{days}d")
- if hours > 0:
- parts.append(f"{hours}h")
- if minutes > 0:
- parts.append(f"{minutes}m")
- return " ".join(parts) if parts else "< 1m"
- @router.get("/info")
- async def get_system_info(db: AsyncSession = Depends(get_db)):
- """Get comprehensive system information."""
- # Database stats
- archive_count = await db.scalar(select(func.count(PrintArchive.id)))
- printer_count = await db.scalar(select(func.count(Printer.id)))
- filament_count = await db.scalar(select(func.count(Filament.id)))
- project_count = await db.scalar(select(func.count(Project.id)))
- smart_plug_count = await db.scalar(select(func.count(SmartPlug.id)))
- # Archive stats by status
- completed_count = await db.scalar(select(func.count(PrintArchive.id)).where(PrintArchive.status == "completed"))
- failed_count = await db.scalar(select(func.count(PrintArchive.id)).where(PrintArchive.status == "failed"))
- printing_count = await db.scalar(select(func.count(PrintArchive.id)).where(PrintArchive.status == "printing"))
- # Total print time
- total_print_time = (
- await db.scalar(
- select(func.sum(PrintArchive.print_time_seconds)).where(PrintArchive.print_time_seconds.isnot(None))
- )
- or 0
- )
- # Total filament used
- total_filament = (
- await db.scalar(
- select(func.sum(PrintArchive.filament_used_grams)).where(PrintArchive.filament_used_grams.isnot(None))
- )
- or 0
- )
- # Connected printers
- connected_printers = []
- for printer_id, client in printer_manager._clients.items():
- state = client.state
- if state and state.connected:
- # Get printer name and model from database
- result = await db.execute(select(Printer.name, Printer.model).where(Printer.id == printer_id))
- row = result.first()
- name = row[0] if row else f"Printer {printer_id}"
- model = row[1] if row else "unknown"
- connected_printers.append(
- {
- "id": printer_id,
- "name": name,
- "state": state.state,
- "model": model,
- }
- )
- # Storage info
- archive_dir = settings.archive_dir
- archive_size = get_directory_size(archive_dir) if archive_dir.exists() else 0
- # Database file size
- db_path = settings.base_dir / "bambuddy.db"
- db_size = db_path.stat().st_size if db_path.exists() else 0
- # Disk usage
- disk = psutil.disk_usage(str(settings.base_dir))
- # System info
- memory = psutil.virtual_memory()
- boot_time = datetime.fromtimestamp(psutil.boot_time())
- uptime_seconds = (datetime.now() - boot_time).total_seconds()
- # Python and system info
- import sys
- return {
- "app": {
- "version": APP_VERSION,
- "base_dir": str(settings.base_dir),
- "archive_dir": str(archive_dir),
- },
- "database": {
- "archives": archive_count,
- "archives_completed": completed_count,
- "archives_failed": failed_count,
- "archives_printing": printing_count,
- "printers": printer_count,
- "filaments": filament_count,
- "projects": project_count,
- "smart_plugs": smart_plug_count,
- "total_print_time_seconds": total_print_time,
- "total_print_time_formatted": format_uptime(total_print_time),
- "total_filament_grams": round(total_filament, 1),
- "total_filament_kg": round(total_filament / 1000, 2),
- },
- "printers": {
- "total": printer_count,
- "connected": len(connected_printers),
- "connected_list": connected_printers,
- },
- "storage": {
- "archive_size_bytes": archive_size,
- "archive_size_formatted": format_bytes(archive_size),
- "database_size_bytes": db_size,
- "database_size_formatted": format_bytes(db_size),
- "disk_total_bytes": disk.total,
- "disk_total_formatted": format_bytes(disk.total),
- "disk_used_bytes": disk.used,
- "disk_used_formatted": format_bytes(disk.used),
- "disk_free_bytes": disk.free,
- "disk_free_formatted": format_bytes(disk.free),
- "disk_percent_used": disk.percent,
- },
- "system": {
- "platform": platform.system(),
- "platform_release": platform.release(),
- "platform_version": platform.version(),
- "architecture": platform.machine(),
- "hostname": platform.node(),
- "python_version": sys.version.split()[0],
- "uptime_seconds": uptime_seconds,
- "uptime_formatted": format_uptime(uptime_seconds),
- "boot_time": boot_time.isoformat(),
- },
- "memory": {
- "total_bytes": memory.total,
- "total_formatted": format_bytes(memory.total),
- "available_bytes": memory.available,
- "available_formatted": format_bytes(memory.available),
- "used_bytes": memory.used,
- "used_formatted": format_bytes(memory.used),
- "percent_used": memory.percent,
- },
- "cpu": {
- "count": psutil.cpu_count(),
- "count_logical": psutil.cpu_count(logical=True),
- "percent": psutil.cpu_percent(interval=0.1),
- },
- }
|