fix(strommix): per-field fallback to Energy-Charts when SMARD value missing
The widget rendered "Keine gültigen Live-Werte" the moment SMARD's load filter (id 410) lagged behind the production filters by even one slot — the aggregator coalesced "filter unavailable" with "filter returned 0" and the percent calculation refused to divide by zero. Three changes: 1. SmardSnapshot.values now uses `number | null` so callers can tell "filter genuinely missing" apart from "filter reported zero" (e.g. solar at night, decommissioned nuclear). 2. Aggregator pulls Energy-Charts (CMS strommix_de) in parallel and uses it as a per-field fallback for any SMARD null. New `dataSource: "smard+energy-charts"` flag travels in the response so the attribution stays honest. Pure SMARD when everything resolves; pure Energy-Charts when SMARD is entirely down; the hybrid label for the common case where only load drops out. 3. Widget renders the production breakdown + context row + price row even when the percentage is unavailable, with a clear "Lastwert von SMARD gerade nicht verfügbar" hint instead of going blank.
This commit is contained in:
@@ -186,18 +186,25 @@
|
||||
</div>
|
||||
{:else if !live}
|
||||
<div class="p-6 text-center text-neutral-500 text-sm">Lade Live-Daten…</div>
|
||||
{:else if renewablePct == null}
|
||||
<div class="p-6 text-sm text-amber-700">Keine gültigen Live-Werte verfügbar.</div>
|
||||
{:else}
|
||||
<div class="p-6 text-center">
|
||||
<div class="text-6xl sm:text-7xl font-extrabold tabular-nums {accentClasses.pct}">
|
||||
{Math.round(renewablePct)}%
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-neutral-700">Wind & Solar am Bedarf</div>
|
||||
<div class="mt-3 text-base font-semibold {accentClasses.label}">→ {statusLabel}</div>
|
||||
{#if argumentHtml}
|
||||
<div class="prose prose-sm mt-3 max-w-prose mx-auto text-neutral-800">
|
||||
{@html argumentHtml}
|
||||
{#if renewablePct != null}
|
||||
<div class="text-6xl sm:text-7xl font-extrabold tabular-nums {accentClasses.pct}">
|
||||
{Math.round(renewablePct)}%
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-neutral-700">Wind & Solar am Bedarf</div>
|
||||
<div class="mt-3 text-base font-semibold {accentClasses.label}">→ {statusLabel}</div>
|
||||
{#if argumentHtml}
|
||||
<div class="prose prose-sm mt-3 max-w-prose mx-auto text-neutral-800">
|
||||
{@html argumentHtml}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- Production data arrived but load is missing → no percentage,
|
||||
but we still show what we have so the widget isn't blank. -->
|
||||
<div class="text-sm text-amber-700">
|
||||
Lastwert von SMARD gerade nicht verfügbar — Prozentanzeige ausgesetzt.
|
||||
Erzeugung wird unten in der Detailansicht weiter angezeigt.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -270,9 +277,14 @@
|
||||
<div class="text-neutral-500 font-semibold">Last</div><div class="font-semibold">{fmtMw(live.loadMw)}</div>
|
||||
</div>
|
||||
<p class="mt-1 text-[11px] text-neutral-500">
|
||||
Quelle: {live.dataSource === "smard"
|
||||
? "smard.de (Bundesnetzagentur)"
|
||||
: "energy-charts.info (ENTSO-E + SMARD)"}
|
||||
Quelle:
|
||||
{#if live.dataSource === "smard"}
|
||||
smard.de (Bundesnetzagentur)
|
||||
{:else if live.dataSource === "smard+energy-charts"}
|
||||
smard.de + energy-charts.info (Last-Wert ergänzt)
|
||||
{:else}
|
||||
energy-charts.info (ENTSO-E + SMARD)
|
||||
{/if}
|
||||
— 15-Min-Auflösung, Stand: {fmtTime(live.measuredAtUnix)} Uhr.
|
||||
Konventionell = Braunkohle + Steinkohle + Erdgas + Kernkraft (Pumpspeicher,
|
||||
Sonstige Konventionelle und Wasserkraft sind separat ausgewiesen).
|
||||
|
||||
+8
-4
@@ -116,8 +116,12 @@ export type SmardSnapshot = {
|
||||
* set, so every value is from a slot that all filters have reported.
|
||||
* Unix seconds (not ms). */
|
||||
measuredAtUnix: number;
|
||||
/** Average MW for each filter at the aligned slot. Missing filters → 0. */
|
||||
values: Record<SmardFilter, number>;
|
||||
/** Average MW for each filter at the aligned slot. `null` means the
|
||||
* filter could not be resolved (404, all-null series, fetch error)
|
||||
* — distinguished from a genuine zero so callers can choose a
|
||||
* fallback source per-field instead of ignoring real zeroes
|
||||
* (e.g. solar at night, nuclear in DE). */
|
||||
values: Record<SmardFilter, number | null>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -174,9 +178,9 @@ export async function fetchSmardSnapshot(
|
||||
})
|
||||
);
|
||||
|
||||
const values = {} as Record<SmardFilter, number>;
|
||||
const values = {} as Record<SmardFilter, number | null>;
|
||||
for (const key of Object.keys(SMARD_FILTERS) as SmardFilter[]) {
|
||||
values[key] = 0;
|
||||
values[key] = null;
|
||||
}
|
||||
for (const [key, mwh] of aligned) {
|
||||
if (mwh != null) {
|
||||
|
||||
+7
-4
@@ -11,10 +11,13 @@ export type StrommixResponse = {
|
||||
// the rest of the values. Cross-border + prices may have their own,
|
||||
// earlier or later, timestamps.
|
||||
measuredAtUnix: number | null;
|
||||
/** Where production + load came from. `smard` is canonical (German
|
||||
* grid feed); `energy-charts` is the fallback when SMARD is
|
||||
* unreachable. */
|
||||
dataSource: "smard" | "energy-charts";
|
||||
/** Where production + load came from. `smard` = pure SMARD (canonical);
|
||||
* `smard+energy-charts` = SMARD with Energy-Charts as per-field
|
||||
* fallback for filters SMARD couldn't resolve at the snapshot's slot
|
||||
* (typically the load filter when SMARD's pipeline lags more than
|
||||
* the production filters); `energy-charts` = SMARD entirely
|
||||
* unreachable, full fallback. */
|
||||
dataSource: "smard" | "smard+energy-charts" | "energy-charts";
|
||||
// Production (MW)
|
||||
windOnshoreMw: number;
|
||||
windOffshoreMw: number;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
import type { RequestHandler } from "./$types";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import type { StrommixResponse } from "$lib/strommix";
|
||||
import { fetchSmardSnapshot, type SmardSnapshot } from "$lib/smard";
|
||||
import { fetchSmardSnapshot } from "$lib/smard";
|
||||
|
||||
const CMS_BASE = (
|
||||
env.PUBLIC_CMS_URL ||
|
||||
@@ -32,6 +32,20 @@ const CMS_BASE = (
|
||||
"http://localhost:3000"
|
||||
).replace(/\/$/, "");
|
||||
|
||||
type StrommixRaw = {
|
||||
wind_onshore_mw?: number | null;
|
||||
wind_offshore_mw?: number | null;
|
||||
solar_mw?: number | null;
|
||||
biomass_mw?: number | null;
|
||||
hydro_mw?: number | null;
|
||||
lignite_mw?: number | null;
|
||||
hard_coal_mw?: number | null;
|
||||
gas_mw?: number | null;
|
||||
nuclear_mw?: number | null;
|
||||
load_mw?: number | null;
|
||||
measured_at_unix?: number | null;
|
||||
};
|
||||
|
||||
type CrossborderRaw = {
|
||||
flow_at_gw?: number | null;
|
||||
flow_be_gw?: number | null;
|
||||
@@ -113,26 +127,53 @@ async function fetchPriceAtNow(
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ fetch }) => {
|
||||
const [smard, cross, priceDe, priceFr] = await Promise.all([
|
||||
const [smard, 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),
|
||||
fetchOne<CrossborderRaw>("crossborder_de", fetch),
|
||||
fetchPriceAtNow("DE-LU", fetch),
|
||||
fetchPriceAtNow("FR", fetch),
|
||||
]);
|
||||
|
||||
// Production breakdown — SMARD as primary. Each filter is in MW
|
||||
// already (SMARD MWh × 4).
|
||||
const v: SmardSnapshot["values"] = smard?.values ?? {
|
||||
lignite: 0, hardCoal: 0, gas: 0, nuclear: 0,
|
||||
windOnshore: 0, windOffshore: 0, solar: 0,
|
||||
hydro: 0, biomass: 0,
|
||||
otherConventional: 0, pumpedStorage: 0, load: 0,
|
||||
const ec = energyCharts.data ?? {};
|
||||
|
||||
// Per-field fallback. SMARD value `null` = filter unavailable; we
|
||||
// reach for the Energy-Charts equivalent. SMARD `0` is preserved
|
||||
// (genuine night-time solar / decommissioned nuclear / etc).
|
||||
const pick = (smardVal: number | null | undefined, ecVal: number | null | undefined): number =>
|
||||
typeof smardVal === "number"
|
||||
? smardVal
|
||||
: typeof ecVal === "number" && Number.isFinite(ecVal)
|
||||
? ecVal
|
||||
: 0;
|
||||
|
||||
const v = {
|
||||
lignite: pick(smard?.values.lignite, ec.lignite_mw),
|
||||
hardCoal: pick(smard?.values.hardCoal, ec.hard_coal_mw),
|
||||
gas: pick(smard?.values.gas, ec.gas_mw),
|
||||
nuclear: pick(smard?.values.nuclear, ec.nuclear_mw),
|
||||
windOnshore: pick(smard?.values.windOnshore, ec.wind_onshore_mw),
|
||||
windOffshore: pick(smard?.values.windOffshore, ec.wind_offshore_mw),
|
||||
solar: pick(smard?.values.solar, ec.solar_mw),
|
||||
hydro: pick(smard?.values.hydro, ec.hydro_mw),
|
||||
biomass: pick(smard?.values.biomass, ec.biomass_mw),
|
||||
load: pick(smard?.values.load, ec.load_mw),
|
||||
};
|
||||
|
||||
// Track whether any field needed the Energy-Charts fallback so the
|
||||
// widget can attribute the source honestly.
|
||||
const usedFallback =
|
||||
!smard ||
|
||||
(Object.keys(v) as (keyof typeof v)[]).some(
|
||||
(k) => smard.values[k as keyof typeof smard.values] == null && (v[k] ?? 0) > 0
|
||||
);
|
||||
|
||||
// Conventional, the way the widget label spells it: lignite + hard
|
||||
// coal + gas + nuclear. Pumped-storage + biomass + run-of-river
|
||||
// hydro are NOT included (different categories or already-stored
|
||||
// energy that would double-count).
|
||||
// hydro are NOT included.
|
||||
const conventionalMw = v.lignite + v.hardCoal + v.gas + v.nuclear;
|
||||
const weatherRenewableMw = v.windOnshore + v.windOffshore + v.solar;
|
||||
|
||||
@@ -153,8 +194,8 @@ export const GET: RequestHandler = async ({ fetch }) => {
|
||||
const netFlowGw = Object.values(flowsByCountry).reduce((a, b) => a + b, 0);
|
||||
|
||||
const body: StrommixResponse = {
|
||||
measuredAtUnix: smard?.measuredAtUnix ?? null,
|
||||
dataSource: smard ? "smard" : "energy-charts",
|
||||
measuredAtUnix: smard?.measuredAtUnix ?? ec.measured_at_unix ?? null,
|
||||
dataSource: !smard ? "energy-charts" : usedFallback ? "smard+energy-charts" : "smard",
|
||||
windOnshoreMw: v.windOnshore,
|
||||
windOffshoreMw: v.windOffshore,
|
||||
solarMw: v.solar,
|
||||
@@ -173,7 +214,7 @@ export const GET: RequestHandler = async ({ fetch }) => {
|
||||
priceDeEurMwh: priceDe.price,
|
||||
priceFrEurMwh: priceFr.price,
|
||||
priceMeasuredAtUnix: priceDe.slotUnix,
|
||||
staleStrommix: smard == null,
|
||||
staleStrommix: smard == null && !energyCharts.data,
|
||||
staleCrossborder: cross.stale,
|
||||
stalePrice: priceDe.stale || priceFr.stale,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user