Wettervorhersagen mit Daten des DWD – Teil [3/4]: MOSMIX verstehen

Wer mit DWD-Daten arbeiten will, muss sich mit KMZ (Keyhole Markup Language Zipped)-Dateien auseinandersetzen. Eine Binärdatei, die eine XML-Datei mit Hunderten von Zeitreihen enthält, in einem Schema, das man aus keiner npm-Paketbeschreibung errät.

Dieser Artikel erklärt das MOSMIX-Format, warum es so aufgebaut ist, was es enthält und welche Fähigkeiten ein Programm braucht, das darauf zugreifen will.

Was MOSMIX ist und warum dieses Format?

MOSMIX (Model Output Statistics Mix) ist das Kurzfrist-Vorhersageprodukt des DWD. Es kombiniert numerische Wettermodelle (ICON, ECMWF) mit statistischen Korrekturen aus historischen Stationsbeobachtungen.

Das Format KMZ hat eine Geschichte: KML (Keyhole Markup Language) ist ein XML-Dialekt des Open Geospatial Consortium, ursprünglich von Google Earth als Geo-Markup-Format entwickelt. KMZ ist das komprimierte Archivformat (ZIP) dazu. Das DWD verwendet KML nicht für seinen Ursprungszweck (geografische Features wie Punkte, Linien, Polygone), sondern als Träger für Zeitreihen-Daten, erweitert durch einen dwd:–Namensraum mit proprietären Elementen.

Das Format ist ein WMO (World Meteorological Organization) -Standard und garantiert Interoperabilität mit anderen Wetterdiensten weltweit. Für Entwickler hat es den Nachteil, dass es unvertraut ist, aber den Vorteil, dass es vollständig selbst beschreibend ist und keine externe Schemadefinition braucht.

Aufbau einer KMZ-Datei des DWD

1. Die Hülle (Das KMZ-Archiv)

Technisch gesehen ist die Datei ein ZIP-Archiv. Wenn du sie entpackst, findest du darin meist nur eine einzige Datei namens doc.kml.

2. Der Kopf der Datei (Metadaten)

Der XML-Header enthält wichtige Informationen über den Ersteller und den Lauf des Modells.

<kml xmlns:dwd="https://opendata.dwd.de/weather/lib/gml/mosmix/1.0">
<Document>
<ExtendedData>
<dwd:ProductDefinition>
<dwd:Issuer>Deutscher Wetterdienst</dwd:Issuer>
<dwd:ProductID>MOSMIX</dwd:ProductID>
<dwd:IssueTime>2026-04-10T15:00:00.000Z</dwd:IssueTime>
<dwd:ReferencedModel>
<dwd:Model name="ICON" baseTime="2026-04-10T12:00:00Z" />
<dwd:Model name="ECMWF" baseTime="2026-04-10T00:00:00Z" />
</dwd:ReferencedModel>
</dwd:ProductDefinition>
</ExtendedData>

<kml xmlns:dwd="https://opendata.dwd.de/weather/lib/gml/mosmix/1.0">
<Document>
<ExtendedData>
<dwd:ProductDefinition>
<dwd:Issuer>Deutscher Wetterdienst</dwd:Issuer>
<dwd:ProductID>MOSMIX</dwd:ProductID>
<dwd:IssueTime>2026-04-10T15:00:00.000Z</dwd:IssueTime>
<dwd:ReferencedModel>
<dwd:Model name="ICON" baseTime="2026-04-10T12:00:00Z" />
<dwd:Model name="ECMWF" baseTime="2026-04-10T00:00:00Z" />
</dwd:ReferencedModel>
</dwd:ProductDefinition>
</ExtendedData>

IssueTime: Wann die Vorhersage berechnet wurde.

ReferencedModel: Hier sieht man, dass MOSMIX ein „Mix“ aus dem deutschen ICON und dem europäischen ECMWF Modell ist.

3. Die Zeitachse (ForecastTimeSteps)

Vor den eigentlichen Wetterdaten, definiert der DWD eine zentrale Zeitachse. Alle folgenden Wetterwerte beziehen sich indexbasiert auf diese Liste, d.h. die Wetterdaten enthalten keinen Zeitstempel, sondern die Datenblöcke müssen mit der Timesteps-Liste abgeglichen werden.

<dwd:ForecastTimeSteps>
<dwd:TimeStep>2026-04-10T16:00:00.000Z</dwd:TimeStep>
<dwd:TimeStep>2026-04-10T17:00:00.000Z</dwd:TimeStep>
<dwd:TimeStep>2026-04-10T18:00:00.000Z</dwd:TimeStep>
</dwd:ForecastTimeSteps>

<dwd:ForecastTimeSteps>
<dwd:TimeStep>2026-04-10T16:00:00.000Z</dwd:TimeStep>
<dwd:TimeStep>2026-04-10T17:00:00.000Z</dwd:TimeStep>
<dwd:TimeStep>2026-04-10T18:00:00.000Z</dwd:TimeStep>
</dwd:ForecastTimeSteps>

4. Die Wetterdaten (Placemark)

Jede Station wird als Placemark geführt. Das Herzstück sind die ExtendedData-Blöcke, in denen die Zeitreihen kommagetrennt (CSV-Stil innerhalb von XML) gespeichert sind.

<Placemark>
<kml:name>BERLIN-BRANDENBURG</kml:name>
<kml:description>WMO-ID: 10382</kml:description>
<kml:Point>
<kml:coordinates>13.50,52.36,46.0</kml:coordinates>
</kml:Point>
<kml:ExtendedData>
<dwd:Forecast dwd:elementName="TTT">
<dwd:value>
283.55, 282.45, 281.15, 280.05 ...
</dwd:value>
</dwd:Forecast>

<dwd:Forecast dwd:elementName="ff">
<dwd:value>
4.1, 3.8, 3.5, 3.2 ...
</dwd:value>
</dwd:Forecast>
</kml:ExtendedData>
</Placemark>

<Placemark>
<kml:name>BERLIN-BRANDENBURG</kml:name>
<kml:description>WMO-ID: 10382</kml:description>
<kml:Point>
<kml:coordinates>13.50,52.36,46.0</kml:coordinates>
</kml:Point>
<kml:ExtendedData>
<dwd:Forecast dwd:elementName="TTT">
<dwd:value>
283.55, 282.45, 281.15, 280.05 ...
</dwd:value>
</dwd:Forecast>

<dwd:Forecast dwd:elementName="ff">
<dwd:value>
4.1, 3.8, 3.5, 3.2 ...
</dwd:value>
</dwd:Forecast>
</kml:ExtendedData>
</Placemark>

Der „Kürzel-Dschungel“

Der „Kürzel-Dschungel“ ist für Entwickler und Meteorologen Fluch und Segen zugleich. Diese Abkürzungen stammen aus einer Zeit, in der Speicherplatz und Bandbreite extrem teuer waren. Anstatt sprechende Namen wie Temperature_Kelvin zu verwenden, nutzt der DWD kompakte 2- bis 5-stellige Kürzel, die auf internationalen Standards (WMO) oder internen Konventionen basieren.

Hier ist eine kleine Aufschlüsselung der wichtigsten Kategorien, die dir in den MOSMIX-KMZ-Dateien begegnen werden:

1. Thermische Parameter (Temperatur & Feuchte)

Diese Kürzel beginnen fast immer mit einem T.

TTT: Die Lufttemperatur in 2 Meter Höhe. Achtung: Im MOSMIX in Kelvin angegeben (0 °C = 273,15 K).

Td: Der Taupunkt (Dew Point). Ein Maß für die Luftfeuchtigkeit.

TN/ TX: Die Minimal- bzw. Maximaltemperatur innerhalb eines Zeitintervalls (oft 6 oder 12 Stunden), ebenfalls in Kelvin.

2. Wind-Parameter

Die Abkürzungen beginnen mit f (für lateinisch fortitudo = Stärke) oder d (direction).

ff: Die mittlere Windgeschwindigkeit (m/s).

DD: Die Windrichtung in Grad (0-360°, 0° = Norden, 90° = Osten, 180° = Süden und 270°=Westen).

FX1: Die Windspitze bzw. Böe (Maximum Gust) in der letzten Stunde. In Warnwetter-Apps ist dies der wichtigste Wert für Sturmwarnungen

3. Bewölkung und Sicht

Das Kürzel N steht traditionell für die Bewölkung (lat. nimbus oder nebulosus).

Neff: Die „effektive“ Gesamtbewölkung. Ein gewichteter Wert, der angibt, wie stark der Himmel bedeckt ist (in Prozent oder Achteln).

Nl / Nm / Nh: Die Bewölkung in verschiedenen Höhen (2 km, 2-7 km, >7 km).

VV: Die horizontale Sichtweite (oft in Metern).

4. Niederschlag

RR1c: Die Niederschlagsmenge der letzten Stunde (in kg/m2 oder mm). Das „c“ steht oft für consistent oder corrected.

RRS1c: Die Neuschneemenge (Snow) innerhalb einer Stunde.

Ein vollständige Liste findet man unter MetElementDefintion.xml.

Was jede Applikation braucht

Jede Applikation, die MOSMIX-Daten verarbeitet, durchläuft dieselben Schritte:

Daten vom DWD-Server laden

Daten von XML in ein Format konvertieren, mit dem JavaScript arbeiten kann

Zeitreihen synchronieren

Einheiten konvertieren

Ausgabe

Jeder dieser Schritte hat seine eigenen Tücken. Wir gehen sie nacheinander durch.

Schritt 1: Daten vom DWD-Server laden

Der Download selbst ist nicht schwierig, aber der User-Agent-Header ist verpflichtend — nicht technisch erzwungen, aber in den DWD-Nutzungsbedingungen als Anforderung formuliert. Die stationID wurde im vorherigen Teil 2 ermittelt.

const USER_AGENT = 'dwd-weather-cli/1.0 (github.com/latz/dwd-weather)';

async function fetchKMZ(stationId) {
const url = `https://opendata.dwd.de/weather/local_forecasts/mos/MOSMIX_L/` +
`single_stations/${stationId}/kml/MOSMIX_L_LATEST_${stationId}.kmz`;

const response = await fetch(url, {
headers: {
'User-Agent': USER_AGENT
}
});

if (response.status === 404) {
throw new Error(`Station ${stationId} has no MOSMIX data available`);
}

if (!response.ok) {
throw new Error(`Failed to fetch KMZ: ${response.status} ${response.statusText}`);
}

return Buffer.from(await response.arrayBuffer());
}

const USER_AGENT = 'dwd-weather-cli/1.0 (github.com/latz/dwd-weather)';

async function fetchKMZ(stationId) {
const url = `https://opendata.dwd.de/weather/local_forecasts/mos/MOSMIX_L/` +
`single_stations/${stationId}/kml/MOSMIX_L_LATEST_${stationId}.kmz`;

const response = await fetch(url, {
headers: {
'User-Agent': USER_AGENT
}
});

if (response.status === 404) {
throw new Error(`Station ${stationId} has no MOSMIX data available`);
}

if (!response.ok) {
throw new Error(`Failed to fetch KMZ: ${response.status} ${response.statusText}`);
}

return Buffer.from(await response.arrayBuffer());
}

USER_AGENTist eine Konstante wie‚dwd-weather-cli/1.0 (github.com/latz/dwd-weather)’`. Der gleiche Header wurde bereits bei Nominatim (Geocoding, Teil 2) genutzt.

Das 404-Handling („Datei nicht gefunden“) ist wichtig: Nicht alle der 2.698 Stationen im Katalog haben zu jedem Zeitpunkt verfügbare Daten. DWD-Stationen können temporär ausfallen oder reorganisiert werden. Eine Anwendung muss behandeln können.

Seit Node 18 kann man AbortSignal.timeout() für Timeouts nutzen:

const response = await fetch(url, {
headers: { 'User-Agent': USER_AGENT },
signal: AbortSignal.timeout(15_000) // 15s
});

const response = await fetch(url, {
headers: { 'User-Agent': USER_AGENT },
signal: AbortSignal.timeout(15_000) // 15s
});

Schritt 2: ZIP-Extraktion

Das KMZ ist eine ZIP-Datei. Für Node.js ist unzipperder robusteste Entpacker. Er liest die ZIP-Struktur inkrementell und gibt einzelne Einträge als Streams zurück, ohne die Datei zweimal zu puffern:

import unzipper from 'unzipper';
import { Readable } from 'stream';

async function unzipKMZ(kmzBuffer) {
return new Promise((resolve, reject) => {
const stream = Readable.from([kmzBuffer]);

stream.pipe(unzipper.Parse())
.on('entry', entry => {
if (entry.path.endsWith('.kml')) {
entry.buffer().then(buf => {
resolve(buf);
}).catch(reject);
} else {
entry.autodrain(); // Andere Einträge verwerfen
}
})
.on('error', reject);
});
}

import unzipper from 'unzipper';
import { Readable } from 'stream';

async function unzipKMZ(kmzBuffer) {
return new Promise((resolve, reject) => {
const stream = Readable.from([kmzBuffer]);

stream.pipe(unzipper.Parse())
.on('entry', entry => {
if (entry.path.endsWith('.kml')) {
entry.buffer().then(buf => {
resolve(buf);
}).catch(reject);
} else {
entry.autodrain(); // Andere Einträge verwerfen
}
})
.on('error', reject);
});
}

Um eine Stream-Blockade zu vermeiden, müssen alle ZIP-Einträge entweder verarbeitet oder via autodrain() verworfen („abgeführt“) werden.
Auch wenn die aktuelle Spezifikation des DWD nur eine KML-Datei vorsieht, ist dieser defensive Ansatz notwendig, um die Kompatibilität bei zukünftigen Dateierweiterungen zu gewährleisten.

Schritt 3: KML-Parsing

Die KML-Daten liegen im XML-Format vor. Mittels xml2js werden diese in ein natives JavaScript-Objekt geparst, das sich anschließend einfacher als XML-Daten verarbeiten lässt.

import xml2js from 'xml2js';

const parser = new xml2js.Parser();
const kml = await parser.parseStringPromise(kmlBuffer);

import xml2js from 'xml2js';

const parser = new xml2js.Parser();
const kml = await parser.parseStringPromise(kmlBuffer);

DWD-KML-Dateien sind ISO-8859-1-kodiert, nicht UTF-8. xml2js handhabt das automatisch — aber wer einen anderen Parser einsetzt, muss das explizit berücksichtigen. Stationsnamen mit Umlauten (z.B. „MÜNCHEN-FLUGHAFEN“) werden sonst falsch dekodiert.

Das resultierende kml-Objekt spiegelt die XML-Hierarchie direkt wider. Die Naemsräume bleiben erhalten:

kml['kml:kml']['kml:Document'][0] // Das einzige Document-Element

kml['kml:kml']['kml:Document'][0] // Das einzige Document-Element

Schritt 4: Zeitstempel aus ProductDefinition

Das ist die Stelle, die am meisten Verwirrung stiftet: Die Zeitstempel stehen im ProductDefinition -Element des Dokuments, nicht in der Placemark und sind ISO 8601 in UTC.

<dwd:ForecastTimeSteps>
<dwd:TimeStep>2026-05-06T11:00:00.000Z</dwd:TimeStep>
<dwd:TimeStep>2026-05-06T12:00:00.000Z</dwd:TimeStep>
<dwd:TimeStep>2026-05-06T13:00:00.000Z</dwd:TimeStep>
<dwd:TimeStep>2026-05-06T14:00:00.000Z</dwd:TimeStep>
[...]
</dwd:ForecastTimeSteps>

<dwd:ForecastTimeSteps>
<dwd:TimeStep>2026-05-06T11:00:00.000Z</dwd:TimeStep>
<dwd:TimeStep>2026-05-06T12:00:00.000Z</dwd:TimeStep>
<dwd:TimeStep>2026-05-06T13:00:00.000Z</dwd:TimeStep>
<dwd:TimeStep>2026-05-06T14:00:00.000Z</dwd:TimeStep>
[...]
</dwd:ForecastTimeSteps>

const doc = kml['kml:kml']['kml:Document'][0];
const docExt = doc['kml:ExtendedData'] && doc['kml:ExtendedData'][0];
const prodDef = docExt['dwd:ProductDefinition'][0];
const timeSteps = prodDef['dwd:ForecastTimeSteps'][0]['dwd:TimeStep'];

const timestamps = timeSteps.map(ts => new Date(ts));
// [Date(2026-03-28T21:00:00Z), Date(2026-03-28T22:00:00Z), ...]

const doc = kml['kml:kml']['kml:Document'][0];
const docExt = doc['kml:ExtendedData'] && doc['kml:ExtendedData'][0];
const prodDef = docExt['dwd:ProductDefinition'][0];
const timeSteps = prodDef['dwd:ForecastTimeSteps'][0]['dwd:TimeStep'];

const timestamps = timeSteps.map(ts => new Date(ts));
// [Date(2026-03-28T21:00:00Z), Date(2026-03-28T22:00:00Z), ...]

Die Zeitstempel und Werte sind bewusst getrennt gespeichert und müssen per Index abgeglichen werden — Value 0 gehört zu Timestamp 0, Value 1 zu Timestamp 1, usw. (für Programmiersprachen mit null-basierten Feldern). Das ist das Grundprinzip der KML-Zeitreihenstruktur: einmal eine geteilte Zeitachse, dann beliebig viele Parameter-Felder.

Der Code sollte mehrere mögliche Pfade prüfen, weil sich die genaue Verschachtelung in verschiedenen MOSMIX-Varianten unterscheiden kann:

let timeSteps;
if (docExt && docExt['dwd:ProductDefinition']) {
const prodDef = docExt['dwd:ProductDefinition'][0];
if (prodDef['dwd:ForecastTimeSteps']) {
timeSteps = prodDef['dwd:ForecastTimeSteps'][0]['dwd:TimeStep'];
}
}
if (!timeSteps && docExt && docExt['dwd:ForecastTimeSteps']) {
timeSteps = docExt['dwd:ForecastTimeSteps'][0]['dwd:TimeStep'];
}
if (!timeSteps) {
throw new Error('No ForecastTimeSteps found in KML');
}

let timeSteps;
if (docExt && docExt['dwd:ProductDefinition']) {
const prodDef = docExt['dwd:ProductDefinition'][0];
if (prodDef['dwd:ForecastTimeSteps']) {
timeSteps = prodDef['dwd:ForecastTimeSteps'][0]['dwd:TimeStep'];
}
}
if (!timeSteps && docExt && docExt['dwd:ForecastTimeSteps']) {
timeSteps = docExt['dwd:ForecastTimeSteps'][0]['dwd:TimeStep'];
}
if (!timeSteps) {
throw new Error('No ForecastTimeSteps found in KML');
}

Schritt 5: Vorhersagewerte aus der Placemark

Die eigentlichen Vorhersagewerte liegen in den <dwd:Forecast>-Elementen der kml:Placemark -Elemente:

<Forecast dwd:elementName="TTT">
<dwd:value>283.15 284.50 285.20 ... </dwd:value>
</Forecast>
<Forecast dwd:elementName="RR1c">
<dwd:value>0.0 1.2 0.5 ...</dwd:value>
</Forecast>

<Forecast dwd:elementName="TTT">
<dwd:value>283.15 284.50 285.20 ... </dwd:value>
</Forecast>
<Forecast dwd:elementName="RR1c">
<dwd:value>0.0 1.2 0.5 ...</dwd:value>
</Forecast>

Die Werte sind Fließkommazahlen getrennt durch Leerzeichen. Der String ‚-‚ bedeutet, das kein Wert verfügbar ist.

const placemark = doc['kml:Placemark'][0];
const pmExt = placemark['kml:ExtendedData'] && placemark['kml:ExtendedData'][0];
const forecasts = pmExt && pmExt['dwd:Forecast'] || [];
const parametersMap = {};

for (const forecast of forecasts) {
const elementName = forecast['$']['dwd:elementName']; // z.B. "TTT"
const valueStr = forecast['dwd:value'][0];

const values = valueStr.split(/\s+/).map(v =>
v === '-' ? NaN : parseFloat(v)
);

parametersMap[elementName] = values;
}
// { TTT: [283.15, 284.50, ...], RR1c: [0.0, 1.2, ...], FF: [...], ... }

const placemark = doc['kml:Placemark'][0];
const pmExt = placemark['kml:ExtendedData'] && placemark['kml:ExtendedData'][0];
const forecasts = pmExt && pmExt['dwd:Forecast'] || [];
const parametersMap = {};

for (const forecast of forecasts) {
const elementName = forecast['$']['dwd:elementName']; // z.B. "TTT"
const valueStr = forecast['dwd:value'][0];

const values = valueStr.split(/\s+/).map(v =>
v === '-' ? NaN : parseFloat(v)
);

parametersMap[elementName] = values;
}
// { TTT: [283.15, 284.50, ...], RR1c: [0.0, 1.2, ...], FF: [...], ... }

Das parametersMap-Objekt enthält bis zu 115 Parameter. Der Index stimmt mit den Zeitstempeln überein: parametersMap['TTT'][5] ist die Temperatur zum sechsten Zeitpunkt (in Programmiersprachen mit null-basierten Feldern)

Schritt 6: Zeitreihen zusammenführen

Wir haben jetzt zwei Objekte vorliegen, die wir zusammenführen müssen:

const timeseries = timestamps.map((time, idx) => {
const point = { time };

if (parametersMap['TTT']) {
const kelvin = parametersMap['TTT'][idx];
point.temp = Number.isFinite(kelvin) ? kelvin - 273.15 : null;
}

if (parametersMap['RR1c']) {
const precip = parametersMap['RR1c'][idx];
point.precipitation = Number.isFinite(precip) ? precip : null;
}

if (parametersMap['FF']) {
const windMs = parametersMap['FF'][idx];
point.windSpeed = Number.isFinite(windMs) ? windMs * 3.6 : null;
}

// ... weitere Parameter ...

return point;
});

const timeseries = timestamps.map((time, idx) => {
const point = { time };

if (parametersMap['TTT']) {
const kelvin = parametersMap['TTT'][idx];
point.temp = Number.isFinite(kelvin) ? kelvin - 273.15 : null;
}

if (parametersMap['RR1c']) {
const precip = parametersMap['RR1c'][idx];
point.precipitation = Number.isFinite(precip) ? precip : null;
}

if (parametersMap['FF']) {
const windMs = parametersMap['FF'][idx];
point.windSpeed = Number.isFinite(windMs) ? windMs * 3.6 : null;
}

// ... weitere Parameter ...

return point;
});

Schritt 7: Einheitenkonvertierung

Das DWD liefert die Daten in SI-Einheiten. Die meisten Leute bevorzugen allerdings die „klassischen“ Einheiten. Hier ein paar Beispiele zur Umrechnung:

Parameter | DWD-Einheit | Ausgabe-Einheit | Umrechnung
TTT (Temperatur) | Kelvin | °Celsius | K – 273.15
Td (Taupunkt) | Kelvin | °Celsius | K – 273.15
FF (Windgeschwindigkeit) | m/s | km/h | × 3.6
FX1 (Windböen) | m/s | km/h | × 3.6
PPPP (Luftdruck) | Pascal | hPa ( mbar) | ÷ 100

Relative Luftfeuchtigkeit (RH) wird von MOSMIX nicht direkt geliefert, ist aber aus Temperatur und Taupunkt berechenbar.

Das Ergebnis

Nach der Verarbeitung der Daten liegt ein Array mit ca. 240 Objekten vor:

[
{
time: Date(2026-03-28T21:00:00.000Z),
temp: 10.4, // °C
precipitation: 0.0, // mm
windSpeed: 14.4, // km/h
windGust: 21.6, // km/h
cloudCover: 45, // %
weatherCode: 2, // WMO-Code
dewpoint: 5.1, // °C
visibility: 22000, // m
pressure: 1018.2, // hPa
windDir: 337.5 // Grad
},
// ... 239 weitere Zeitpunkte
]

[
{
time: Date(2026-03-28T21:00:00.000Z),
temp: 10.4, // °C
precipitation: 0.0, // mm
windSpeed: 14.4, // km/h
windGust: 21.6, // km/h
cloudCover: 45, // %
weatherCode: 2, // WMO-Code
dewpoint: 5.1, // °C
visibility: 22000, // m
pressure: 1018.2, // hPa
windDir: 337.5 // Grad
},
// ... 239 weitere Zeitpunkte
]

Von hier aus können die Daten weiter verarbeitet werden.