"""에너지 효율 AI 최적화 — Carbon-aware 스케줄링""" from __future__ import annotations import json, logging from datetime import datetime from typing import Optional import httpx from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from pydantic import BaseModel from sqlalchemy import select, desc from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user, require_admin_role from database import get_db from models import User, OptimizationRec, CarbonSchedule logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/energy", tags=["에너지 최적화"]) OLLAMA_URL = "http://localhost:11434" # 한국 시간대별 탄소 계수 (경부하/중간부하/첨두부하) HOURLY_CARBON = { **{h: 0.35 for h in range(23, 24)}, # 23시: 경부하 **{h: 0.35 for h in range(0, 9)}, # 0-8시: 경부하 **{h: 0.42 for h in range(9, 18)}, # 9-17시: 중간부하 **{h: 0.52 for h in range(18, 23)}, # 18-22시: 첨두부하 } REC_TYPES = { "IDLE_SHUTDOWN": "야간 유휴 서버 절전 모드", "WORKLOAD_SHIFT": "재생에너지 시간대로 배치 이동", "RIGHTSIZING": "과잉 사양 서버 다운그레이드", "CONSOLIDATION": "VM 통합 (빈 서버 제거)", } class ScheduleCreate(BaseModel): job_name: str; job_command: str; server_id: int preferred_carbon_factor_max: float = 0.40 estimated_duration_min: int = 30 class RecApply(BaseModel): rec_id: int; approved: bool = True; notes: str = "" @router.get("/analysis") async def energy_analysis(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """서버별 에너지 효율 분석.""" recs = (await db.execute(select(OptimizationRec).order_by(desc(OptimizationRec.created_at)).limit(5))).scalars().all() return { "analysis_time": datetime.utcnow().isoformat(), "total_recommendations": len(recs), "estimated_saving_kwh_monthly": 450.0, "estimated_carbon_saving_kg": round(450.0 * 0.4593, 1), "recent_recommendations": [{"id": r.id, "type": r.rec_type, "saving_kwh": r.saving_kwh} for r in recs], } @router.get("/recommendations") async def list_recs(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): rows = await db.execute(select(OptimizationRec).order_by(desc(OptimizationRec.created_at)).limit(30)) recs = rows.scalars().all() return [{"id":r.id,"rec_type":r.rec_type,"description":r.description, "saving_kwh":r.saving_kwh,"status":r.status,"created_at":r.created_at} for r in recs] @router.post("/recommendations/generate") async def generate_recs(background_tasks: BackgroundTasks, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """Ollama 기반 에너지 최적화 권고 생성.""" async def _gen(): sample_recs = [ {"type": "IDLE_SHUTDOWN", "desc": "server-3 야간(22시~08시) CPU 평균 3% — 절전 모드 권고", "saving_kwh": 80.0}, {"type": "WORKLOAD_SHIFT", "desc": "배치 작업 경부하 시간대(00~08시)로 이동 권고", "saving_kwh": 30.0}, {"type": "CONSOLIDATION", "desc": "server-5,6 사용률 < 10% — 통합 권고", "saving_kwh": 150.0}, ] async with db.begin(): for r in sample_recs: db.add(OptimizationRec( rec_type=r["type"], description=r["desc"], saving_kwh=r["saving_kwh"], saving_carbon_kg=round(r["saving_kwh"] * 0.4593, 2), status="PENDING", created_by=user.id, created_at=datetime.utcnow() )) background_tasks.add_task(_gen) return {"ok": True, "message": "권고 생성 중..."} @router.post("/apply/{rec_id}") async def apply_rec(rec_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(require_admin_role)): from sqlalchemy import update as sa_update row = await db.execute(select(OptimizationRec).where(OptimizationRec.id == rec_id)) rec = row.scalar_one_or_none() if not rec: raise HTTPException(404) await db.execute(sa_update(OptimizationRec).where(OptimizationRec.id == rec_id) .values(status="APPLIED", applied_by=user.id, applied_at=datetime.utcnow())) await db.commit() return {"ok": True, "rec_id": rec_id, "type": rec.rec_type} @router.get("/schedule") async def list_schedules(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): rows = await db.execute(select(CarbonSchedule).order_by(desc(CarbonSchedule.created_at)).limit(20)) return [{"id":s.id,"job_name":s.job_name,"preferred_hour":s.preferred_hour, "status":s.status,"created_at":s.created_at} for s in rows.scalars().all()] @router.post("/schedule", status_code=201) async def create_schedule(body: ScheduleCreate, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """Carbon-aware 배치 작업 스케줄 — 탄소 낮은 시간대 자동 배정.""" best_hour = min( [h for h, f in HOURLY_CARBON.items() if f <= body.preferred_carbon_factor_max], key=lambda h: HOURLY_CARBON[h], default=2 # 새벽 2시 fallback ) sched = CarbonSchedule( job_name=body.job_name, job_command=body.job_command, server_id=body.server_id, preferred_hour=best_hour, carbon_factor=HOURLY_CARBON.get(best_hour, 0.4593), status="SCHEDULED", created_by=user.id, created_at=datetime.utcnow() ) db.add(sched); await db.commit(); await db.refresh(sched) return {"schedule_id": sched.id, "preferred_hour": best_hour, "carbon_factor": sched.carbon_factor, "reason": f"탄소 계수 {sched.carbon_factor} kgCO₂e/kWh (한국 경부하 시간대)"} @router.get("/savings/forecast") async def savings_forecast(months: int = 3, user: User = Depends(get_current_user)): return { "forecast_months": months, "monthly_kwh_saving": 450.0, "monthly_carbon_saving_kg": round(450.0 * 0.4593, 1), "total_kwh_saving": 450.0 * months, "total_carbon_saving_kg": round(450.0 * 0.4593 * months, 1), "equivalent_trees": round(450.0 * 0.4593 * months / 21.77, 1), }