fix(strommix): bypass CMS for Energy-Charts production — upstream now needs explicit start/end
Deploy / verify (push) Successful in 48s
Deploy / deploy (push) Successful in 49s

Energy-Charts' /public_power endpoint started rejecting requests without
`start` + `end` query params ("HTTP 404: no content available"). The
CMS-cached `strommix_de` external schema cannot pass dynamic time
windows (external-collections v1 has no runtime-vars), so the per-field
fallback chain was effectively dead — when SMARD's load filter lagged
12+ h, both sources returned null and the widget surfaced "Lastwert
nicht verfügbar" with all zeros.

Aggregator now fetches /public_power directly with a 2-hour window,
picks the latest non-null per series, and feeds those values into the
existing per-field fallback. CMS strommix_de schema becomes obsolete
for the widget but stays available for admin debugging.

Same shape as the previous CMS payload, so the fallback merge is
unchanged. Picks `Wind onshore`, `Wind offshore`, `Solar`, `Biomass`,
`Hydro Run-of-River`, `Fossil brown coal / lignite`, `Fossil hard
coal`, `Fossil gas`, `Nuclear`, `Load` from production_types.
This commit is contained in:
Peter Meier
2026-05-05 00:09:25 +02:00
parent 0d13e85639
commit 080e69dcac
+65 -4
View File
@@ -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<StrommixRaw>("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<CrossborderRaw>("crossborder_de", fetch),
fetchPriceAtNow("DE-LU", fetch),
fetchPriceAtNow("FR", fetch),