diff --git a/src/routes/api/strommix/+server.ts b/src/routes/api/strommix/+server.ts index 7dd3c3c..21ef3f1 100644 --- a/src/routes/api/strommix/+server.ts +++ b/src/routes/api/strommix/+server.ts @@ -60,33 +60,26 @@ 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( +/** Direct EC fetch — fallback when strommix_history_de via CMS returns null + * (e.g. CMS not yet deployed with ${time:...} env_subst support). */ +async function fetchEnergyChartsDirectFallback( fetcher: typeof fetch, fromSec: number -): Promise<{ data: StrommixRaw | null; rawSeries: PublicPowerResp | null; stale: boolean }> { +): Promise<{ data: StrommixRaw | 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(fromSec); - const end = fmt(nowSec); const url = `https://api.energy-charts.info/public_power?country=de` + - `&start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`; + `&start=${encodeURIComponent(fmt(fromSec))}&end=${encodeURIComponent(fmt(nowSec))}`; const res = await fetcher(url, { signal: AbortSignal.timeout(8_000) }); - if (!res.ok) return { data: null, rawSeries: null, stale: false }; + if (!res.ok) return { data: 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")), @@ -103,36 +96,10 @@ async function fetchEnergyChartsPublicPower( stale: false, }; } catch { - return { data: null, rawSeries: null, stale: true }; + return { data: 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; @@ -527,11 +494,10 @@ export const GET: RequestHandler = async ({ fetch }) => { histCrossborder, ] = await Promise.all([ fetchSmardSnapshot(fetch), - // Energy-Charts /public_power direct (with explicit time range — - // 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), + // Energy-Charts data via CMS cache (strommix_history_de/current). + // The schema URL uses ${time:day_start|iso} + ${time:now|iso} so the + // CMS fetch always succeeds. Used as per-field fallback when SMARD lags. + fetchOne("strommix_history_de", fetch), fetchOne("crossborder_de", fetch), fetchPriceAtNow("DE-LU", fetch), fetchPriceAtNow("FR", fetch), @@ -568,7 +534,13 @@ export const GET: RequestHandler = async ({ fetch }) => { const smard = smardRaw && nowSec - smardRaw.measuredAtUnix < 90 * 60 ? smardRaw : null; - const ec = energyCharts.data ?? {}; + // When the CMS-backed fetch returns null (old CMS binary without ${time:...} + // support, or upstream 404), fall back to a direct Energy-Charts call. + const energyChartsResolved = + energyCharts.data !== null + ? energyCharts + : await fetchEnergyChartsDirectFallback(fetch, dayStartSec); + const ec = energyChartsResolved.data ?? {}; // Per-field fallback. SMARD value `null` = filter unavailable; we // reach for the Energy-Charts equivalent. SMARD `0` is preserved @@ -624,17 +596,11 @@ export const GET: RequestHandler = async ({ fetch }) => { const netFlowGw = Object.values(flowsByCountry).reduce((a, b) => a + b, 0); // 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). + // backing collection is unavailable — widget renders without those panels. const todayHourly = histStrommix && histStrommix.items.length > 0 ? buildTodayHourly(histStrommix.items) - : energyCharts.rawSeries - ? buildTodayHourlyFromEC(energyCharts.rawSeries, dayStartSec) - : null; + : null; const negPriceHoursYtd = histPriceDe ? countNegPriceSlots(histPriceDe.items) : null; const crossborderYtd = histCrossborder ? aggregateCrossborderRange(histCrossborder.items, yearStartSec) @@ -673,7 +639,7 @@ export const GET: RequestHandler = async ({ fetch }) => { priceDeEurMwh: priceDe.price, priceFrEurMwh: priceFr.price, priceMeasuredAtUnix: priceDe.slotUnix, - staleStrommix: smard == null && !energyCharts.data, + staleStrommix: smard == null && !energyChartsResolved.data, staleCrossborder: cross.stale, stalePrice: priceDe.stale || priceFr.stale, todayHourly,