fix: realtime + KPI power dedup by station prefix (v1.6.3)
Sync core fixes: realtime and KPI endpoints now GROUP BY station prefix to prevent double-counting. Matches iSolarCloud within 5%. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user