diff --git a/src/lib/components/blocks/StrommixBlock.svelte b/src/lib/components/blocks/StrommixBlock.svelte
index 72ca55d..7e5bf82 100644
--- a/src/lib/components/blocks/StrommixBlock.svelte
+++ b/src/lib/components/blocks/StrommixBlock.svelte
@@ -186,18 +186,25 @@
{:else if !live}
Lade Live-Daten…
- {:else if renewablePct == null}
- Keine gültigen Live-Werte verfügbar.
{:else}
-
-
Wind & Solar am Bedarf
-
→ {statusLabel}
- {#if argumentHtml}
-
- {@html argumentHtml}
+ {#if renewablePct != null}
+
+
Wind & Solar am Bedarf
+
→ {statusLabel}
+ {#if argumentHtml}
+
+ {@html argumentHtml}
+
+ {/if}
+ {:else}
+
+
+ Lastwert von SMARD gerade nicht verfügbar — Prozentanzeige ausgesetzt.
+ Erzeugung wird unten in der Detailansicht weiter angezeigt.
{/if}
@@ -270,9 +277,14 @@
Last
{fmtMw(live.loadMw)}
- 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).
diff --git a/src/lib/smard.ts b/src/lib/smard.ts
index 62fe6c8..b648984 100644
--- a/src/lib/smard.ts
+++ b/src/lib/smard.ts
@@ -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;
+ /** 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;
};
/**
@@ -174,9 +178,9 @@ export async function fetchSmardSnapshot(
})
);
- const values = {} as Record;
+ const values = {} as Record;
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) {
diff --git a/src/lib/strommix.ts b/src/lib/strommix.ts
index 334f540..1f469c9 100644
--- a/src/lib/strommix.ts
+++ b/src/lib/strommix.ts
@@ -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;
diff --git a/src/routes/api/strommix/+server.ts b/src/routes/api/strommix/+server.ts
index 1add633..ae40176 100644
--- a/src/routes/api/strommix/+server.ts
+++ b/src/routes/api/strommix/+server.ts
@@ -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("strommix_de", fetch),
fetchOne("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,
};