diff --git a/backend/app/routers/energy_flows.py b/backend/app/routers/energy_flows.py index 13f7c1d..54fdd28 100644 --- a/backend/app/routers/energy_flows.py +++ b/backend/app/routers/energy_flows.py @@ -32,6 +32,10 @@ class DailyEnergyFlows(BaseModel): batt_to_grid_kwh: float grid_to_load_kwh: float grid_to_batt_kwh: float + grid_import_cashflow_czk: float + grid_export_revenue_czk: float + grid_to_load_cost_czk: float + grid_to_batt_cost_czk: float class DailyEnergyFlowsResponse(BaseModel): @@ -92,6 +96,10 @@ def _row_to_daily(r: Any) -> DailyEnergyFlows: batt_to_grid_kwh=_num(r["batt_to_grid_kwh"]), grid_to_load_kwh=_num(r["grid_to_load_kwh"]), grid_to_batt_kwh=_num(r["grid_to_batt_kwh"]), + grid_import_cashflow_czk=_num(r["grid_import_cashflow_czk"]), + grid_export_revenue_czk=_num(r["grid_export_revenue_czk"]), + grid_to_load_cost_czk=_num(r["grid_to_load_cost_czk"]), + grid_to_batt_cost_czk=_num(r["grid_to_batt_cost_czk"]), ) @@ -148,8 +156,39 @@ async def get_energy_flows_daily( ROUND(SUM(COALESCE(ai.flow_grid_to_load_wh, 0)) / 1000, 3) AS grid_to_load_kwh, ROUND(SUM(COALESCE(ai.flow_grid_to_batt_wh, 0)) / 1000, 3) - AS grid_to_batt_kwh + AS grid_to_batt_kwh, + ROUND( + SUM( + COALESCE(ai.actual_grid_import_wh, 0) / 1000.0 + * COALESCE(ep.effective_buy_price_czk_kwh, 0) + ), + 2 + ) AS grid_import_cashflow_czk, + ROUND( + SUM( + COALESCE(ai.actual_grid_export_wh, 0) / 1000.0 + * COALESCE(ep.effective_sell_price_czk_kwh, 0) + ), + 2 + ) AS grid_export_revenue_czk, + ROUND( + SUM( + COALESCE(ai.flow_grid_to_load_wh, 0) / 1000.0 + * COALESCE(ep.effective_buy_price_czk_kwh, 0) + ), + 2 + ) AS grid_to_load_cost_czk, + ROUND( + SUM( + COALESCE(ai.flow_grid_to_batt_wh, 0) / 1000.0 + * COALESCE(ep.effective_buy_price_czk_kwh, 0) + ), + 2 + ) AS grid_to_batt_cost_czk FROM ems.audit_interval ai + LEFT JOIN ems.vw_site_effective_price ep + ON ep.site_id = ai.site_id + AND ep.interval_start = ai.interval_start WHERE ai.site_id = $1 AND (date_trunc('day', ai.interval_start AT TIME ZONE 'Europe/Prague'))::date >= $2 diff --git a/docs/04-modules/energy-flows.md b/docs/04-modules/energy-flows.md index 405bf2e..f3dd8f1 100644 --- a/docs/04-modules/energy-flows.md +++ b/docs/04-modules/energy-flows.md @@ -18,7 +18,7 @@ Základní 6 Wh veličin (import/export, PV, baterie, load) zůstává ve Fázi ## API -- `GET /api/v1/sites/{site_id}/energy-flows/daily?month=YYYY-MM` +- `GET /api/v1/sites/{site_id}/energy-flows/daily?month=YYYY-MM` — kromě toků vrací denní součty **financí sítě**: `grid_import_cashflow_czk`, `grid_export_revenue_czk` (jako `vw_economics_interval`, join na `vw_site_effective_price`) a nákladový rozpad importu `grid_to_load_cost_czk` / `grid_to_batt_cost_czk` (efektivní nákupní cena × modelovaný tok). - `GET /api/v1/sites/{site_id}/energy-flows/daily/{day}/intervals` ## SQL @@ -29,7 +29,7 @@ Základní 6 Wh veličin (import/export, PV, baterie, load) zůstává ve Fázi ## UI - Sankey (`@nivo/sankey`) – součet toků za **celý měsíc** nebo za **jeden vybraný den** (rozbalovací pole „Graf a karty“; klik na název dne v tabulce také přepne den). Síť je ve vizualizaci rozdělena na **Import ze sítě** a **Export do sítě** (jinak by vznikl cyklus síť↔baterie a knihovna hlásí „circular link“). -- Čtyři perspektivní karty (FVE / síť / baterie / spotřeba — odkud šla energie do odběru). +- Pět perspektivních karet: FVE, síť, baterie, spotřeba a **financí** (nákup/prodej v Kč, průměrná cena Kč/kWh, rozpad nákladů importu do spotřeby vs. baterie; stejný měsíční/denní rozsah jako Sankey). - Tabulka dnů s rozbalením na 15min intervaly. ## Backfill diff --git a/frontend/src/pages/EnergyFlows.tsx b/frontend/src/pages/EnergyFlows.tsx index 8e87cae..22ed0af 100644 --- a/frontend/src/pages/EnergyFlows.tsx +++ b/frontend/src/pages/EnergyFlows.tsx @@ -41,6 +41,11 @@ function kwh(v: number | null | undefined, d = 2): string { return v.toFixed(d) } +function czk(v: number | null | undefined): string { + if (v == null) return '–' + return v.toLocaleString('cs-CZ', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) +} + function aggregateFlows(days: DailyEnergyFlows[]): FlowTotals & { pv_production_kwh: number grid_import_kwh: number @@ -48,6 +53,10 @@ function aggregateFlows(days: DailyEnergyFlows[]): FlowTotals & { batt_charge_kwh: number batt_discharge_kwh: number load_kwh: number + grid_import_cashflow_czk: number + grid_export_revenue_czk: number + grid_to_load_cost_czk: number + grid_to_batt_cost_czk: number } { const z = { pv_production_kwh: 0, @@ -63,6 +72,10 @@ function aggregateFlows(days: DailyEnergyFlows[]): FlowTotals & { batt_to_grid_kwh: 0, grid_to_load_kwh: 0, grid_to_batt_kwh: 0, + grid_import_cashflow_czk: 0, + grid_export_revenue_czk: 0, + grid_to_load_cost_czk: 0, + grid_to_batt_cost_czk: 0, } for (const d of days) { z.pv_production_kwh += d.pv_production_kwh @@ -78,6 +91,10 @@ function aggregateFlows(days: DailyEnergyFlows[]): FlowTotals & { z.batt_to_grid_kwh += d.batt_to_grid_kwh z.grid_to_load_kwh += d.grid_to_load_kwh z.grid_to_batt_kwh += d.grid_to_batt_kwh + z.grid_import_cashflow_czk += d.grid_import_cashflow_czk ?? 0 + z.grid_export_revenue_czk += d.grid_export_revenue_czk ?? 0 + z.grid_to_load_cost_czk += d.grid_to_load_cost_czk ?? 0 + z.grid_to_batt_cost_czk += d.grid_to_batt_cost_czk ?? 0 } return z } @@ -167,6 +184,17 @@ export default function EnergyFlows() { ? Math.min(100, (totals.batt_discharge_kwh / totals.batt_charge_kwh) * 100) : null + const avgImportKcPerKwh = + totals && totals.grid_import_kwh > 0.001 + ? totals.grid_import_cashflow_czk / totals.grid_import_kwh + : null + const avgExportKcPerKwh = + totals && totals.grid_export_kwh > 0.001 + ? totals.grid_export_revenue_czk / totals.grid_export_kwh + : null + const gridNetCzk = + totals != null ? totals.grid_import_cashflow_czk - totals.grid_export_revenue_czk : null + return (
{siteReady && siteRow && ( @@ -239,7 +267,7 @@ export default function EnergyFlows() { )} {totals && ( -
+

Perspektiva FVE

{totals.pv_production_kwh.toFixed(1)} kWh

@@ -296,6 +324,50 @@ export default function EnergyFlows() { zaokrouhlení po 15min intervalech.

+
+

Perspektiva financí

+

+ Nákup ze sítě:{' '} + {czk(totals.grid_import_cashflow_czk)} Kč +

+

+ Prodej do sítě:{' '} + {czk(totals.grid_export_revenue_czk)} Kč +

+

+ Bilance sítě (nákup − prodej):{' '} + {czk(gridNetCzk)} Kč +

+
    +
  • + Prům. cena nákupu:{' '} + {avgImportKcPerKwh != null ? ( + {avgImportKcPerKwh.toFixed(3)} Kč/kWh + ) : ( + + )} +
  • +
  • + Prům. cena prodeje:{' '} + {avgExportKcPerKwh != null ? ( + {avgExportKcPerKwh.toFixed(3)} Kč/kWh + ) : ( + + )} +
  • +
+

+ Rozpad nákladů importu (efektivní cena × modelovaný tok) +

+
    +
  • Do spotřeby: {czk(totals.grid_to_load_cost_czk)} Kč
  • +
  • Do baterie: {czk(totals.grid_to_batt_cost_czk)} Kč
  • +
+

+ Stejná jednotková cena v každém 15min slotu; součet rozpadu se může mírně lišit od celkového nákupu kvůli + zaokrouhlení a odchylce modelu toků od měřeného importu. +

+
)} diff --git a/frontend/src/types/energy-flows.ts b/frontend/src/types/energy-flows.ts index b5b5c83..02917f0 100644 --- a/frontend/src/types/energy-flows.ts +++ b/frontend/src/types/energy-flows.ts @@ -14,6 +14,10 @@ export type DailyEnergyFlows = { batt_to_grid_kwh: number grid_to_load_kwh: number grid_to_batt_kwh: number + grid_import_cashflow_czk: number + grid_export_revenue_czk: number + grid_to_load_cost_czk: number + grid_to_batt_cost_czk: number } export type DailyEnergyFlowsResponse = {