diff --git a/db/routines/R__003_fn_baseline_consumption.sql b/db/routines/R__003_fn_baseline_consumption.sql index 5aeee96..b82c025 100644 --- a/db/routines/R__003_fn_baseline_consumption.sql +++ b/db/routines/R__003_fn_baseline_consumption.sql @@ -101,8 +101,11 @@ AS $$ cbs.avg_power_w + 0.5 * COALESCE(cbs.stddev_power_w, 100), 550 )::INT AS confidence_w - FROM generate_series(p_from, p_to - INTERVAL '15 minutes', - INTERVAL '15 minutes') AS gs(slot) + FROM generate_series( + date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z'), + date_bin(interval '15 minutes', p_to, timestamptz '1970-01-01T00:00:00Z') - interval '15 minutes', + interval '15 minutes' + ) AS gs(slot) LEFT JOIN ems.consumption_baseline_stats cbs ON cbs.site_id = p_site_id AND cbs.day_of_week = EXTRACT(DOW FROM gs.slot AT TIME ZONE 'Europe/Prague')::INT diff --git a/db/routines/R__075_fn_forecast_pv_slots_range.sql b/db/routines/R__075_fn_forecast_pv_slots_range.sql index ae47a1f..f7d2be4 100644 --- a/db/routines/R__075_fn_forecast_pv_slots_range.sql +++ b/db/routines/R__075_fn_forecast_pv_slots_range.sql @@ -11,11 +11,11 @@ stable as $fn$ with bounds as ( select - p_from as ts_from, + date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z') as ts_from, case - when p_to <= p_from then p_from + interval '15 minutes' - when p_to > p_from + interval '60 days' then p_from + interval '60 days' - else p_to + when p_to <= p_from then date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z') + interval '15 minutes' + when p_to > p_from + interval '60 days' then date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z') + interval '60 days' + else date_bin(interval '15 minutes', p_to, timestamptz '1970-01-01T00:00:00Z') end as ts_to ), slot_spine as ( diff --git a/db/routines/R__079_fn_forecast_pv_slots_range_corrected.sql b/db/routines/R__079_fn_forecast_pv_slots_range_corrected.sql index f145083..8f0932d 100644 --- a/db/routines/R__079_fn_forecast_pv_slots_range_corrected.sql +++ b/db/routines/R__079_fn_forecast_pv_slots_range_corrected.sql @@ -23,11 +23,11 @@ as $fn$ ), bounds as ( select - p_from as ts_from, + date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z') as ts_from, case - when p_to <= p_from then p_from + interval '15 minutes' - when p_to > p_from + interval '60 days' then p_from + interval '60 days' - else p_to + when p_to <= p_from then date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z') + interval '15 minutes' + when p_to > p_from + interval '60 days' then date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z') + interval '60 days' + else date_bin(interval '15 minutes', p_to, timestamptz '1970-01-01T00:00:00Z') end as ts_to ), slot_spine as ( diff --git a/frontend/src/pages/ForecastVsActual.tsx b/frontend/src/pages/ForecastVsActual.tsx index 14a9f49..1e2aa72 100644 --- a/frontend/src/pages/ForecastVsActual.tsx +++ b/frontend/src/pages/ForecastVsActual.tsx @@ -84,17 +84,19 @@ function DayChart({ strokeWidth={2} dot={false} connectNulls + isAnimationActive={false} /> {showForecast ? ( ) : null} {showCorrected ? ( @@ -102,11 +104,12 @@ function DayChart({ type="monotone" dataKey="corrected_kw" name="Korigovaná" - stroke="#ef9f27" + stroke="#fde68a" strokeWidth={1.5} dot={false} connectNulls strokeDasharray="2 3" + isAnimationActive={false} /> ) : null} @@ -167,9 +170,15 @@ export default function ForecastVsActual() { const byInterval = useMemo(() => { const map = new Map() - for (const r of telemetry) map.set(r.slot_start, { ...(map.get(r.slot_start) ?? {}), tel: r }) - for (const r of pvSlots) map.set(r.interval_start, { ...(map.get(r.interval_start) ?? {}), pv: r }) - for (const r of baselineSlots) map.set(r.interval_start, { ...(map.get(r.interval_start) ?? {}), base: r }) + for (const r of telemetry) { + map.set(r.slot_start, { ...(map.get(r.slot_start) ?? {}), tel: r }) + } + for (const r of pvSlots) { + map.set(r.interval_start, { ...(map.get(r.interval_start) ?? {}), pv: r }) + } + for (const r of baselineSlots) { + map.set(r.interval_start, { ...(map.get(r.interval_start) ?? {}), base: r }) + } return map }, [telemetry, pvSlots, baselineSlots]) @@ -211,8 +220,11 @@ export default function ForecastVsActual() { byDay.set(day, arr) } return [...byDay.entries()] - .sort((a, b) => a[0].localeCompare(b[0])) - .map(([day, points]) => ({ day, points })) + .sort((a, b) => b[0].localeCompare(a[0])) + .map(([day, points]) => ({ + day, + points: points.sort((p, q) => new Date(p.k).getTime() - new Date(q.k).getTime()), + })) }, [byInterval, metric]) const title = metric === 'pv' ? 'FVE (výroba)' : metric === 'load' ? 'Spotřeba (bazál)' : 'Síť (signed)' @@ -270,6 +282,15 @@ export default function ForecastVsActual() { )} + {siteReady && siteId != null ? ( +

+ Legenda: plná šedá = skutečnost z telemetrie (15m CA),{' '} + čárkovaná žlutá = předpověď (FVE: Open‑Meteo/pvlib; spotřeba: baseline + z historie), tečkovaná světlá = korigovaná FVE (delta profil z historie). + U sítě zatím nemáme samostatnou „předpověď“ řady (jen skutečnost). +

+ ) : null} + {error ? (
{error}