diff --git a/src/routes/api/strommix/+server.ts b/src/routes/api/strommix/+server.ts index 661bbaf..3063a94 100644 --- a/src/routes/api/strommix/+server.ts +++ b/src/routes/api/strommix/+server.ts @@ -46,6 +46,66 @@ 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[]; + production_types?: PublicPowerSeries[]; +}; + +function lastNonNull(arr: (number | null)[]): number | null { + for (let i = arr.length - 1; i >= 0; i--) { + const v = arr[i]; + if (typeof v === "number" && Number.isFinite(v)) return v; + } + return null; +} + +async function fetchEnergyChartsPublicPower( + fetcher: typeof fetch +): 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(nowSec - 2 * 60 * 60); + 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 }; + 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 { + data: { + wind_onshore_mw: lastNonNull(find("Wind onshore")), + wind_offshore_mw: lastNonNull(find("Wind offshore")), + solar_mw: lastNonNull(find("Solar")), + biomass_mw: lastNonNull(find("Biomass")), + hydro_mw: lastNonNull(find("Hydro Run-of-River")), + lignite_mw: lastNonNull(find("Fossil brown coal / lignite")), + hard_coal_mw: lastNonNull(find("Fossil hard coal")), + gas_mw: lastNonNull(find("Fossil gas")), + nuclear_mw: lastNonNull(find("Nuclear")), + load_mw: lastNonNull(find("Load")), + measured_at_unix: ts[ts.length - 1] ?? null, + }, + stale: false, + }; + } catch { + return { data: null, stale: true }; + } +} + type CrossborderRaw = { flow_at_gw?: number | null; flow_be_gw?: number | null; @@ -129,10 +189,11 @@ async function fetchPriceAtNow( export const GET: RequestHandler = async ({ fetch }) => { const [smardRaw, energyCharts, cross, priceDe, priceFr] = await Promise.all([ fetchSmardSnapshot(fetch), - // Energy-Charts via the CMS — used as per-field fallback when SMARD - // returns null for a specific filter (notably load 410 when SMARD's - // pipeline lags more than the production filters). - fetchOne("strommix_de", 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), fetchOne("crossborder_de", fetch), fetchPriceAtNow("DE-LU", fetch), fetchPriceAtNow("FR", fetch),