fix: realtime + KPI power dedup by station prefix (v1.4.3)

Realtime endpoint was summing ALL device power readings, causing
double-counting when multiple devices share the same Sungrow station.
E.g. 10 devices × station-level power = 5x inflated total.

Fix: GROUP BY station prefix (first 3 chars of device name) and
take MAX per station. Same fix applied to KPI daily_generation.

Result: 5,550 kW → 1,931 kW (matches iSolarCloud's 2,049 kW
within the 15-min collection timing window).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Du Wenbo
2026-04-11 14:18:49 +08:00
parent 8e5e52e8ee
commit a05b25bcc2
2 changed files with 59 additions and 21 deletions

View File

@@ -105,21 +105,58 @@ async def get_overview(db: AsyncSession = Depends(get_db), user: User = Depends(
@router.get("/realtime") @router.get("/realtime")
async def get_realtime_data(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): async def get_realtime_data(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
"""实时功率数据 - 获取最近的采集数据""" """实时功率数据 - 获取最近的采集数据,按站去重防止重复计数"""
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
five_min_ago = now - timedelta(minutes=20) window_start = now - timedelta(minutes=20)
latest_q = await db.execute(
select(EnergyData).where(
and_(EnergyData.timestamp >= five_min_ago, EnergyData.data_type == "power")
).order_by(EnergyData.timestamp.desc()).limit(50)
)
data_points = latest_q.scalars().all()
# 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) pv_ids = await _get_pv_device_ids(db)
hp_ids = await _get_hp_device_ids(db) hp_ids = await _get_hp_device_ids(db)
pv_power = sum(d.value for d in data_points if d.device_id in pv_ids)
heatpump_power = sum(d.value for d in data_points if d.device_id in hp_ids) # PV power: dedup by station prefix
if pv_ids:
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())
else:
pv_power = 0
# Heat pump power: dedup by station prefix
if hp_ids:
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())
else:
heatpump_power = 0
return { return {
"timestamp": str(now), "timestamp": str(now),

View File

@@ -35,31 +35,32 @@ async def get_solar_kpis(db: AsyncSession = Depends(get_db), user: User = Depend
"total_rated_kw": 0, "daily_generation_kwh": 0, "total_rated_kw": 0, "daily_generation_kwh": 0,
} }
# Get latest daily_energy per PV device for today # Get latest daily_energy per station (dedup by device name prefix)
# Sungrow collectors report station-level data per device, so multiple
# devices sharing the same station report identical values.
# Group by station prefix (first 3 chars of name, e.g. "AP1" vs "AP2")
# and take MAX per station to avoid double-counting.
from sqlalchemy import text as sa_text
daily_gen_q = await db.execute( daily_gen_q = await db.execute(
select( select(
EnergyData.device_id, func.substring(Device.name, 1, 3).label("station"),
func.max(EnergyData.value).label("max_energy"), func.max(EnergyData.value).label("max_energy"),
).select_from(EnergyData).join(
Device, EnergyData.device_id == Device.id
).where( ).where(
and_( and_(
EnergyData.timestamp >= today_start, EnergyData.timestamp >= today_start,
EnergyData.data_type == "daily_energy", EnergyData.data_type == "daily_energy",
EnergyData.device_id.in_(pv_ids), EnergyData.device_id.in_(pv_ids),
) )
).group_by(EnergyData.device_id) ).group_by(sa_text("1"))
) )
# Check if values are station-level (all identical) or device-level
daily_values = daily_gen_q.all() daily_values = daily_gen_q.all()
if not daily_values: if not daily_values:
daily_generation_kwh = 0 daily_generation_kwh = 0
else: else:
values = [row[1] or 0 for row in daily_values] daily_generation_kwh = sum(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) # Performance Ratio (PR) = actual output / (rated capacity * peak sun hours)
# Approximate peak sun hours from time of day (simplified) # Approximate peak sun hours from time of day (simplified)