feat: v2.0 — maintenance module, AI analysis, station power fix
- Add full 检修维护中心 (6.4): 3-type work orders (消缺/巡检/抄表), asset management, warehouse, work plans, billing settlement - Add AI智能分析 tab with LLM-powered diagnostics (StepFun + ZhipuAI) - Add AI模型配置 settings page (provider, temperature, prompts) - Fix station power accuracy: use API station total (station_power) instead of inverter-level computation — eliminates timing gaps - Add 7 new DB models, 4 new API routers, 5 new frontend pages - Migrations: 009 (maintenance expansion) + 010 (AI analysis) - Version bump: 1.6.1 → 2.0.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -105,58 +105,107 @@ async def get_overview(db: AsyncSession = Depends(get_db), user: User = Depends(
|
||||
|
||||
@router.get("/realtime")
|
||||
async def get_realtime_data(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""实时功率数据 - 获取最近的采集数据,按站去重防止重复计数"""
|
||||
"""实时功率数据 - 优先使用API电站汇总数据(station_power),回退到按站去重计算"""
|
||||
now = datetime.now(timezone.utc)
|
||||
window_start = now - timedelta(minutes=20)
|
||||
|
||||
# Get latest power per station (dedup by device name prefix)
|
||||
# Sungrow collectors report station-level power, so multiple devices
|
||||
# sharing the same station (AP1xx = Phase 1, AP2xx = Phase 2) report
|
||||
# identical values. GROUP BY station prefix and take MAX to avoid
|
||||
# double-counting.
|
||||
from sqlalchemy import text as sa_text
|
||||
pv_ids = await _get_pv_device_ids(db)
|
||||
hp_ids = await _get_hp_device_ids(db)
|
||||
|
||||
# PV power: dedup by station prefix
|
||||
# ── PV power ──
|
||||
# Strategy: Use station_power (direct from API station summary) if available.
|
||||
# This matches iSolarCloud's own total exactly, avoiding any timing/grouping
|
||||
# discrepancies from computing it ourselves.
|
||||
# Fallback: group by device name prefix and take MAX (legacy method).
|
||||
pv_power = 0
|
||||
if pv_ids:
|
||||
pv_q = await db.execute(
|
||||
# Try station_power first (API station total, stored by collector)
|
||||
station_q = await db.execute(
|
||||
select(
|
||||
func.substring(Device.name, 1, 3).label("station"),
|
||||
EnergyData.device_id,
|
||||
func.max(EnergyData.value).label("power"),
|
||||
).select_from(EnergyData).join(
|
||||
Device, EnergyData.device_id == Device.id
|
||||
).where(
|
||||
and_(
|
||||
EnergyData.timestamp >= window_start,
|
||||
EnergyData.data_type == "power",
|
||||
EnergyData.data_type == "station_power",
|
||||
EnergyData.device_id.in_(pv_ids),
|
||||
)
|
||||
).group_by(sa_text("1"))
|
||||
).group_by(EnergyData.device_id)
|
||||
)
|
||||
pv_power = sum(row[1] or 0 for row in pv_q.all())
|
||||
else:
|
||||
pv_power = 0
|
||||
station_rows = station_q.all()
|
||||
|
||||
# Heat pump power: dedup by station prefix
|
||||
if station_rows:
|
||||
# station_power is per-station total. Multiple devices sharing
|
||||
# the same ps_id will have identical values, so we dedup by value
|
||||
# to avoid double-counting. In practice, only ONE device per ps_id
|
||||
# stores station_power (collector-level dedup), but we guard here
|
||||
# too by taking distinct values.
|
||||
seen_powers = set()
|
||||
for row in station_rows:
|
||||
val = round(row[1] or 0, 1)
|
||||
if val > 0:
|
||||
seen_powers.add(val)
|
||||
pv_power = sum(seen_powers)
|
||||
else:
|
||||
# Fallback: legacy method — group by device name prefix, MAX per group
|
||||
from sqlalchemy import text as sa_text
|
||||
pv_q = await db.execute(
|
||||
select(
|
||||
func.substring(Device.name, 1, 3).label("station"),
|
||||
func.max(EnergyData.value).label("power"),
|
||||
).select_from(EnergyData).join(
|
||||
Device, EnergyData.device_id == Device.id
|
||||
).where(
|
||||
and_(
|
||||
EnergyData.timestamp >= window_start,
|
||||
EnergyData.data_type == "power",
|
||||
EnergyData.device_id.in_(pv_ids),
|
||||
)
|
||||
).group_by(sa_text("1"))
|
||||
)
|
||||
pv_power = sum(row[1] or 0 for row in pv_q.all())
|
||||
|
||||
# ── Heat pump power (same logic) ──
|
||||
heatpump_power = 0
|
||||
if hp_ids:
|
||||
hp_q = await db.execute(
|
||||
station_q = await db.execute(
|
||||
select(
|
||||
func.substring(Device.name, 1, 3).label("station"),
|
||||
EnergyData.device_id,
|
||||
func.max(EnergyData.value).label("power"),
|
||||
).select_from(EnergyData).join(
|
||||
Device, EnergyData.device_id == Device.id
|
||||
).where(
|
||||
and_(
|
||||
EnergyData.timestamp >= window_start,
|
||||
EnergyData.data_type == "power",
|
||||
EnergyData.data_type == "station_power",
|
||||
EnergyData.device_id.in_(hp_ids),
|
||||
)
|
||||
).group_by(sa_text("1"))
|
||||
).group_by(EnergyData.device_id)
|
||||
)
|
||||
heatpump_power = sum(row[1] or 0 for row in hp_q.all())
|
||||
else:
|
||||
heatpump_power = 0
|
||||
station_rows = station_q.all()
|
||||
|
||||
if station_rows:
|
||||
seen_powers = set()
|
||||
for row in station_rows:
|
||||
val = round(row[1] or 0, 1)
|
||||
if val > 0:
|
||||
seen_powers.add(val)
|
||||
heatpump_power = sum(seen_powers)
|
||||
else:
|
||||
from sqlalchemy import text as sa_text
|
||||
hp_q = await db.execute(
|
||||
select(
|
||||
func.substring(Device.name, 1, 3).label("station"),
|
||||
func.max(EnergyData.value).label("power"),
|
||||
).select_from(EnergyData).join(
|
||||
Device, EnergyData.device_id == Device.id
|
||||
).where(
|
||||
and_(
|
||||
EnergyData.timestamp >= window_start,
|
||||
EnergyData.data_type == "power",
|
||||
EnergyData.device_id.in_(hp_ids),
|
||||
)
|
||||
).group_by(sa_text("1"))
|
||||
)
|
||||
heatpump_power = sum(row[1] or 0 for row in hp_q.all())
|
||||
|
||||
return {
|
||||
"timestamp": str(now),
|
||||
|
||||
Reference in New Issue
Block a user