fix(strommix): bypass CMS for Energy-Charts production — upstream now needs explicit start/end
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:
@@ -46,6 +46,66 @@ type StrommixRaw = {
|
|||||||
measured_at_unix?: number | null;
|
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 = {
|
type CrossborderRaw = {
|
||||||
flow_at_gw?: number | null;
|
flow_at_gw?: number | null;
|
||||||
flow_be_gw?: number | null;
|
flow_be_gw?: number | null;
|
||||||
@@ -129,10 +189,11 @@ async function fetchPriceAtNow(
|
|||||||
export const GET: RequestHandler = async ({ fetch }) => {
|
export const GET: RequestHandler = async ({ fetch }) => {
|
||||||
const [smardRaw, energyCharts, cross, priceDe, priceFr] = await Promise.all([
|
const [smardRaw, energyCharts, cross, priceDe, priceFr] = await Promise.all([
|
||||||
fetchSmardSnapshot(fetch),
|
fetchSmardSnapshot(fetch),
|
||||||
// Energy-Charts via the CMS — used as per-field fallback when SMARD
|
// Energy-Charts /public_power direct (with explicit time range —
|
||||||
// returns null for a specific filter (notably load 410 when SMARD's
|
// upstream now returns 404 without start/end). Used as per-field
|
||||||
// pipeline lags more than the production filters).
|
// fallback when SMARD's load filter (id 410) lags 12+ hours behind
|
||||||
fetchOne<StrommixRaw>("strommix_de", fetch),
|
// the production filters and the SMARD snapshot gets discarded.
|
||||||
|
fetchEnergyChartsPublicPower(fetch),
|
||||||
fetchOne<CrossborderRaw>("crossborder_de", fetch),
|
fetchOne<CrossborderRaw>("crossborder_de", fetch),
|
||||||
fetchPriceAtNow("DE-LU", fetch),
|
fetchPriceAtNow("DE-LU", fetch),
|
||||||
fetchPriceAtNow("FR", fetch),
|
fetchPriceAtNow("FR", fetch),
|
||||||
|
|||||||
Reference in New Issue
Block a user