refactor(strommix): route EC live snapshot through CMS, keep direct fallback
Energy-Charts /public_power now fetched via strommix_history_de CMS collection
(which uses ${time:day_start|iso}/${time:now|iso} in its source URL). When the
CMS returns null (old binary without env_subst support), falls back to a direct
EC call so charts keep working during the transition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<StrommixRaw>("strommix_history_de", fetch),
|
||||
fetchOne<CrossborderRaw>("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,
|
||||
|
||||
Reference in New Issue
Block a user