diff --git a/src/routes/api/strommix/+server.ts b/src/routes/api/strommix/+server.ts index a2fc321..7dd3c3c 100644 --- a/src/routes/api/strommix/+server.ts +++ b/src/routes/api/strommix/+server.ts @@ -46,13 +46,6 @@ type StrommixRaw = { measured_at_unix?: number | null; }; -/** - * Fetch Energy-Charts /public_power directly with an explicit time - * window. The CMS-cached `strommix_de` schema cannot do this because - * external-collections v1 has no runtime-vars; without start/end the - * upstream returns 404 ("no content available"). 2-hour window covers - * latest 8 quarter-hour slots; we pick the latest non-null per series. - */ type PublicPowerSeries = { name: string; data: (number | null)[] }; type PublicPowerResp = { unix_seconds?: number[]; @@ -67,25 +60,33 @@ function lastNonNull(arr: (number | null)[]): number | null { return null; } +/** + * Fetch Energy-Charts /public_power with an explicit time window. + * `fromSec` is the window start (pass `dayStartSec` to cover all of today). + * Returns both the current-snapshot values (for live numbers) and the raw + * series (for the today-hourly sparkline when the history backend is empty). + */ async function fetchEnergyChartsPublicPower( - fetcher: typeof fetch -): Promise<{ data: StrommixRaw | null; stale: boolean }> { + fetcher: typeof fetch, + fromSec: number +): Promise<{ data: StrommixRaw | null; rawSeries: PublicPowerResp | null; stale: boolean }> { try { const nowSec = Math.floor(Date.now() / 1000); const fmt = (sec: number) => new Date(sec * 1000).toISOString().slice(0, 16) + "Z"; - const start = fmt(nowSec - 2 * 60 * 60); + const start = fmt(fromSec); const end = fmt(nowSec); const url = `https://api.energy-charts.info/public_power?country=de` + `&start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`; const res = await fetcher(url, { signal: AbortSignal.timeout(8_000) }); - if (!res.ok) return { data: null, stale: false }; + if (!res.ok) return { data: null, rawSeries: null, stale: false }; const d = (await res.json()) as PublicPowerResp; const find = (n: string) => d.production_types?.find((p) => p.name === n)?.data ?? []; const ts = d.unix_seconds ?? []; return { + rawSeries: d, data: { wind_onshore_mw: lastNonNull(find("Wind onshore")), wind_offshore_mw: lastNonNull(find("Wind offshore")), @@ -102,10 +103,36 @@ async function fetchEnergyChartsPublicPower( stale: false, }; } catch { - return { data: null, stale: true }; + return { data: null, rawSeries: null, stale: true }; } } +/** Build today's sparkline series directly from an Energy-Charts response. + * Used as fallback when the CMS history backend has no data yet. */ +function buildTodayHourlyFromEC( + d: PublicPowerResp, + dayStartSec: number +): ResidualLoadPoint[] { + const ts = d.unix_seconds ?? []; + const find = (n: string) => + d.production_types?.find((p) => p.name === n)?.data ?? []; + const load = find("Load"); + const wOn = find("Wind onshore"); + const wOff = find("Wind offshore"); + const sol = find("Solar"); + const result: ResidualLoadPoint[] = []; + for (let i = 0; i < ts.length; i++) { + if (ts[i] < dayStartSec) continue; + if (load[i] == null) continue; + result.push({ + unix: ts[i], + loadMw: num(load[i]), + weatherRenewableMw: num(wOn[i]) + num(wOff[i]) + num(sol[i]), + }); + } + return result.sort((a, b) => a.unix - b.unix); +} + type CrossborderRaw = { flow_at_gw?: number | null; flow_be_gw?: number | null; @@ -501,10 +528,10 @@ export const GET: RequestHandler = async ({ fetch }) => { ] = await Promise.all([ fetchSmardSnapshot(fetch), // Energy-Charts /public_power direct (with explicit time range — - // upstream now returns 404 without start/end). Used as per-field - // fallback when SMARD's load filter (id 410) lags 12+ hours behind - // the production filters and the SMARD snapshot gets discarded. - fetchEnergyChartsPublicPower(fetch), + // upstream returns 404 without start/end). Window starts at midnight so + // the full-day series doubles as the sparkline fallback when the CMS + // history backend is empty. Used as per-field fallback when SMARD lags. + fetchEnergyChartsPublicPower(fetch, dayStartSec), fetchOne("crossborder_de", fetch), fetchPriceAtNow("DE-LU", fetch), fetchPriceAtNow("FR", fetch), @@ -599,10 +626,15 @@ export const GET: RequestHandler = async ({ fetch }) => { // Historical aggregates derived from CMS `_history`. `null` when the // backing collection is unavailable (e.g. CMS not yet upgraded with // history-mode schemas) — widget renders without the affected panels. + // Build today's sparkline series. Prefer the CMS history backend (full + // deduped timeline); fall back to the Energy-Charts direct response when + // history is empty (e.g. backfill not yet run on a fresh install). const todayHourly = histStrommix && histStrommix.items.length > 0 ? buildTodayHourly(histStrommix.items) - : null; + : energyCharts.rawSeries + ? buildTodayHourlyFromEC(energyCharts.rawSeries, dayStartSec) + : null; const negPriceHoursYtd = histPriceDe ? countNegPriceSlots(histPriceDe.items) : null; const crossborderYtd = histCrossborder ? aggregateCrossborderRange(histCrossborder.items, yearStartSec)