do flow pridana ekonomika
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<main className="mx-auto max-w-7xl space-y-6 px-4 py-6 md:px-8">
|
||||
{siteReady && siteRow && (
|
||||
@@ -239,7 +267,7 @@ export default function EnergyFlows() {
|
||||
)}
|
||||
|
||||
{totals && (
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-5">
|
||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-amber-400">Perspektiva FVE</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-white">{totals.pv_production_kwh.toFixed(1)} kWh</p>
|
||||
@@ -296,6 +324,50 @@ export default function EnergyFlows() {
|
||||
zaokrouhlení po 15min intervalech.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-rose-400">Perspektiva financí</p>
|
||||
<p className="mt-2 text-sm text-slate-400">
|
||||
Nákup ze sítě:{' '}
|
||||
<span className="font-semibold text-red-200">{czk(totals.grid_import_cashflow_czk)}</span> Kč
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-slate-400">
|
||||
Prodej do sítě:{' '}
|
||||
<span className="font-semibold text-green-200">{czk(totals.grid_export_revenue_czk)}</span> Kč
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-slate-300">
|
||||
Bilance sítě (nákup − prodej):{' '}
|
||||
<span className="font-semibold text-white">{czk(gridNetCzk)}</span> Kč
|
||||
</p>
|
||||
<ul className="mt-2 space-y-1 border-t border-slate-800 pt-2 text-xs text-slate-400">
|
||||
<li>
|
||||
Prům. cena nákupu:{' '}
|
||||
{avgImportKcPerKwh != null ? (
|
||||
<span className="text-rose-200/90">{avgImportKcPerKwh.toFixed(3)} Kč/kWh</span>
|
||||
) : (
|
||||
<span>–</span>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
Prům. cena prodeje:{' '}
|
||||
{avgExportKcPerKwh != null ? (
|
||||
<span className="text-emerald-200/90">{avgExportKcPerKwh.toFixed(3)} Kč/kWh</span>
|
||||
) : (
|
||||
<span>–</span>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-2 text-[11px] font-medium uppercase tracking-wide text-slate-500">
|
||||
Rozpad nákladů importu (efektivní cena × modelovaný tok)
|
||||
</p>
|
||||
<ul className="mt-1 space-y-1 text-xs text-slate-400">
|
||||
<li>Do spotřeby: {czk(totals.grid_to_load_cost_czk)} Kč</li>
|
||||
<li>Do baterie: {czk(totals.grid_to_batt_cost_czk)} Kč</li>
|
||||
</ul>
|
||||
<p className="mt-2 border-t border-slate-800 pt-2 text-[11px] leading-snug text-slate-500">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user