from datetime import datetime, timezone from fastapi import APIRouter, Depends from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, and_ from app.core.database import get_db from app.core.deps import get_current_user from app.models.device import Device from app.models.energy import EnergyData from app.models.user import User router = APIRouter(prefix="/kpi", tags=["关键指标"]) @router.get("/solar") async def get_solar_kpis(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """Solar performance KPIs - PR, self-consumption, equivalent hours, revenue""" now = datetime.now(timezone.utc) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) # Get PV devices and their rated power pv_q = await db.execute( select(Device.id, Device.rated_power).where( Device.device_type.in_(["pv_inverter", "sungrow_inverter"]), Device.is_active == True, ) ) pv_devices = pv_q.all() pv_ids = [d[0] for d in pv_devices] total_rated_kw = sum(d[1] or 0 for d in pv_devices) # kW if not pv_ids or total_rated_kw == 0: return { "pr": 0, "self_consumption_rate": 0, "equivalent_hours": 0, "revenue_today": 0, "total_rated_kw": 0, "daily_generation_kwh": 0, } # Get latest daily_energy per PV device for today daily_gen_q = await db.execute( select( EnergyData.device_id, func.max(EnergyData.value).label("max_energy"), ).where( and_( EnergyData.timestamp >= today_start, EnergyData.data_type == "daily_energy", EnergyData.device_id.in_(pv_ids), ) ).group_by(EnergyData.device_id) ) # Check if values are station-level (all identical) or device-level daily_values = daily_gen_q.all() if not daily_values: daily_generation_kwh = 0 else: values = [row[1] or 0 for row in daily_values] # If all values are identical, it's station-level data — use max (not sum) if len(set(values)) == 1 and len(values) > 1: daily_generation_kwh = values[0] else: daily_generation_kwh = sum(values) # Performance Ratio (PR) = actual output / (rated capacity * peak sun hours) # Approximate peak sun hours from time of day (simplified) hours_since_sunrise = max(0, min(12, (now.hour + now.minute / 60) - 6)) # approx 6am sunrise theoretical_kwh = total_rated_kw * hours_since_sunrise * 0.8 # 0.8 = typical irradiance factor pr = (daily_generation_kwh / theoretical_kwh * 100) if theoretical_kwh > 0 else 0 pr = min(100, round(pr, 1)) # Cap at 100% # Self-consumption rate (without grid export meter, assume 100% self-consumed for now) # TODO: integrate grid export meter data when available self_consumption_rate = 100.0 # Equivalent utilization hours = daily generation / rated capacity equivalent_hours = round(daily_generation_kwh / total_rated_kw, 2) if total_rated_kw > 0 else 0 # Revenue = daily generation * electricity price # TODO: get actual price from electricity_pricing table # Default industrial TOU average price in Beijing: ~0.65 CNY/kWh avg_price = 0.65 revenue_today = round(daily_generation_kwh * avg_price, 2) return { "pr": pr, "self_consumption_rate": round(self_consumption_rate, 1), "equivalent_hours": equivalent_hours, "revenue_today": revenue_today, "total_rated_kw": total_rated_kw, "daily_generation_kwh": round(daily_generation_kwh, 2), "avg_price_per_kwh": avg_price, "pv_device_count": len(pv_ids), }