Wetterwarnungen vom DWD – Alternative: GDS (FTP)

ACHTUNG: Der GDS-FTP-Server wird zum 15.01.2018 abgeschaltet.

Wie schon im ersten Beitrag zu erahnen, war ich mit den Wetterdaten auf Landkreisebene nicht wirklich zufrieden. Es soll zwar irgendwann auch eine Json-Schnittstelle mit Gemeindewarnungen geben, aber diese gibt noch nicht.

Der DWD liefert per FTP-Server – ebenfalls kostenlos – Wetterdaten im CAP-Format auf Gemeindeebene an. Das Ganze nennt sich GDS (Global Basic Data Set) und ist hier zu finden:

http://www.dwd.de/DE/leistungen/gds/gds.html?nn=480258

Nach einer kostenlosen Registrierung erhält man per Mail die FTP-Zugangsdaten. Der FTP-Server ist voll von Wetterdaten und Grafiken. An der richtigen Stelle findet man eine Menge ZIP-Dateien die unterschiedlich viele XML-CAP-Dateien enthalten.

FTP-Ordnerstruktur

In der zuletzt erstellten ZIP-Datei sind die aktuellen Meldungen enthalten. Für jede Wetterwarnung in Deutschland steht eine XML-CAP-Datei, worin die betroffenen Regionen aufgelistet sind (auf Gemeindeebene).

Hier ein Beispiel eines „Warnhinweis vor STARKWIND“ für das Seegebiet Viking in der Nordsee (Warncell-ID 401000006):

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<alert xmlns="urn:oasis:names:tc:emergency:cap:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:emergency:cap:1.2 https://werdis.dwd.de/conf/CAP-DWD-Profil-v2.1.xsd">
<identifier>2.49.0.1.276.DWD.PVW.1484593937218.17</identifier>
<sender>CAP@dwd.de</sender>
<sent>2017-01-16T19:13:00+00:00</sent>
<status>Actual</status>
<msgType>Alert</msgType>
<source>PVW</source>
<scope>Public</scope>
<info>
<language>de-DE</language>
<category>Met</category>
<event>Starkwind</event>
<responseType>None</responseType>
<urgency>Immediate</urgency>
<severity>Minor</severity>
<certainty>Observed</certainty>
<eventCode>
<valueName>PROFILE_VERSION</valueName>
<value>2.1</value>
</eventCode>
<eventCode>
<valueName>LICENSE</valueName>
<value>Geobasisdaten: Copyright Bundesamt für Kartographie und Geodäsie, Frankfurt am Main, 2013</value>
</eventCode>
<eventCode>
<valueName>II</valueName>
<value>14</value>
</eventCode>
<eventCode>
<valueName>GROUP</valueName>
<value>WIND</value>
</eventCode>
<eventCode>
<valueName>AREA_COLOR</valueName>
<value>255 255 0</value>
</eventCode>
<effective>2017-01-16T17:19:00+00:00</effective>
<onset>2017-01-16T17:19:00+00:00</onset>
<senderName>DWD / Seewetterdienst Hamburg</senderName>
<headline>Warnhinweis vor STARKWIND </headline>
<description>Süd um 6, südwestdrehend, abnehmend 4 bis 5, strichweise diesig, See 3 Meter. </description>
<instruction/>
<web>http://www.wettergefahren.de</web>
<contact>Deutscher Wetterdienst</contact>
<area>
<areaDesc>Viking</areaDesc>
<geocode>
<valueName>WARNCELLID</valueName>
<value>401000006</value>
</geocode>
<geocode>
<valueName>STATE</valueName>
<value>SH</value>
</geocode>
<altitude>0.0</altitude>
<ceiling>9842.5197</ceiling>
</area>
</info>
</alert>

Im Gegensatz zu den Json-Daten gibt es hierzu eine vollständige Dokumentation, was mich sehr gefreut hat:

http://www.dwd.de/DE/leistungen/gds/help/warnungen/cap_dwd_profile_de_pdf.pdf

Das Prinzip der Json-Variante, bei der bei jedem Aufruf der Webseite, die Json-Daten ausgewertet wurden, lässt sich hier natürlich nicht anwenden. Die Zeit, die das Script brauchen würde, die Infos aus den Dateien in der ZIP-Datei vom FTP-Server zu lesen, wäre viel zu lang und die Darstellung auf der Webseite wäre viel zu sehr verzögert.

Aus diesem Grund ist mir die Idee gekommen, die Wetterdaten selbst aufzubereiten und im gleichen Format wie der DWD es tut bereitzustellen. Das hat den Vorteil, dass ich mein bisheriges Script zur Anzeige der Warnungen kaum verändern muss.

Also erstelle ich ein Script, welches wie folgt strukturiert ist:

  1. Anmeldung an FTP-Server
  2. Prüfen ob Datei bereits heruntergeladen wurde (unnötigen Traffic vermeiden)
  3. Download der aktuellsten ZIP-Datei (Sortierung nach Name)
  4. Abmeldung von FTP-Server
  5. Entpacken in temporäres Verzeichnis
  6. XML-Dateien parsen und Informationen sammeln und aufbereiten
  7. Speichern der Daten als Datei (Datenbank wäre natürlich auch möglich)

Ergebnis ist folgendes Script, welches per Cronjob alle 10 Minuten ausgeführt wird:

<?php
require_once("functions.php");

$ftp_server = "**server**";
$ftp_username = "**username**";
$ftp_userpass = "**password**";
$ftp_conn = ftp_connect($ftp_server) or die("Could not connect to $ftp_server");
$login = ftp_login($ftp_conn, $ftp_username, $ftp_userpass);
ftp_pasv($ftp_conn, true);

$filelist = array();

$filelist = ftp_nlist($ftp_conn,"/gds/gds/specials/alerts/cap/GER/community_status_geometry");
sort($filelist);

$remote_file = end($filelist);
$local_file = "download_cache/".pathinfo($remote_file)['basename'];

if (!file_exists($local_file)) {
	echo "updated file '$remote_file' available<br />";
	$files = glob("download_cache/*");
	foreach($files as $file){
		if(is_file($file)) unlink($file);
	}
	if (!DownloadFile($ftp_conn, $remote_file, $local_file)) {
		exit();
	}
	
	$files = glob("unzip_cache/*");
	foreach($files as $file){
		if(is_file($file)) unlink($file);
	}

	if(!Unzip($local_file, "unzip_cache/")) {
		exit();
	}
} else {
	echo "local file '$local_file' still up to date<br />";
}

ftp_close($ftp_conn);

$alerts = array();

$files = glob("unzip_cache/*");
foreach($files as $file){
	if(is_file($file)) {
		$xml = simplexml_load_file($file);

		foreach($xml->info->area as $area) {
			$alert['start'] = (int) strtotime($xml->info->onset) * 1000;
			$alert['end'] = (int) strtotime($xml->info->expires) * 1000;
			$alert['regionName'] = (string) $area->areaDesc;
			$alert['level'] = getLevel($xml->info);
			$alert['type'] = (string) getEventCode("GROUP", $xml->info);
			$alert['altitudeStart'] = getAltitudeStartFromAltitude((float) $area->altitude);
			$alert['event'] = (string) $xml->info->event;
			$alert['headline'] = (string) $xml->info->headline;
			$alert['description'] = (string) $xml->info->description;
			$alert['altitudeEnd'] = getAltitudeEndFromCeiling((float) $area->ceiling);
			$alert['stateShort'] = (string) getGeocode("STATE", $area);
			$alert['instruction'] = (string) $xml->info->instruction;
			$alert['state'] = getState($area);
			
			$alert['ii'] = (int) getEventCode("II", $xml->info);
			$alert['published'] = (int) strtotime($xml->info->effective) * 1000;
			$alerts['time'] = (int) strtotime(date("c")) * 1000;

			if ($alert['regionName'] != "polygonal event area") {
				if((string) $xml->info->urgency == "Immediate") {
					$alerts['warnings'][(string) $area->geocode[0]->value][] = $alert;
				} else {
					$alerts['vorabInformation'][(string) $area->geocode[0]->value][] = $alert;
				}
				
			}
		}
	}
}
echo "found ".count($alerts['warnings'])." warnings<br />";
echo "saving warnings to 'warnings.json'<br />";
$fp = fopen("warnings.json", "w");
fwrite($fp, json_encode($alerts, JSON_PRETTY_PRINT));
fclose($fp);
echo "warnings successfully saved<br /> ";

?>

 

<?php
function Unzip($zipFile, $unzipDir) {
	echo "<br />";
	echo "Unzipping '$zipFile' <br />";
	$zip = new ZipArchive;
	$result = $zip->open($zipFile);
	if($result !== true){
	 echo "Error :- Unable to open the Zip File: $result";
	 return false;
	} 
	/* Extract Zip File */
	$zip->extractTo($unzipDir);
	$zip->close();
	echo "Unzipped to '$unzipDir' <br /><br />";
	return true;
}

function DownloadFile($ftp_conn, $server_file, $local_file) {
	echo "Downloading '$server_file' from server  <br />";
	echo "Creating local file '$local_file' <br />";
	$fp = fopen($local_file,"w");

	// download server file and save it to open local file
	if (ftp_fget($ftp_conn, $fp, $server_file, FTP_BINARY, 0))
	  {
	  echo "Successfully written to '$local_file'. <br />";
	  return true;
	  }
	else
	  {
	  echo "Error downloading '$server_file'. <br />";
	  return false;
	  }
	fclose($fp);
}

function getLevel($info) {
	if ($info->urgency == "Future") return 1;
	switch($info->severity) {
		case "M":
			return 1;
			break;
		case "Minor":
			return 2;
			break;
		case "Moderate":
			return 3;
			break;
		case "Severe":
			return 4;
			break;
		case "Extreme":
			return 5;
			break;
	}
}

function getState($area) {
	$stateShort = getGeocode("STATE", $area);
	switch($stateShort) {
		case "NRW":
			return "Nordrhein-Westfalen";
			break;
		case "RP":
			return "Rheinland-Pfalz";
			break;
		case "BY":
			return "Bayern";
			break;
		case "BW":
			return "Baden-Württemberg";
			break;
		case "HE":
			return "Hessen";
			break;
		case "SN":
			return "Sachsen";
			break;
		case "TH":
			return "Thüringen";
			break;
		case "NS":
			return "Niedersachsen";
			break;
		case "HH":
			return "Hamburg";
			break;
		case "HB":
			return "Bremen";
			break;
		case "SH":
			return "Schleswig-Holstein";
			break;
		case "SL":
			return "Saarland";
			break;
		case "SA":
			return "Sachsen-Anhalt";
			break;
		case "BB":
			return "Brandenburg";
			break;
		case "BL":
			return "Berlin";
			break;
		case "MV":
			return "Mecklenburg-Vorpomern";
			break;
	}
}

function getGeocode($geocode, $area) {
	foreach($area->geocode as $code) {
		if ($code->valueName == $geocode){
			return (string) $code->value;
		}
	}
}

function getEventCode($eventCode, $info) {
	foreach($info->eventCode as $code) {
		if ($code->valueName == $eventCode){
			return (string) $code->value;
		}
	}
}

function getAltitudeStart($area) {
	$altCode = getGeocode("ALTITUDE", $area);
	switch($altCode) {
		case "B":
			return 200;
		case "C":
			return 400;
		case "D":
			return 600;
		case "E":
			return 800;
		case "F":
			return 1000;
		case "G":
			return 1500;
		case "H":
			return 2000;
		case "L":
			return 0;
		case "M":
			return 0;
		case "N":
			return 0;
		case "A":
			return 0;
	}
}

function getAltitudeEnd($area) {
	$altCode = getGeocode("ALTITUDE", $area);
	switch($altCode) {
		case "B":
			return 3000;
		case "C":
			return 3000;
		case "D":
			return 3000;
		case "E":
			return 3000;
		case "F":
			return 3000;
		case "G":
			return 3000;
		case "H":
			return 3000;
		case "L":
			return 800;
		case "M":
			return 600;
		case "N":
			return 400;
		case "A":
			return 200;
	}
}

function getAltitudeStartFromAltitude($altitude) {
	$result = round($altitude * 0.3048);
	if ($result == 0) {
		return null;
	} else {
		return $result;
	}
}

function getAltitudeEndFromCeiling($ceiling) {
	$result = round($ceiling * 0.3048);
	if ($result == 3000) {
		return null;
	} else {
		return $result;
	}
}
?>

Der Abruf der Wetterwarnungen für eine bestimmte Gemeinde geschieht über folgendes Script:

<?php
$region = $_GET['regionCode'];
header("Content-Type: application/json");

$json = file_get_contents("warnings.json");
$jsonObj = json_decode($json);
$json = null;   //Speicherauslastung verringern

$warnings = array();
$warnings['time'] = (string) $jsonObj->time;
$warnings['warnings'][$region] = $jsonObj->warnings->$region;
$warnings['vorabInformation'][$region] = $jsonObj->vorabInformation->$region;

$jsonObj = null; //Speicherauslastung verringern
echo "warnWetter.loadWarnings(".json_encode($warnings).");";

?>

Beispiel:

http://mt88.eu/weather/get_warnings.php?regionCode=401000006

Weitere Infos: Unwetterdaten Service

Categories: DWD Unwetter Parser