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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Direct EC fetch — fallback when strommix_history_de via CMS returns null
|
||||||
* Fetch Energy-Charts /public_power with an explicit time window.
|
* (e.g. CMS not yet deployed with ${time:...} env_subst support). */
|
||||||
* `fromSec` is the window start (pass `dayStartSec` to cover all of today).
|
async function fetchEnergyChartsDirectFallback(
|
||||||
* 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(
|
|
||||||
fetcher: typeof fetch,
|
fetcher: typeof fetch,
|
||||||
fromSec: number
|
fromSec: number
|
||||||
): Promise<{ data: StrommixRaw | null; rawSeries: PublicPowerResp | null; stale: boolean }> {
|
): Promise<{ data: StrommixRaw | null; stale: boolean }> {
|
||||||
try {
|
try {
|
||||||
const nowSec = Math.floor(Date.now() / 1000);
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
const fmt = (sec: number) =>
|
const fmt = (sec: number) =>
|
||||||
new Date(sec * 1000).toISOString().slice(0, 16) + "Z";
|
new Date(sec * 1000).toISOString().slice(0, 16) + "Z";
|
||||||
const start = fmt(fromSec);
|
|
||||||
const end = fmt(nowSec);
|
|
||||||
const url =
|
const url =
|
||||||
`https://api.energy-charts.info/public_power?country=de` +
|
`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) });
|
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 d = (await res.json()) as PublicPowerResp;
|
||||||
const find = (n: string) =>
|
const find = (n: string) =>
|
||||||
d.production_types?.find((p) => p.name === n)?.data ?? [];
|
d.production_types?.find((p) => p.name === n)?.data ?? [];
|
||||||
const ts = d.unix_seconds ?? [];
|
const ts = d.unix_seconds ?? [];
|
||||||
return {
|
return {
|
||||||
rawSeries: d,
|
|
||||||
data: {
|
data: {
|
||||||
wind_onshore_mw: lastNonNull(find("Wind onshore")),
|
wind_onshore_mw: lastNonNull(find("Wind onshore")),
|
||||||
wind_offshore_mw: lastNonNull(find("Wind offshore")),
|
wind_offshore_mw: lastNonNull(find("Wind offshore")),
|
||||||
@@ -103,36 +96,10 @@ async function fetchEnergyChartsPublicPower(
|
|||||||
stale: false,
|
stale: false,
|
||||||
};
|
};
|
||||||
} catch {
|
} 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 = {
|
type CrossborderRaw = {
|
||||||
flow_at_gw?: number | null;
|
flow_at_gw?: number | null;
|
||||||
flow_be_gw?: number | null;
|
flow_be_gw?: number | null;
|
||||||
@@ -527,11 +494,10 @@ export const GET: RequestHandler = async ({ fetch }) => {
|
|||||||
histCrossborder,
|
histCrossborder,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
fetchSmardSnapshot(fetch),
|
fetchSmardSnapshot(fetch),
|
||||||
// Energy-Charts /public_power direct (with explicit time range —
|
// Energy-Charts data via CMS cache (strommix_history_de/current).
|
||||||
// upstream returns 404 without start/end). Window starts at midnight so
|
// The schema URL uses ${time:day_start|iso} + ${time:now|iso} so the
|
||||||
// the full-day series doubles as the sparkline fallback when the CMS
|
// CMS fetch always succeeds. Used as per-field fallback when SMARD lags.
|
||||||
// history backend is empty. Used as per-field fallback when SMARD lags.
|
fetchOne<StrommixRaw>("strommix_history_de", fetch),
|
||||||
fetchEnergyChartsPublicPower(fetch, dayStartSec),
|
|
||||||
fetchOne<CrossborderRaw>("crossborder_de", fetch),
|
fetchOne<CrossborderRaw>("crossborder_de", fetch),
|
||||||
fetchPriceAtNow("DE-LU", fetch),
|
fetchPriceAtNow("DE-LU", fetch),
|
||||||
fetchPriceAtNow("FR", fetch),
|
fetchPriceAtNow("FR", fetch),
|
||||||
@@ -568,7 +534,13 @@ export const GET: RequestHandler = async ({ fetch }) => {
|
|||||||
const smard =
|
const smard =
|
||||||
smardRaw && nowSec - smardRaw.measuredAtUnix < 90 * 60 ? smardRaw : null;
|
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
|
// Per-field fallback. SMARD value `null` = filter unavailable; we
|
||||||
// reach for the Energy-Charts equivalent. SMARD `0` is preserved
|
// 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);
|
const netFlowGw = Object.values(flowsByCountry).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
// Historical aggregates derived from CMS `_history`. `null` when the
|
// Historical aggregates derived from CMS `_history`. `null` when the
|
||||||
// backing collection is unavailable (e.g. CMS not yet upgraded with
|
// backing collection is unavailable — widget renders without those panels.
|
||||||
// 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).
|
|
||||||
const todayHourly =
|
const todayHourly =
|
||||||
histStrommix && histStrommix.items.length > 0
|
histStrommix && histStrommix.items.length > 0
|
||||||
? buildTodayHourly(histStrommix.items)
|
? buildTodayHourly(histStrommix.items)
|
||||||
: energyCharts.rawSeries
|
: null;
|
||||||
? buildTodayHourlyFromEC(energyCharts.rawSeries, dayStartSec)
|
|
||||||
: null;
|
|
||||||
const negPriceHoursYtd = histPriceDe ? countNegPriceSlots(histPriceDe.items) : null;
|
const negPriceHoursYtd = histPriceDe ? countNegPriceSlots(histPriceDe.items) : null;
|
||||||
const crossborderYtd = histCrossborder
|
const crossborderYtd = histCrossborder
|
||||||
? aggregateCrossborderRange(histCrossborder.items, yearStartSec)
|
? aggregateCrossborderRange(histCrossborder.items, yearStartSec)
|
||||||
@@ -673,7 +639,7 @@ export const GET: RequestHandler = async ({ fetch }) => {
|
|||||||
priceDeEurMwh: priceDe.price,
|
priceDeEurMwh: priceDe.price,
|
||||||
priceFrEurMwh: priceFr.price,
|
priceFrEurMwh: priceFr.price,
|
||||||
priceMeasuredAtUnix: priceDe.slotUnix,
|
priceMeasuredAtUnix: priceDe.slotUnix,
|
||||||
staleStrommix: smard == null && !energyCharts.data,
|
staleStrommix: smard == null && !energyChartsResolved.data,
|
||||||
staleCrossborder: cross.stale,
|
staleCrossborder: cross.stale,
|
||||||
stalePrice: priceDe.stale || priceFr.stale,
|
stalePrice: priceDe.stale || priceFr.stale,
|
||||||
todayHourly,
|
todayHourly,
|
||||||
|
|||||||
Reference in New Issue
Block a user