# $Id: 55_GDS.pm 11734 2016-07-03 14:12:48Z betateilchen $

# copyright and license informations
=pod
###################################################################################################
#
#	55_GDS.pm
#
#	An FHEM Perl module to retrieve data from "Deutscher Wetterdienst"
#
#	Copyright: betateilchen ®
#
#   includes:  some patches provided by jensb
#              forecasts    provided by jensb
#
#	This file is part of fhem.
#
#	Fhem is free software: you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation, either version 2 of the License, or
#	(at your option) any later version.
#
#	Fhem is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#	GNU General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with fhem.  If not, see <http://www.gnu.org/licenses/>.
#
###################################################################################################
=cut

package main;

use strict;
use warnings;
use feature qw/switch/;

use Blocking;
#use Archive::Extract;
use Net::FTP;
use XML::Simple;

#use Data::Dumper;

eval "use GDSweblink";

no if $] >= 5.017011, warnings => 'experimental';

my ($bulaList, $cmapList, %rmapList, $fmapList, %bula2bulaShort, 
    %bulaShort2dwd, %dwd2Dir, %dwd2Name, $alertsXml, %capCityHash,
    %capCellHash, $sList, $aList, $fList, $fcList, $fcmapList, $tempDir, @weekdays);

my %allForecastData;

###################################################################################################
#
#	Main routines

sub GDS_Initialize($) {
	my ($hash) = @_;
	my $name = $hash->{NAME};

	return "This module must not be used on microso... platforms!" if($^O =~ m/Win/);

	$hash->{DefFn}		=	"GDS_Define";
	$hash->{UndefFn}	=	"GDS_Undef";
	$hash->{DeleteFn}	=	"GDS_Delete";
	$hash->{GetFn}		=	"GDS_Get";
	$hash->{SetFn}		=	"GDS_Set";
	$hash->{RenameFn}	=	"GDS_Rename";
	$hash->{ShutdownFn}	=	"GDS_Shutdown";
#	$hash->{NotifyFn}   =   "GDS_Notify";
	$hash->{NOTIFYDEV}  =   "global";
	$hash->{AttrFn}		=	"GDS_Attr";
	
	no warnings 'qw';
	my @attrList = qw(
		disable:0,1
		gdsAll:0,1
		gdsDebug:0,1
		gdsFwName
		gdsFwType:0,1,2,3,4,5,6,7
		gdsHideFiles:0,1
		gdsLong:0,1
		gdsPassiveFtp:1,0
		gdsPolygon:0,1
		gdsSetForecast
		gdsUseAlerts:0,1
		gdsUseForecasts:0,1
	);
	use warnings 'qw';
	$hash->{AttrList}  = join(" ", @attrList);
	$hash->{AttrList} .= " $readingFnAttributes";

    $tempDir  = "/tmp/";
	_fillMappingTables($hash);

}
sub _fillMappingTables($){
    my ($hash) = @_;

    $tempDir  = "/tmp/";
    
    $aList    = "no_data";
	$sList    = $aList;
 	$fList    = $aList;
 	$fcList   = $aList;

	$bulaList =	"Baden-Württemberg,Bayern,Berlin,Brandenburg,Bremen,".
				"Hamburg,Hessen,Mecklenburg-Vorpommern,Niedersachsen,".
				"Nordrhein-Westfalen,Rheinland-Pfalz,Saarland,Sachsen,".
				"Sachsen-Anhalt,Schleswig-Holstein,Thüringen";

	$cmapList =	"Deutschland,Mitte,Nordost,Nordwest,Ost,Suedost,Suedwest,West";

	%rmapList = (
	Deutschland	=> "",
	Mitte		=> "central/",
	Nordost		=> "northeast/",
	Nordwest	=> "northwest/",
	Ost			=> "east/",
	Suedost		=> "southeast/",
	Suedwest	=> "southwest/",
	West		=> "west/");

	$fmapList =	"Deutschland_heute_frueh,Deutschland_heute_mittag,Deutschland_heute_spaet,Deutschland_heute_nacht,".
				"Deutschland_morgen_frueh,Deutschland_morgen_spaet,".
				"Deutschland_ueberm_frueh,Deutschland_ueberm_spaet,".
				"Deutschland_tag4_frueh,Deutschland_tag4_spaet,".
				"Mitte_heute_frueh,Mitte_heute_mittag,Mitte_heute_spaet,Mitte_heute_nacht,".
				"Mitte_morgen_frueh,Mitte_morgen_spaet,".
				"Mitte_ueberm_frueh,Mitte_ueberm_spaet,".
				"Mitte_tag4_frueh,Mitte_tag4_spaet,".
				"Nordost_heute_frueh,Nordost_heute_mittag,Nordost_heute_spaet,Nordost_heute_nacht,".
				"Nordost_morgen_frueh,Nordost_morgen_spaet,".
				"Nordost_ueberm_frueh,Nordost_ueberm_spaet,".
				"Nordost_tag4_frueh,Nordost_tag4_spaet,".
				"Nordwest_heute_frueh,Nordwest_heute_mittag,Nordwest_heute_spaet,Nordwest_heute_nacht,".
				"Nordwest_morgen_frueh,Nordwest_morgen_spaet,".
				"Nordwest_ueberm_frueh,Nordwest_ueberm_spaet,".
				"Nordwest_tag4_frueh,Nordwest_tag4_spaet,".
				"Ost_heute_frueh,Ost_heute_mittag,Ost_heute_spaet,Ost_heute_nacht,".
				"Ost_morgen_frueh,Ost_morgen_spaet,".
				"Ost_ueberm_frueh,Ost_ueberm_spaet,".
				"Ost_tag4_frueh,Ost_tag4_spaet,".
				"Suedost_heute_frueh,Suedost_heute_mittag,Suedost_heute_spaet,Suedost_heute_nacht,".
				"Suedost_morgen_frueh,Suedost_morgen_spaet,".
				"Suedost_ueberm_frueh,Suedost_ueberm_spaet,".
				"Suedost_tag4_frueh,Suedost_tag4_spaet,".
				"Suedwest_heute_frueh,Suedwest_heute_mittag,Suedwest_heute_spaet,Suedwest_heute_nacht,".
				"Suedwest_morgen_frueh,Suedwest_morgen_spaet,".
				"Suedwest_ueberm_frueh,Suedwest_ueberm_spaet,".
				"Suedwest_tag4_frueh,Suedwest_tag4_spaet,".
				"West_heute_frueh,West_heute_mittag,West_heute_spaet,West_heute_nacht,".
				"West_morgen_frueh,West_morgen_spaet,".
				"West_ueberm_frueh,West_ueberm_spaet,".
				"West_tag4_frueh,West_tag4_spaet";

	$fcmapList =	"Deutschland_frueh,Deutschland_mittag,Deutschland_spaet,Deutschland_nacht,".
				"Deutschland_morgen_frueh,Deutschland_morgen_spaet,".
				"Deutschland_uebermorgen_frueh,Deutschland_uebermorgen_spaet,".
				"Deutschland_Tag4_frueh,Deutschland_Tag4_spaet,".
				"Mitte_frueh,Mitte_mittag,Mitte_spaet,Mitte_nacht,".
				"Mitte_morgen_frueh,Mitte_morgen_spaet,".
				"Mitte_uebermorgen_frueh,Mitte_uebermorgen_spaet,".
				"Mitte_Tag4_frueh,Mitte_Tag4_spaet,".
				"Nordost_frueh,Nordost_mittag,Nordost_spaet,Nordost_nacht,".
				"Nordost_morgen_frueh,Nordost_morgen_spaet,".
				"Nordost_uebermorgen_frueh,Nordost_uebermorgen_spaet,".
				"Nordost_Tag4_frueh,Nordost_Tag4_spaet,".
				"Nordwest_frueh,Nordwest_mittag,Nordwest_spaet,Nordwest_nacht,".
				"Nordwest_morgen_frueh,Nordwest_morgen_spaet,".
				"Nordwest_uebermorgen_frueh,Nordwest_uebermorgen_spaet,".
				"Nordwest_Tag4_frueh,Nordwest_Tag4_spaet,".
				"Ost_frueh,Ost_mittag,Ost_spaet,Ost_nacht,".
				"Ost_morgen_frueh,Ost_morgen_spaet,".
				"Ost_uebermorgen_frueh,Ost_uebermorgen_spaet,".
				"Ost_Tag4_frueh,Ost_Tag4_spaet,".
				"Suedost_frueh,Suedost_mittag,Suedost_spaet,Suedost_nacht,".
				"Suedost_morgen_frueh,Suedost_morgen_spaet,".
				"Suedost_uebermorgen_frueh,Suedost_uebermorgen_spaet,".
				"Suedost_Tag4_frueh,Suedost_Tag4_spaet,".
				"Suedwest_frueh,Suedwest_mittag,Suedwest_spaet,Suedwest_nacht,".
				"Suedwest_morgen_frueh,Suedwest_morgen_spaet,".
				"Suedwest_uebermorgen_frueh,Suedwest_uebermorgen_spaet,".
				"Suedwest_Tag4_frueh,Suedwest_Tag4_spaet,".
				"West_frueh,West_mittag,West_spaet,West_nacht,".
				"West_morgen_frueh,West_morgen_spaet,".
				"West_uebermorgen_frueh,West_uebermorgen_spaet,".
				"West_Tag4_frueh,West_Tag4_spaet";

#
# Bundesländer den entsprechenden Dienststellen zuordnen
#
	%bula2bulaShort = (
	"baden-württemberg"			=> "bw",
	"bayern"					=> "by",
	"berlin"					=> "be",
	"brandenburg"				=> "bb",
	"bremen"					=> "hb",
	"hamburg"					=> "hh",
	"hessen" 					=> "he",
	"mecklenburg-vorpommern"	=> "mv",
	"niedersachsen"				=> "ni",
	"nordrhein-westfalen"		=> "nw",
	"rheinland-pfalz"			=> "rp",
	"saarland"					=> "sl",
	"sachsen"					=> "sn",
	"sachsen-anhalt"			=> "st",
	"schleswig-holstein"		=> "sh",
	"thüringen"					=> "th",
	"deutschland"				=> "xde",
	"bodensee"					=> "xbo" );

	%bulaShort2dwd = (
	bw => "DWSG",
	by => "DWMG",
	be => "DWPG",
	bb => "DWPG",
	hb => "DWHG",
	hh => "DWHH",
	he => "DWOH",
	mv => "DWPH",
	ni => "DWHG",
	nw => "DWEH",
	rp => "DWOI",
	sl => "DWOI",
	sn => "DWLG",
	st => "DWLH",
	sh => "DWHH",
	th => "DWLI",
	xde => "xde",
	xbo => "xbo" );

#
# Dienststellen den entsprechenden Serververzeichnissen zuordnen
#
	%dwd2Dir = (
	DWSG => "SU", # Stuttgart
	DWMG => "MS", # München
	DWPG => "PD", # Potsdam
	DWHG => "HA", # Hamburg
	DWHH => "HA", # Hamburg
	DWOH => "OF", # Offenbach
	DWPH => "PD", # Potsdam
	DWHG => "HA", # Hamburg
	DWEH => "EM", # Essen
	DWOI => "OF", # Offenbach
	DWLG => "LZ", # Leipzig
	DWLH => "LZ", # Leipzig
	DWLI => "LZ", # Leipzig
	DWHC => "HA", # Hamburg
	DWHB => "HA", # Hamburg
	DWPD => "PD", # Potsdam
	DWRW => "PD", # Potsdam
	DWEM => "EM", # Essen
	LSAX => "LZ", # Leipzig
	LSNX => "LZ", # Leipzig
	THLX => "LZ", # Leipzig
	DWOF => "OF", # Offenbach
	DWTR => "OF", # Offenbach
	DWSU => "SU", # Stuttgart
	DWMS => "MS", # München
	xde  => "D",
	xbo  => "Bodensee");
#	???? => "FG" # Freiburg);

	%dwd2Name = (
	EM => "Essen",
	FG => "Freiburg",
	HA => "Hamburg",
	LZ => "Leipzig",
	MS => "München",
	OF => "Offenbach",
	PD => "Potsdam",
	SU => "Stuttgart");

  
# German weekdays  
    @weekdays = ("So", "Mo", "Di", "Mi", "Do", "Fr", "Sa");  
 
    return;
}

sub GDS_Define($$$) {
	my ($hash, $def) = @_;
	my $name = $hash->{NAME};
	my @a = split("[ \t][ \t]*", $def);

	my $skuser = getKeyValue($name."_user");
	my $skpass = getKeyValue($name."_pass");
	my $skhost = getKeyValue($name."_host");

	unless(defined($skuser) && defined($skpass)) {
		return "syntax: define <name> GDS <username> <password> [<host>]" if(int(@a) < 4 ); 
	}

	$hash->{helper}{URL}		= defined($a[4]) ? $a[4] : "ftp-outgoing2.dwd.de";
	$hash->{helper}{INTERVAL}   = 1200;

	setKeyValue($name."_user",$a[2]) unless(defined($skuser));
	setKeyValue($name."_pass",$a[3]) unless(defined($skpass));
	setKeyValue($name."_host",$hash->{helper}{URL}) unless(defined($skhost));

	$hash->{DEF} = undef;

	Log3($name, 4, "GDS $name: created");
	Log3($name, 4, "GDS $name: tempDir=".$tempDir);

    _GDS_addExtension("GDS_CGI","gds","GDS Files");
#	$hash->{firstrun} = 1;
#	retrieveData($hash,'conditions') unless (time > 1457996400);
#	delete $hash->{firstrun};
	readingsSingleUpdate($hash, 'state', 'active',1);

	return undef;
}

sub GDS_Undef($$) {
	my ($hash, $arg) = @_;
	my $name = $hash->{NAME};
	RemoveInternalTimer($hash);
    my $url = '/gds';
    delete $data{FWEXT}{$url} if int(devspec2array('TYPE=GDS')) == 1;
	return undef;
}

sub GDS_Delete() {
	my ($hash, $arg) = @_;
	my $name = $hash->{NAME};
	setKeyValue($name."_user",undef);
	setKeyValue($name."_pass",undef);
	setKeyValue($name."_host",undef);
}

sub GDS_Rename() {
	my ($new,$old) = @_;
	setKeyValue($new."_user",getKeyValue($old."_user"));
	setKeyValue($new."_pass",getKeyValue($old."_pass"));
	setKeyValue($new."_host",getKeyValue($old."_host"));

	setKeyValue($old."_user",undef);
	setKeyValue($old."_pass",undef);
	setKeyValue($old."_host",undef);
	return undef;
}

sub GDS_Shutdown($) {
	my ($hash) = @_;
	my $name = $hash->{NAME};
	RemoveInternalTimer($hash);
	Log3 ($name,4,"GDS $name: shutdown requested");
	return undef;
}

sub GDS_Notify ($$) {
	my ($hash,$dev) = @_;
	my $name = $hash->{NAME};
	return if($dev->{NAME} ne "global");
	my $type = $dev->{CHANGED}[0];
	return unless (grep(m/^INITIALIZED/, $type));

	$aList		= "disabled_by_attribute" unless AttrVal($name,'gdsUseAlerts',0);
	$fList		= "disabled_by_attribute" unless AttrVal($name,'gdsUseForecasts',0);
	$fcmapList	= "disabled_by_attribute" unless AttrVal($name,'gdsUseForecasts',0);

	GDS_GetUpdate($hash);
	
	return undef;
}

sub GDS_Set($@) {
	my ($hash, @a) = @_;
	my $name = $hash->{NAME};
	my $usage =	"Unknown argument, choose one of ".
	            "clear:alerts,conditions,forecasts,all ".
				"forecasts:$fList ".
	            "help:noArg ".
	            "update:noArg ";				;

	my $command		= lc($a[1]);
	my $parameter	= $a[2] if(defined($a[2]));

	my ($result, $next);

	return $usage if $command eq '?';

	if(IsDisabled($name)) {
		readingsSingleUpdate($hash, 'state', 'disabled', 0);
		return "GDS $name is disabled. Aborting..." if IsDisabled($name);
	}

	readingsSingleUpdate($hash, 'state', 'active', 0);

	given($command) {
		when("clear"){
			CommandDeleteReading(undef, "$name a_.*")
			       if(defined($parameter) && ($parameter eq "all" || $parameter eq "alerts"));
			CommandDeleteReading(undef, "$name c_.*")    
			       if(defined($parameter) && ($parameter eq "all" || $parameter eq "conditions"));
			CommandDeleteReading(undef, "$name g_.*")
			       if(defined($parameter) && ($parameter eq "all" || $parameter eq "conditions"));
			CommandDeleteReading(undef, "$name fc.?_.*") 
			       if(defined($parameter) && ($parameter eq "all" || $parameter eq "forecasts"));
			}

		when("help"){
			$result = setHelp();
			break;
			}

		when("update"){
			RemoveInternalTimer($hash);
			GDS_GetUpdate($hash,'set update');
			break;
			}

		when("forecasts"){
			return "Error: Forecasts disabled by attribute." unless AttrVal($name,'gdsUseForecasts',0);
			CommandDeleteReading(undef, "$name fc.*") if($parameter ne AttrVal($name,'gdsSetForecast',''));
            $attr{$name}{gdsSetForecast} = $parameter;
			GDS_GetUpdate($hash,'set forecasts');
			break;
			}

		default { return $usage; };
	}
	return $result;
}

sub GDS_Get($@) {
	my ($hash, @a) = @_;
	my $command		= lc($a[1]);
	my $parameter	= $a[2] if(defined($a[2]));
	my $name		= $hash->{NAME};

	my $usage = "Unknown argument $command, choose one of help:noArg rereadcfg:noArg ".
				"list:capstations ".
				"alerts:".$aList." ".
				"conditionsmap:".$cmapList." ".
				"forecasts:".$fcList." ".
				"forecastsmap:".$fmapList." ".
				"headlines ".
				"radarmap:".$cmapList." ".
				"warningsmap:"."Deutschland,Bodensee,".$bulaList." ".
				"warnings:".$bulaList;

	return $usage if $command eq '?';

	if(IsDisabled($name)) {
		readingsSingleUpdate($hash, 'state', 'disabled', 0);
		return "GDS $name is disabled. Aborting..." if IsDisabled($name);
	}

	readingsSingleUpdate($hash, 'state', 'active', 0);
	my $_gdsAll		= AttrVal($name,"gdsAll", 0);
	my $gdsDebug	= AttrVal($name,"gdsDebug", 0);

	my ($result, @datensatz, $found);

	given($command) {

		when("conditionsmap"){
			# retrieve map: current conditions
			$hash->{file}{dir}		= "gds/specials/observations/maps/germany/";
			$hash->{file}{dwd}		= $parameter."*";
			$hash->{file}{target}	= $tempDir.$name."_conditionsmap.jpg";
			retrieveData($hash,'FILE');
			break;
		}

		when("forecastsmap"){
			# retrieve map: forecasts
			$hash->{file}{dir}		= "gds/specials/forecasts/maps/germany/";
			$hash->{file}{dwd}		= $parameter."*";
			$hash->{file}{target}	= $tempDir.$name."_forecastsmap.jpg";
			retrieveData($hash,'FILE');
			break;
		}

		when("headlines"){
			return "Error: Alerts disabled by attribute." unless AttrVal($name,'gdsUseAlerts',0);
			$parameter //= "|";
			return gdsAlertsHeadlines($name,$parameter);
		}

		when("warningsmap"){
			# retrieve map: warnings
			if(length($parameter) != 2){
				$parameter = $bula2bulaShort{lc($parameter)};
			}
			$hash->{file}{dwd}		= "Schilder".$dwd2Dir{$bulaShort2dwd{lc($parameter)}}.".jpg";
			$hash->{file}{dir}		= "gds/specials/alerts/maps/";
			$hash->{file}{target}	= $tempDir.$name."_warningsmap.jpg";
			retrieveData($hash,'FILE');
			break;
		}

		when("radarmap"){
			# retrieve map: radar
			$parameter = ucfirst($parameter);
			$hash->{file}{dir}		= "gds/specials/radar/".$rmapList{$parameter};
			$hash->{file}{dwd}		= "Webradar_".$parameter."*";
			$hash->{file}{target}	= $tempDir.$name."_radarmap.jpg";
			retrieveData($hash,'FILE');
			break;
			}

		when("help"){
			$result = getHelp();
			break;
			}

		when("list"){
			given($parameter){
				when("capstations")	{ 
					return "Error: Alerts disabled by attribute." unless AttrVal($name,'gdsUseAlerts',0);
					$result = getListCapStations($hash,$parameter); }
				default				{ $usage  = "get <name> list <parameter>"; return $usage; }
			}
			break;
			}

		when("alerts"){
			return "Error: Alerts disabled by attribute." unless AttrVal($name,'gdsUseAlerts',0);

			if($parameter =~ y/0-9// == length($parameter)){
				while ( my( $key, $val ) = each %capCellHash ) {
					push @datensatz,$val if $key =~ m/^$parameter/;
				}
#				push @datensatz,$capCellHash{$parameter};
			} else {
				push @datensatz,$capCityHash{$parameter};
			}
			CommandDeleteReading(undef, "$name a_.*");
			if($datensatz[0]){
				my $anum = 0;
				foreach(@datensatz) {
					decodeCAPData($hash,$_,$anum);
					$anum++;
				};
				readingsSingleUpdate($hash,'a_count',$anum,1);
			} else {
				$result = "Keine Warnmeldung für die gesuchte Region vorhanden.";
			}
            my $_gdsAll		= AttrVal($name,"gdsAll", 0);
            my $gdsDebug	= AttrVal($name,"gdsDebug", 0);
			break;
			}

		when("rereadcfg"){
			DoTrigger($name, "REREAD", 1);
			$hash->{GDS_REREAD}  = int(time());
			if (AttrVal($name,'gdsUseAlerts',0)) {
				%capCityHash = ();
				%capCellHash = ();
				retrieveData($hash,'capdata');
				retrieveListCapStations($hash);
			}
			retrieveData($hash,'forecast')   if AttrVal($name,'gdsUseForecasts',0);
#			GDS_GetUpdate($hash);
			break;
			}

		when("warnings"){
			my $vhdl;
			$result =	"     VHDL30 = current          |     VHDL31 = weekend or holiday\n".
						"     VHDL32 = preliminary      |     VHDL33 = cancel VHDL32\n".
						sepLine(31)."+".sepLine(38)."\n";

			if(length($parameter) != 2){
				$parameter = $bula2bulaShort{lc($parameter)};
			}
			my $dwd = $bulaShort2dwd{lc($parameter)};
			my $dir = "gds/specials/warnings/".$dwd2Dir{$dwd}."/";
			$hash->{file}{dir}		= $dir;

			for ($vhdl=30; $vhdl <=33; $vhdl++){
				my $dwd2	= "VHDL".$vhdl."_".$dwd."*";
				my $target	= $tempDir.$name."_warnings_$vhdl";
				unlink $target;
				$hash->{file}{dwd}		= $dwd2;
				$hash->{file}{target}	= $target;
				retrieveData($hash,'FILE');
			}

			sleep 2;
			for ($vhdl=30; $vhdl <=33; $vhdl++){
				my $target	= $tempDir.$name."_warnings_$vhdl";
				$result .= retrieveText($hash, "warnings_$vhdl", "") if (-e $target);
				$result .= "\n".sepLine(70);
			}

			$result .= "\n\n";
			break;
			}

		when("forecasts"){
			return "Error: Forecasts disabled by attribute." unless AttrVal($name,'gdsUseForecasts',0);
			$parameter = "Daten_$parameter";
			my ($k,$v,$data);
			$result = sepLine(67)."\n";

			# retrieve from hash
			$data = undef;
			while(($k, $v) = each %allForecastData){
				if ($k eq $parameter) {
					$data = $v;
					last;
				};
			}
			$data //= "No forecast data found.";
			$data =~ s/\$/\n/g;
			$result .= $data;
			$result .= "\n".sepLine(67)."\n";
			break;
			}

		default { return $usage; };
	}
	return $result;
}

sub GDS_Attr(@){
	my @a = @_;
	my $hash = $defs{$a[1]};
	my ($cmd, $name, $attrName, $attrValue) = @a;
	$attrValue //= '';
	my $useUpdate = 0;

	given($attrName){
 		when("gdsUseAlerts"){
			if ($attrValue == 0 || $cmd eq 'del') {
		    	$aList = "disabled_by_attribute";
			} else {
				$aList = "data_retrieval_running";
				retrieveData($hash,'capdata');
				retrieveListCapStations($hash);
			}
		}
 		when("gdsUseForecasts"){
			if ($attrValue == 0 || $cmd eq 'del') {
		    	$fList = "disabled_by_attribute";
		    	$fcList = "disabled_by_attribute";
			} else {
				$fcList = $fcmapList;
				$fList = "data_retrieval_running";
				$attr{$name}{$attrName} = $attrValue;
				retrieveData($hash,'forecast');
				$useUpdate = 1;
			}
		}
 		when("gdsHideFiles"){
 		    my $hR = AttrVal($FW_wname,'hiddenroom','');
 		    $hR =~ s/\,GDS.Files//g;
			if($attrValue) {
	 		    $hR .= "," if(length($hR));
 			    $hR .= "GDS Files";
			}
			CommandAttr(undef,"$FW_wname hiddenroom $hR");
 			break;
		}
		default {$attr{$name}{$attrName} = $attrValue;}
	}
	if(IsDisabled($name)) {
		readingsSingleUpdate($hash, 'state', 'disabled', 0);
	} else {
		readingsSingleUpdate($hash, 'state', 'active', 0);
		if ($useUpdate) {
			RemoveInternalTimer($hash);
			my $next = gettimeofday()+$hash->{helper}{INTERVAL};
			InternalTimer($next, "GDS_GetUpdate", $hash, 0);
		}
	}
	return;
}

sub GDS_GetUpdate($;$) {
	my ($hash,$local) = @_;
	$local //= 0;
	my $name = $hash->{NAME};

	RemoveInternalTimer($hash);

	my $fs = AttrVal($name, "gdsSetForecast", 0);

	if(IsDisabled($name)) {
    	readingsSingleUpdate($hash, 'state', 'disabled', 0);
		Log3 ($name, 2, "GDS $name is disabled, data update cancelled.");
	} else {
 		readingsSingleUpdate($hash, 'state', 'active', 0);
		if($fs) {
			retrieveData($hash,'forecast') ;
			my @a;
			push @a, undef;
			push @a, undef;
			push @a, $fs;
			retrieveForecasts($hash, "fc", @a);    
		}
	}

	# schedule next update
	my $next = gettimeofday()+$hash->{helper}{INTERVAL};
	my $gdsAll		= AttrVal($name,"gdsAll", 0);
	my $gdsDebug	= AttrVal($name,"gdsDebug", 0);
	InternalTimer($next, "GDS_GetUpdate", $hash, 1);
	readingsSingleUpdate($hash, "_nextUpdate", localtime($next), 1) if($gdsAll || $gdsDebug);

	return 1;
}

###################################################################################################
#
#	FWEXT implementation

sub _GDS_addExtension($$$) {
    my ($func,$link,$friendlyname)= @_;
  
    my $url = "/" . $link;
    Log3(undef,4,"Register gds webservice in FWEXT");
    $data{FWEXT}{$url}{FUNC} = $func;
    $data{FWEXT}{$url}{LINK} = "+$link";
    $data{FWEXT}{$url}{NAME} = $friendlyname;
    $data{FWEXT}{$url}{FORKABLE} = 0;
}
sub GDS_CGI {
  my ($request) = @_;
  my ($name,$ext)= _GDS_splitRequest($request);
  if(defined($name)) {
     my $filename= "$tempDir/$name.$ext";
     my $MIMEtype= filename2MIMEType($filename);
     my @contents;
     if(open(INPUTFILE, $filename)) {
       binmode(INPUTFILE);
       @contents= <INPUTFILE>;
       close(INPUTFILE);
       return("$MIMEtype; charset=utf-8", join("", @contents));
     } else {
       return("text/plain; charset=utf-8", "File not found: $filename");
     }
  } else {
    return _GDS_Overview();
  }
}
sub _GDS_splitRequest($) {
  my ($request) = @_;

  if($request =~ /^.*\/gds$/) {
    # http://localhost:8083/fhem/gds2
    return (undef,undef); # name, ext
  } else {
    my $call= $request;
    $call =~ s/^.*\/gds\/([^\/]*)$/$1/;
    my $name= $call;
    $name =~ s/^(.*)\.(jpg)$/$1/;
    my $ext= $call;
    $ext =~ s/^$name\.(.*)$/$1/;
    return ($name,$ext);
  }
}
sub _GDS_Overview {
  my ($name, $url);
  my $html= __GDS_HTMLHead("GDS Overview") . "<body>\n\n";
  foreach my $def (sort keys %defs) {
     if($defs{$def}{TYPE} eq "GDS") {
        $name  = $defs{$def}{NAME};
        $url   = __GDS_getURL();
        $html .= "$name<br>\n<ul>\n";
        $html .= "<a href=\"$url/gds/$name\_conditionsmap.jpg\" target=\"_blank\">Aktuelle Wetterkarte: Wetterlage</a><br/>\n";
        $html .= "<a href=\"$url/gds/$name\_forecastsmap.jpg\" target=\"_blank\">Aktuelle Wetterkarte: Vorhersage</a><br/>\n";
        $html .= "<a href=\"$url/gds/$name\_warningsmap.jpg\" target=\"_blank\">Aktuelle Wetterkarte: Warnungen</a><br/>\n";
        $html .= "<a href=\"$url/gds/$name\_radarmap.jpg\" target=\"_blank\">Aktuelle Wetterkarte: Radarkarte</a><br/>\n";
        $html .= "</ul>\n\n";
    }
  }
  $html.="</body>\n" . __GDS_HTMLTail();
  return ("text/html; charset=utf-8", $html);
}
sub __GDS_HTMLHead($) {
  my ($title) = @_;
  my $doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" '.
                '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
  my $xmlns   = 'xmlns="http://www.w3.org/1999/xhtml"';
  my $code    = "$doctype\n<html $xmlns>\n<head>\n<title>$title</title>\n</head>\n";
  return $code;
}
sub __GDS_getURL {
  my $proto = (AttrVal($FW_wname, 'HTTPS', 0) == 1) ? 'https' : 'http';
  return $proto."://$FW_httpheader{Host}$FW_ME"; #".$FW_ME;
}
sub __GDS_HTMLTail {
  return "</html>";
}

###################################################################################################
#
#	Tools

sub gdsAlertsHeadlines($;$) {
  my ($d,$sep) = @_;
  my $text = "";
  $sep = (defined($sep)) ? $sep : '|';
  my $count = ReadingsVal($d,'a_count',0);
  for (my $i = 0; $i < $count; $i++) {
    $text .= $sep if $i;
    $text .= ReadingsVal('gds','a_'.$i.'_headline','')
  }
  return $text;
}

sub gdsHeadlines($;$) {
  my $text = "GDS error: gdsHeadlines() is deprecated. Please use gdsAlertsHeadlines()";
  Log 1, $text;
  return $text; 
}

sub setHelp(){
	return	"Use one of the following commands:\n".
			sepLine(35)."\n".
			"set <name> clear alerts|all\n".
			"set <name> forecasts <regionName>/<stationName>\n".
			"set <name> help\n".
			"set <name> rereadcfg\n".
			"set <name> update\n";
}

sub getHelp(){
	return	"Use one of the following commands:\n".
			sepLine(35)."\n".
			"get <name> alerts <region>\n".
			"get <name> forecasts <regionName>\n".
			"get <name> help\n".
			"get <name> list capstations|data\n".
			"get <name> rereadcfg\n".
			"get <name> warnings <region>\n";
}

sub sepLine($;$) {
	my ($len,$c) = @_;
    $c //= '-';
	my ($output, $i);
	for ($i=0; $i<$len; $i++) { $output .= $c; }
	return $output;
}

sub _readDir($) {
   my ($destinationDirectory) = @_;
   eval { opendir(DIR,$destinationDirectory) or warn "$!"; };
   if ($@) {
      Log3(undef,1,'GDS: file system error '.$@);
      return ("");
   }
   my @files = readdir(DIR); 
   close(DIR); 
   return @files;
}

sub retrieveText($$$) {
	my ($hash, $fileName, $separator) = @_;
	my $name = $hash->{NAME};
	my ($err,@a);
	$fileName = $tempDir.$name."_$fileName";
	($err,@a) = FileRead({FileName=>$fileName,ForceType=>"file" });
	return "GDS error reading $fileName" if($err);
	@a = map (latin1ToUtf8($_), @a);
	return join($separator, @a);
}

sub gds_calctz($@) {
	my ($nt,@lt) = @_;
	my $off = $lt[2]*3600+$lt[1]*60+$lt[0];
	$off = 12*3600-$off;
	$nt += $off;  # This is noon, localtime
	my @gt = gmtime($nt);
	return (12-$gt[2]);
}

###################################################################################################
#
#	Data retrieval (internal)

sub getListCapStations($$){
	my ($hash, $command) = @_;
	my $name = $hash->{NAME};
	my (%capHash, $file, @columns, $key, $cList, $count);

	$file = $tempDir.'capstations.csv';
	return "GDS error: $file not found." unless(-e $file);

	# CSV öffnen und parsen
	my ($err,@a) = FileRead({FileName=>$file,ForceType=>"file" });
	return "GDS error reading $file" if($err);
	foreach my $l (@a) {
		next if (substr($l,0,1) eq '#');
		@columns = split(";",$l);
		$capHash{latin1ToUtf8($columns[4])} = $columns[0];
	}

	# Ausgabe sortieren und zusammenstellen
	foreach $key (sort keys %capHash) {
		$cList .= $capHash{$key}."\t".$key."\n";
	}
	return $cList;
}
sub retrieveListCapStations($){
	my ($hash) = @_;
	$hash->{file}{dir}		= "gds/help/";
	$hash->{file}{dwd}		= "legend_warnings_CAP_WarnCellsID.csv";
	$hash->{file}{target}	= $tempDir."capstations.csv";
	unless(-e $hash->{file}{target}) {
		retrieveData($hash,'FILE');
	} else {
		# read capstationslist once a day
		my $alter = time() - (stat($hash->{file}{target}))[9];
		retrieveData($hash,'FILE') if ($alter > 86400);
	}
}

sub decodeCAPData($$$){
	my ($hash, $datensatz, $anum) = @_;
	my $name		= $hash->{NAME};
	my $info		= 9999; # to be deleted
	my $alert		= int($datensatz/100);
	my $area		= $datensatz-$alert*100;

	my (%readings, @dummy, $i, $k, $n, $v, $t);

	my $gdsAll		= AttrVal($name,"gdsAll", 0);
	my $gdsDebug	= AttrVal($name,"gdsDebug", 0);
	my $gdsLong		= AttrVal($name,"gdsLong", 0);
	my $gdsPolygon	= AttrVal($name,"gdsPolygon", 0);

	Log3($name, 4, "GDS $name: Decoding CAP record #".$datensatz);

# topLevel informations
	if($gdsAll || $gdsDebug) {
		@dummy = split(/\./, $alertsXml->{alert}[$alert]{identifier});
		$readings{"a_".$anum."_identifier"}		= $alertsXml->{alert}[$alert]{identifier};
		$readings{"a_".$anum."_idPublisher"}	= $dummy[5];
		$readings{"a_".$anum."_idSysten"}		= $dummy[6];
		$readings{"a_".$anum."_idTimeStamp"}	= $dummy[7];
		$readings{"a_".$anum."_idIndex"}		= $dummy[8];
	}
	
	$readings{"a_".$anum."_sent"}			= $alertsXml->{alert}[$alert]{sent};
	$readings{"a_".$anum."_status"}			= $alertsXml->{alert}[$alert]{status};
	$readings{"a_".$anum."_msgType"}		= $alertsXml->{alert}[$alert]{msgType};

# infoSet informations
	if($gdsAll || $gdsDebug) {
		$readings{"a_".$anum."_language"}	= $alertsXml->{alert}[$alert]{info}{language};
		$readings{"a_".$anum."_urgency"}	= $alertsXml->{alert}[$alert]{info}{urgency};
		$readings{"a_".$anum."_severity"}	= $alertsXml->{alert}[$alert]{info}{severity};
		$readings{"a_".$anum."_certainty"}	= $alertsXml->{alert}[$alert]{info}{certainty};
	}
	
	$readings{"a_".$anum."_category"}		= $alertsXml->{alert}[$alert]{info}{category};
	$readings{"a_".$anum."_event"}			= $alertsXml->{alert}[$alert]{info}{event};
	$readings{"a_".$anum."_responseType"}	= $alertsXml->{alert}[$alert]{info}{responseType};

# eventCode informations
# loop through array
	$i = 0;
	while(1){
		($n, $v) = (undef, undef);
		$n = $alertsXml->{alert}[$alert]{info}{eventCode}[$i]{valueName};
		if(!$n) {last;}
		$n = "a_".$anum."_eventCode_".$n;
		$v = $alertsXml->{alert}[$alert]{info}{eventCode}[$i]{value};
		$readings{$n} .= $v." " if($v);
		$i++;
	}

# time/validity informations
	$readings{"a_".$anum."_effective"}		= $alertsXml->{alert}[$alert]{info}{effective} if($gdsAll);
	$readings{"a_".$anum."_onset"}			= $alertsXml->{alert}[$alert]{info}{onset};
	$readings{"a_".$anum."_expires"}		= $alertsXml->{alert}[$alert]{info}{expires};
	$readings{"a_".$anum."_valid"}			= _checkCAPValid($readings{"a_".$anum."_onset"},$readings{"a_".$anum."_expires"});
	$readings{"a_".$anum."_onset_local"}	= _capTrans($readings{"a_".$anum."_onset"});
	$readings{"a_".$anum."_expires_local"}	= _capTrans($readings{"a_".$anum."_expires"}) 
	         if(defined($alertsXml->{alert}[$alert]{info}{expires}));
	$readings{"a_".$anum."_sent_local"}		= _capTrans($readings{"a_".$anum."_sent"});

	$readings{a_valid} = ReadingsVal($name,'a_valid',0) || $readings{"a_".$anum."_valid"};

# text informations
	$readings{"a_".$anum."_headline"}		= $alertsXml->{alert}[$alert]{info}{headline};
	$readings{"a_".$anum."_description"}	= $alertsXml->{alert}[$alert]{info}{description} if($gdsAll || $gdsLong);
	$readings{"a_".$anum."_instruction"}	= $alertsXml->{alert}[$alert]{info}{instruction}
			if($readings{"a_".$anum."_responseType"} eq "Prepare" & ($gdsAll || $gdsLong));

# area informations
	$readings{"a_".$anum."_areaDesc"} 		=  $alertsXml->{alert}[$alert]{info}{area}[$area]{areaDesc};
	$readings{"a_".$anum."_areaPolygon"}	=  $alertsXml->{alert}[$alert]{info}{area}[$area]{polygon} if($gdsAll || $gdsPolygon);

# area geocode informations
# loop through array
	$i = 0;
	while(1){
		($n, $v) = (undef, undef);
		$n = $alertsXml->{alert}[$alert]{info}{area}[$area]{geocode}[$i]{valueName};
		if(!$n) {last;}
		$n = "a_".$anum."_geoCode_".$n;
		$v = $alertsXml->{alert}[$alert]{info}{area}[$area]{geocode}[$i]{value};
		$readings{$n} .= $v." " if($v);
		$i++;
	}

	$readings{"a_".$anum."_altitude"}		= $alertsXml->{alert}[$alert]{info}{area}[$area]{altitude}		if($gdsAll);
	$readings{"a_".$anum."_ceiling"}		= $alertsXml->{alert}[$alert]{info}{area}[$area]{ceiling}		if($gdsAll);

	readingsBeginUpdate($hash);
	readingsBulkUpdate($hash, "_dataSource", "Quelle: Deutscher Wetterdienst");
	while(($k, $v) = each %readings){
		# skip update if no valid data is available
        next unless(defined($v));
		readingsSingleUpdate($hash, $k, latin1ToUtf8($v),1); 
	}

	# convert color value to hex
	my $r = ReadingsVal($name, 'a_'.$anum.'_eventCode_AREA_COLOR', '');
	if(length($r)) {
		my $v = sprintf( "%02x%02x%02x", split(" ", $r));
		readingsSingleUpdate($hash, 'a_'.$anum.'_eventCode_AREA_COLOR_hex', $v,1);
	}
	
	readingsEndUpdate($hash, 1);

	return;
}
sub _checkCAPValid($$;$$){
	my ($onset,$expires,$t,$tmax) = @_;
	my $valid = 0;
  
	$t = time() if (!defined($t));
	my $offset = gds_calctz($t,localtime($t))*3600; 
	$t -= $offset;
	$tmax -= $offset if (defined($tmax));

	$onset =~ s/T/ /;
	$onset =~ s/\+/ \+/;
	$onset = time_str2num($onset);

	$expires =~ s/T/ /;
	$expires =~ s/\+/ \+/;
	$expires = time_str2num($expires);

	if (defined($tmax)) {  
		$valid = 1 if($tmax ge $onset && $t lt $expires);
	} else {
		$valid = 1 if($onset lt $t && $expires gt $t);
	}
	return $valid;
}
sub _capTrans($) {
	my ($t) = @_;
	my $valid = 0;
	my $offset = gds_calctz(time,localtime(time))*3600; # used from 99_SUNRISE_EL
	$t =~ s/T/ /;
	$t =~ s/\+/ \+/;
	$t = time_str2num($t);
	my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($t+$offset);
	$mon  += 1;
	$year += 1900;
	$t = sprintf "%02s.%02s.%02s %02s:%02s:%02s", $mday, $mon, $year, $hour, $min, $sec;
	return $t;
}

###################################################################################################
#
#	nonblocking data retrieval

sub retrieveData($$){
	my ($hash,$req) = @_;
	$req = uc($req);
	if ($req eq "FORECAST") {
		my $busyTag = "GDS_".$req."_BUSY";
		if (defined($hash->{$busyTag})) {
			return;
		} else {
			$hash->{$busyTag} = localtime(time());
		}
	}
	my $tag = "GDS_".$req."_READ";
	delete $hash->{$tag};
	$tag = "GDS_".$req."_ABORTED";
	delete $hash->{$tag};
	BlockingCall("_retrieve$req",$hash,"_finished$req",60,"_aborted$req",$hash);
}

#	any file
sub _retrieveFILE {
	my ($hash)		= shift;
	my $name		= $hash->{NAME};
	my $user		= getKeyValue($name."_user");
	my $pass		= getKeyValue($name."_pass");
	my $host		= getKeyValue($name."_host");
	my $proxyName	= AttrVal($name, "gdsProxyName", "");
	my $proxyType	= AttrVal($name, "gdsProxyType", "");
	my $passive		= AttrVal($name, "gdsPassiveFtp", 1);

	my $dir			= $hash->{file}{dir};
	my $dwd			= $hash->{file}{dwd};
	my $target		= $hash->{file}{target};

	my $ret = "";

	eval {
		my $ftp = Net::FTP->new(	$host,
									Debug        => 0,
									Timeout      => 10,
									Passive      => $passive,
									FirewallType => $proxyType,
									Firewall     => $proxyName);
		if(defined($ftp)){
			Log3($name, 4, "GDS $name: ftp connection established.");
			$ftp->login($user, $pass);
			$ftp->binary;
			$ftp->cwd($dir);
			my @files = $ftp->ls($dwd);
			if(@files) {
				@files = sort(@files);
				$dwd   = $files[-1];
 				Log3($name, 4, "GDS $name: file found.");
				Log3($name, 4, "GDS $name: retrieving $dwd");
				if(defined($target)) {
					$ftp->get($dwd,$target);
					my $s = -s $target;
        	        Log3($name, 4, "GDS: ftp transferred $s bytes");
				} else {
					my ($file_content,$file_handle);
					open($file_handle, '>', \$file_content);
					$ftp->get($dwd,$file_handle);
					$file_content = latin1ToUtf8($file_content);
					$file_content =~ s/\r\n/\$/g;
					$ret = $file_content;
				}
			}
			$ftp->quit;
		}
	};
	return "$name;;;$dwd;;;$ret";
}
sub _finishedFILE {
	my ($name,$file,$ret) = split(/;;;/,shift); #@_;
	my $hash = $defs{$name};
	DoTrigger($name,"REREADFILE $file",1);
}
sub _abortedFILE {
	my ($hash) = shift;
}

# 	CapData
sub _retrieveCAPDATA {
	my ($hash)		= shift;
	my $name		= $hash->{NAME};
	my $user		= getKeyValue($name."_user");
	my $pass		= getKeyValue($name."_pass");
	my $host		= getKeyValue($name."_host");
	my $proxyName	= AttrVal($name, "gdsProxyName", "");
	my $proxyType	= AttrVal($name, "gdsProxyType", "");
	my $passive		= AttrVal($name, "gdsPassiveFtp", 1);
	my $dir 		= "gds/specials/alerts/cap/GER/status/";
	my $dwd			= "Z_CAP*";

	my $datafile	= "";
	my $targetDir	= $tempDir.$name."_alerts.dir";
	my $targetFile	= $tempDir.$name."_alerts.zip";
	mkdir $targetDir unless -d $targetDir;

	# delete archive file
	unlink $targetFile;

	eval {
		my $ftp = Net::FTP->new(	$host,
									Debug        => 0,
									Timeout      => 10,
									Passive      => $passive,
									FirewallType => $proxyType,
									Firewall     => $proxyName);
		if(defined($ftp)){
			Log3($name, 4, "GDS $name: ftp connection established.");
			$ftp->login($user, $pass);
			$ftp->binary;
			$ftp->cwd("$dir");
			my @files = $ftp->ls($dwd);
			if(@files) {
 				Log3($name, 4, "GDS $name: filelist found.");
 				@files			= sort(@files);
				$datafile		= $files[-1];
				Log3($name, 5, "GDS $name: retrieving $datafile");
				$ftp->get($datafile,$targetFile);
				my $s = -s $targetFile;
                Log3($name, 5, "GDS: ftp transferred $s bytes");
			}
			$ftp->quit;
		}
	};

	
	# delete old files in directory
	if (-d $targetDir) {
		my @remove = _readDir($targetDir); 
		foreach my $f (@remove){
			next if -d $f;
			next if $targetFile =~ m/$f$/;
			Log3($name, 4, "GDS $name: deleting $targetDir/$f"); 
			unlink("$targetDir/$f"); 
		}
	}

	# unzip
	qx(unzip -o $targetFile -d $targetDir);

	# merge
    my ($countInfo,$cF)		= _mergeCapFile($hash);
	my ($aList,$cellData)	= _buildCAPList($hash,$countInfo,$cF);

	unlink $targetFile unless AttrVal($name,'gdsDebug',0); 

	return "$name;;;$datafile;;;$aList;;;$cF;;;$cellData";
}
sub _finishedCAPDATA {
	my ($name,$datafile,$aL,$capFile,$cellData) = split(/;;;/,shift);
	my $hash = $defs{$name};
	$aList = $aL;
	my @h = split(/;;/,$cellData);
	foreach(@h) {
		my ($n,$city,$cell)		= split(/:/,$_);
		$capCityHash{$city}		= $n;
		$capCellHash{"$cell$n"}	= $n;
	}

	my $xml		= new XML::Simple;
	eval {
	$alertsXml	= $xml->XMLin($capFile, KeyAttr => {}, ForceArray => [ 'alert', 'eventCode', 'area', 'geocode' ]);
	};
    if ($@) {
       Log3($name,1,'GDS: error analyzing alerts XML:'.$@);
       return;
    }
	readingsSingleUpdate($hash, "_dF_alerts",$datafile,0) if(AttrVal($name, "gdsDebug", 0));
	$hash->{GDS_CAPDATA_READ} = int(time());
	DoTrigger($name,"REREADALERTS",1);
}
sub _abortedCAPDATA {
	my ($hash) = shift;
	delete $hash->{GDS_CAPDATA_READ};
	$hash->{GDS_CAPDATA_ABORTED} = localtime(time());
}
sub _mergeCapFile($) {
    my ($hash) = @_;
    my $name = $hash->{NAME};
    
    my $destinationDirectory = $tempDir.$name."_alerts.dir";
    my @capFiles = _readDir($destinationDirectory);

    my @alertsArray;
    my $xmlHeader   = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>';
    push (@alertsArray,$xmlHeader);
    push (@alertsArray,"<gds>");
    my $countInfo   = 0;
	
    foreach my $cF (@capFiles){
       # merge all capFiles
       $cF = $destinationDirectory."/".$cF;
       next if -d $cF;
       next unless -s $cF;
       next unless $cF =~ m/\.xml$/; # read xml files only!
       Log3($name, 4, "GDS $name: analyzing $cF"); 

       my ($err,@a) = FileRead({FileName=>$cF,ForceType=>"file" });
       foreach my $l (@a) {
          next unless length($l);
          next if($l =~ m/^\<\?xml version.*/);
          $l = "<alert>"  if($l =~ m/^\<alert.*/);
#          next if($l =~ m/^\<alert.*/);
#          next if($l =~ m/^\<\/alert.*/);
          next if($l =~ m/^\<sender\>.*/);
          $countInfo++ if($l =~ m/^\<info\>/);
          push (@alertsArray,$l);
       }
    }
    push (@alertsArray,"</gds>");

    # write the big XML file if needed
    if(AttrVal($name,"gdsDebug", 0)) {
       my $cF = $destinationDirectory."/gds_alerts";
       unlink $cF if -e $cF;
       FileWrite({ FileName=>$cF,ForceType=>"file" },@alertsArray);
    }

    my $xmlContent = join('',@alertsArray);
    return ($countInfo,$xmlContent);
}
sub _buildCAPList($$$){
	my ($hash,$countInfo,$cF) = @_;
	my $name = $hash->{NAME};

	$alertsXml		= undef;

	my $xml			= new XML::Simple;
	my $area		= 0;
	my $record		= 0;
	my $n			= 0;
	my ($capCity, $capCell, $capEvent, $capEvt, @a);
    my $destinationDirectory = $tempDir.$name."_alerts.dir";
    
    # make XML array and analyze data
    eval	{	
	  $alertsXml = $xml->XMLin($cF, KeyAttr => {}, ForceArray => [ 'alert', 'eventCode', 'area', 'geocode' ]);
    };
    if ($@) {
       Log3($name,1,'GDS: error analyzing alerts XML:'.$@);
       return (undef,undef);
    }

    # analyze entries based on info and area array
    # array elements are determined by $info and $area
    #

	my $cellData = '';

    for (my $info=0; $info<=$countInfo;$info++) {
       $area = 0;
       while(1){
          $capCity  = $alertsXml->{alert}[$info]{info}{area}[$area]{areaDesc};
          $capEvent = $alertsXml->{alert}[$info]{info}{event};
          last unless $capCity;
          $capCell  = __findCAPWarnCellId($info, $area);
          $n        = 100*$info+$area;
          $capCity  = latin1ToUtf8($capCity.' '.$capEvent);
          push @a, $capCity;
          $capCity =~ s/\s/_/g;
          $cellData .= "$n:$capCity:$capCell$n;;";
          $area++;
          $record++;
          $capCity = undef;
       }
    }

	@a = sort(@a);
    $aList = undef;
	$aList = join(",", @a);
	$aList =~ s/\s/_/g;
	$aList = "No_alerts_published!" if !$record;

    return($aList,$cellData);
}
sub __findCAPWarnCellId($$){
	my ($info, $area) = @_;
	my $i = 0;
	while($i < 100){
		if($alertsXml->{alert}[$info]{info}{area}[$area]{geocode}[$i]{valueName} eq "WARNCELLID"){
			return $alertsXml->{alert}[$info]{info}{area}[$area]{geocode}[$i]{value};
			last;
		}
		$i++; # emergency exit :)
	}
}

#	ForecastData
sub _retrieveFORECAST {
	my ($hash)		= shift;
	my $name		= $hash->{NAME};
	my $user		= getKeyValue($name."_user");
	my $pass		= getKeyValue($name."_pass");
	my $host		= getKeyValue($name."_host");
	my $proxyName	= AttrVal($name, "gdsProxyName", "");
	my $proxyType	= AttrVal($name, "gdsProxyType", "");
	my $passive		= AttrVal($name, "gdsPassiveFtp", 1);
	my $useFritz	= AttrVal($name, "gdsUseFritzkotz", 0);
	my $dir         = "gds/specials/forecasts/tables/germany/";

    my $ret = "";

	eval {
		my $ftp = Net::FTP->new(	$host,
									Debug        => 0,
									Timeout      => 10,
									Passive      => $passive,
									FirewallType => $proxyType,
									Firewall     => $proxyName);
		if(defined($ftp)){
			Log3($name, 4, "GDS $name: ftp connection established.");
			$ftp->login($user, $pass);
			$ftp->binary;
			$ftp->cwd("$dir");
			my @files = $ftp->ls();
			if(@files) {
 				Log3($name, 4, "GDS $name: filelist found.");
 				@files = sort(@files);
				$fcmapList = undef;
 				map ( $fcmapList .= (split(/Daten_/,$_,2))[1].",", @files );
				my $count = 0;
				foreach my $file (@files) {
					my ($file_content,$file_handle);
					open($file_handle, '>', \$file_content);
					$ftp->get($file,$file_handle);
					next unless (length($file_content));
					$file_content = latin1ToUtf8($file_content);
					$file_content =~ s/\r\n/\$/g;
					$ret .= "$file:$file_content;";
					$count++;
					Log3 ($name, 5, "GDS $name retrieved forecast $file");
				}
			}
			$ftp->quit;
		}
	};

	return $name.";;;".$ret;
}
sub _finishedFORECAST {
	my ($name,$ret) = split(/;;;/,shift); #@_;
	my $hash = $defs{$name};
	my @a = split(/;/,$ret);
	%allForecastData = ();
	foreach my $l (@a) {
		my ($fn,$fc) = split(/\:/,$l);
		$allForecastData{$fn} = $fc;
	}
	$hash->{GDS_FORECAST_READ} = int(time());
	DoTrigger($name,"REREADFORECAST",1);
	getListForecastStations($hash);
	my $sf = AttrVal($name,'gdsSetForecast',0);
#	GDS_GetUpdate($hash,1) if $sf; 
	my @b;
	push @b, undef;
	push @b, undef;
	push @b, $sf;
	retrieveForecasts($hash, "fc", @b);    
	delete $hash->{GDS_FORECAST_BUSY};
}
sub _abortedFORECAST {
	my ($hash) = shift;
	delete $hash->{GDS_FORECAST_READ};
	$hash->{GDS_FORECAST_ABORTED} = localtime(time());
	delete $hash->{GDS_FORECAST_BUSY};
}

###################################################################################################
#
#	forecast retrieval - provided by jensb

sub retrieveForecasts($$@) {
	#
	# parameter: hash, prefix, region/station, forecast index (0 .. 10)
	#
	my ($hash, $prefix, @a) = @_;
	my $name		= $hash->{NAME};

	# extract region and station name
	return unless defined($a[2]);

	my ($area,$station) = split(/\//,$a[2]);
	return unless $station;

	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime();  
	my ($dataFile, $found, $line, %fread, $k, $v, $data); 
	$area			=  utf8ToLatin1($area);
	$station		=~ s/_/ /g; # replace underscore in station name by space
	my $searchLen	=  length($station);  
	%fread		= ();

	# define fetch scope (all forecasts or single forecast)
	my $fc = 0;
	my $fcStep = 1;
	if (defined($a[3]) && $a[3] > 0) {
	    # single forecast
	    $fc = $a[3] - 1;
	    $fcStep = 10;
	}

	# fetch up to 10 forecasts for today and the next 3 days
	do {
		my $day;
		my $early;
		if ($fc < 4) {
			$day = 0;
			$early = 0;
		} else {
			$day = int(($fc - 2)/2);
			$early = $fc%2 == 0;
		} 
		my $areaAndTime = $area;
		if ($day == 1) {
			$areaAndTime .= "_morgen";
		} elsif ($day == 2) {
			$areaAndTime .= "_uebermorgen";
		} elsif ($day == 3) {
			$areaAndTime .= "_Tag4";
		}
		my $timeLabel = undef;
		my $tempLabel = '_tAvgAir';
		my $copyDay = undef;
		my $copyTimeLabel = undef;
		if ($day == 0) {
			if ($fc == 0) {
				$areaAndTime .= "_frueh";  # .. 6 h
				$timeLabel = '06';
				$tempLabel ='_tMinAir';
				$copyDay = 1;
				$copyTimeLabel = '12';
			} elsif ($fc == 1) {
				$areaAndTime .= "_mittag"; # .. 12 h
				$timeLabel = '12';
				$tempLabel .= $timeLabel;
			} elsif ($fc == 2) {
				$areaAndTime .= "_spaet";  # .. 18 h
				$timeLabel = '18';
				$tempLabel ='_tMaxAir';
				$copyDay = 1;
				$copyTimeLabel = '24';
			} elsif ($fc == 3) {
				$areaAndTime .= "_nacht";  # .. 24 h
				$timeLabel = '24';
				$tempLabel .= $timeLabel;
			}
		} else {
			if ($early) {    
				$areaAndTime .= "_frueh";  # .. 12 h
				$timeLabel = '12';
				$tempLabel ='_tMinAir';
				if ($day < 3) {
					$copyDay = $day + 1;
					$copyTimeLabel = '12';
				}
			} else {
				$areaAndTime .= "_spaet";  # .. 24 h
				$timeLabel .= '24';
				$tempLabel ='_tMaxAir';
				if ($day < 3) {
					$copyDay = $day + 1;
					$copyTimeLabel = '24';
				}
			}
		} # if ($day == 0) {

		# define forecast date (based on "now" + day)   
		my $fcEpoch = time() + $day*86400;
		if ($fc == 3) {
			# night continues at next day
			$fcEpoch += 86400;
		}
		my ($fcSec,$fcMin,$fcHour,$fcMday,$fcMon,$fcYear,$fcWday,$fcYday,$fcIsdst) = localtime($fcEpoch);
		my $fcWeekday = $weekdays[$fcWday];
		my $fcDate = sprintf("%02d.%02d.%04d", $fcMday, 1+$fcMon, 1900+$fcYear);
		my $fcDateFound = 0;

		# retrieve from hash
		my $noDataFound = 1;
		$data = undef;
		my $hashSize = keys%allForecastData; # checking size of hash seems to make all entries "visible" in loop
		while(($k, $v) = each %allForecastData){
			if ($k eq "Daten_$areaAndTime") {
				$data = $v;
				last;
			};
		}

		if (defined($data) && $data) {
			my @data = split(/\$/,$data);

			foreach my $l (@data) {
				if (index($l, $fcDate) > 0) { 
					# forecast date found
					$fcDateFound = 1; 
				} # if
				if (index(substr(lc($l),0,$searchLen), substr(lc($station),0,$searchLen)) != -1) { 
					# station found
					$line = $l;
					last; 
				} # if
			} # foreach

			# parse file
			if ($fcDateFound && length($line) > 0) {
				if (index(substr(lc($line),0,$searchLen), substr(lc($station),0,$searchLen)) != -1) {
					# station found but there is no header line and column width varies:
					$line =~ s/---/   ---/g;	# column distance may drop to zero between station name 
												# and invalid temp "---" -> prepend 3 spaces
					$line =~ s/   /;/g;			# now min. column distance is 3 spaces -> convert to semicolon 
					$line =~ s/;+/;/g;			# replace multiple consecutive semicolons by one semicolon
					my @b = split(';', $line);	# split columns by semicolon
					$b[0] =~ s/^\s+|\s+$//g;	# trim station name
					$b[1] =~ s/^\s+|\s+$//g;	# trim temperature
					$b[2] =~ s/^\s+|\s+$//g;	# trim weather   
					if (scalar(@b) > 3) {
						$b[3] =~ s/^\s+|\s+$//g; # trim wind gust
					} else {
						$b[3] = ' ';
					}
					$fread{$prefix."_stationName"} = $area.'/'.$b[0];
					$fread{$prefix.$day.$tempLabel}  = $b[1];
					$fread{$prefix.$day."_weather".$timeLabel} = $b[2];
					$fread{$prefix.$day."_windGust".$timeLabel} = $b[3];
					if ($fc != 3) {
						$fread{$prefix.$day."_weekday"} = $fcWeekday;
					}
					$noDataFound = 0;
				} else {
					# station not found, abort
					$fread{$prefix."_stationName"} = "unknown: $station in $area";
					last;
				}
			}
		} # unless

		if ($noDataFound) {
			# forecast period already passed or no data available 			
			$fread{$prefix.$day.$tempLabel} = "---";
			$fread{$prefix.$day."_weather".$timeLabel} = "---";      
			$fread{$prefix.$day."_windGust".$timeLabel} = "---";      
			if ($fc != 3) {
				$fread{$prefix.$day."_weekday"} = $fcWeekday;
			}
		}

		# day change preset by rotation
		my $ltime = ReadingsTimestamp($name, $prefix.$day."_weather".$timeLabel, undef);
		my ($lsec,$lmin,$lhour,$lmday,$lmon,$lyear,$lwday,$lyday,$lisdst);
		if (defined($ltime)) {
			($lsec,$lmin,$lhour,$lmday,$lmon,$lyear,$lwday,$lyday,$lisdst) = localtime(time_str2num($ltime));
		}
		if (!defined($ltime) || $mday != $lmday) {
			# day has changed, rotate old forecast forward by one day because new forecast is not immediately available
			my $temp = $fread{$prefix.$day.$tempLabel};
			if (defined($temp) && substr($temp, 0, 2) eq '--') {
				if (defined($copyTimeLabel)) {
					$fread{$prefix.$day.$tempLabel} = ReadingsVal($name, $prefix.$copyDay.$tempLabel, '---');
				} else {
					# today noon/night and 3rd day is undefined
					$fread{$prefix.$day.$tempLabel} = ' ';
				}
			}
			my $weather = $fread{$prefix.$day."_weather".$timeLabel};
			if (defined($weather) && substr($weather, 0, 2) eq '--') {
				if (defined($copyTimeLabel)) {
					$fread{$prefix.$day."_weather".$timeLabel} = 
						ReadingsVal($name, $prefix.$copyDay."_weather".$copyTimeLabel, '---');
			} else {
				# today noon/night and 3rd day is undefined
				$fread{$prefix.$day."_weather".$timeLabel} = ' ';
			}
		}
		my $windGust = $fread{$prefix.$day."_windGust".$timeLabel};
		if (defined($windGust) && substr($windGust, 0, 2) eq '--') {
			if (defined($copyTimeLabel)) {
				$fread{$prefix.$day."_windGust".$timeLabel} = 
					ReadingsVal($name, $prefix.$copyDay."_windGust".$copyTimeLabel, '---');
			} else {
				# today noon/night and 3rd day is undefined
				$fread{$prefix.$day."_windGust".$timeLabel} = ' ';
			}
		}
	}
	$fc += $fcStep;
	} while ($fc < 10);

	readingsBeginUpdate($hash);
	while (($k, $v) = each %fread) {
		# skip update if no valid data is available
        unless(defined($v))      {delete($defs{$name}{READINGS}{$k}); next;}
		if($v =~ m/^--/)        {delete($defs{$name}{READINGS}{$k}); next;};
        unless(length(trim($v))) {delete($defs{$name}{READINGS}{$k}); next;};
		readingsBulkUpdate($hash, $k, $v); 
	}
	readingsEndUpdate($hash, 1);
}

sub getListForecastStations($) {
	my ($hash)  = @_;
	my $name    = $hash->{NAME};
	my @regions = keys(%rmapList);
	my (@a,$data,$k,$v);

	eval {
		foreach my $region (@regions) {
			$data = "";
			my $areaAndTime = 'Daten_'.$region.'_morgen_spaet';
			while(($k, $v) = each %allForecastData){
				if ($k eq $areaAndTime) {
					$data = $v;
					last;
				};
			}
			next unless $data;
			my @data = split(/\$/,$data);
			splice(@data, 0,2);
			splice(@data,-2);
			map ( push(@a,"$region/".(split(/(\s|--)/,$_,2))[0]), @data );
		}
	};
	   
	Log3($name, 4, "GDS $name: forecast data not found") unless (@a);

	@a = sort(@a);
  	$fList = join(",", @a);
	$fList =~ s/\s+,/,/g; # replace multiple spaces followed by comma with comma
	$fList =~ s/\s/_/g;   # replace spaces in stationName with underscore for list in frontend
	return;
}


1;


# development documentation
=pod
###################################################################################################
#
#   ToDo
#
###################################################################################################
#
#	Changelog
#
###################################################################################################
#
#	2016-06-05	changed		use os based unzip for decoding capdata
#
#	2016-03-29	changed		remove all conditions code
#
#	2016-01-27	changed		use setKeyValue/getKeyValue for username and password
#
#	2016-01-01	fixed		use txt file instead html for conditions (adopt DWD changes)
#
#	2015-12-31	fixed		conditions retrieval on startup
#
#	2015-11-26	fixed		wrong region handling
#				added		gdsAlertsHeadlines()
#
#	2015-11-17	changed		decodeCAPData - fix wrong cumulation (first try)
#				fixed		minor bugs
#
#	2015-11-06	changed		character encoding in forecast readings (jensb)
#				fixed		problems after global rereadcfg
#				fixed		delete CAP-zipfile unless gdsDebug set
#
#	2015-11-01	changed		getListForecastStations: fixed inverted logging "data not found"
#				changed		GDS_GetUpdate, retrieveData, _finishedFORECAST, _abortedFORECAST: 
#							prevent multiple parallel processing
#				changed		retrieveForecasts: make available data in hash "visible" for processing
#
#	2015-10-31	public		new version released, SVN #9739
#
#	2015-10-30	public		RC6 published, SVN #9727
#				changed		use passive ftp per default
#
#	2015-10-27	changed		add own function gds_calcTz due to announced
#							changes in SUNRISE_EL
#
#	2015-10-26	changed		multiple instances are forbidden
#
#	2015-10-25	public		RC5 published, SVN #9663
#				changed		a lot of code cleanup
#
#	2015-10-24	public		RC3 published, SVN #9627
#
#	2015-10-13	changed		getListForecastStations()	completed
#				changed		retrieveForecasts()			completed
#				added		DoTrigger() according to reread
#
#	2015-10-12  changed		conditions		completed
#				changed		capstationlist	completed
#				changed		conditionsmap	completed
#				changed		forecastsmap	completed
#				changed		radarmap		completed
#				changed		warningsmap		completed
#				changed		warnings		completed
#				changed		get alerts		completed
#
#   2015-10-11 	changed		use Archive::Extract for unzip
#				changed		code cleanup
# 				changed		forecast nonblocking retrieval:
#								hash generation completed
#				changed		capstations nonblocking retrieval:
#								alertslist dropdown completed
#								datafile retrieval completed
#
#
#   ----------  public    RC2 published, SVN #9429
#
#   2015-10-11  renamed   99_gdsUtils.pm to GDSweblink.pm
#               changed   load GDSweblink.pm in eval() on module startup
#
#   2015-10-10  added     attribute gdsHideFile to hide "GDS File" Menu
#               added     optional parameter "host" in define() to override default hostname
#
#               changed   weblink generator           moved into 99_gdsUtils.pm
#               changed   perl module List::MoreUtils is no longer used
#               changed   perl module Text::CSV is no longer needed
#               changed   use binary mode for all ftp transfers to preven errors in images
#
#               fixed     handling for alert items msgType, sent, status
#               fixed     handling for alert messages without "expires" data
#
#               updated   commandref documentation
#
#   ----------  public    RC1 published, SVN #9416
#
#   2015-10-09  removed   createIndexFile()
#               added     forecast retrieval
#               added     weblink generator
#               added     more "set clear ..." commands
#               changed   lots and lots of code cleanup
#               feature   make retrieveFile() nonblocking
#
#
#   2015-10-08  changed   added mergeCapFile()
#                         code cleanup in buildCAPList()
#                         use system call "unzip" instead of Archive::Zip
#               added     NotifyFn for rereadcfg after INITIALIZED
#               improved  startup data retrieval
#               improved  attribute handling
#
#   ----------  public    first publication in ./contrib/55_GDS.2015 for testing
#
#   2015-10-07  changed   remove LWP - we will only use ftp for transfers
#               added     first solution for filemerge
#               added     reliable counter for XML analyzes instead of while(1) loops 
#               added     (implementation started) forecast retrieval by jensb
#               changed   make text file retrieval more generic
#
#   2015-10-06  removed   Coro Support
#               removed   $useFTP - always use http internally 
#               changed   use LWP::Parallel::UserAgent for nonblocking transfers
#               changed   use  Archive::ZIP for alert files transfer and unzip
#
#   2015-10-05  started   redesign for new data structures provided by DWD
#
#   ----------
#
#   2015-09-24  fixed   prevent fhem crash on empty conditions file
#
#   2015-04-07  fixed   a_X_valid calculation: use onset, too
#
#   2015-01-30  changed use own FWEXT instead of HTTPSRV
#
#	2015-01-03	added	multiple alerts handling
#
#	2014-10-15	added	attr disable
#
#	2014-05-23	added	set <name> clear alerts|all
#						fixed some typos in docu and help
#
#	2014-05-22	added	reading a_sent_local
#
#	2014-05-07	added	readings a_onset_local & a_expires_local
#
#	2014-02-26	added	attribute gdsPassiveFtp
#
#	2014-02-04	added	ShutdownFn
#				changed	FTP Timeout
#
#	2013-11-03	added	error handling for malformed XML files from GDS
#
#	2013-08-13	fixed	some minor bugs to prevent annoying console messages
#				added	support for fhem installtions running on windows-based systems
#
#	2013-08-11	added	retrieval for condition maps
#				added	retrieval for forecast maps
#				added	retrieval for warning maps
#				added	retrieval for radar maps
#				modi	use LWP::ua for some file transfers instead of ftp
#						due to transfer errors on image files
#						use parameter #5 = 1 in RetrieveFile for ftp
#				added	get <name> caplist
#
#	2013-08-10	added	some more tolerance on text inputs
#				modi	switched from GetLogList to Log3
#
#	2013-08-09	added	more logging
#				fixed	missing error message if WARNCELLID does not exist
#				update	commandref
#
#	2013-08-08	added	logging
#				added	firewall/proxy support
#				fixed	XMLin missing parameter 
#				added	:noArg to setlist-definitions
#				added	AttrFn
#				modi	retrieval of VHDL messages 30-33
#
#	2013-08-07	public  initial release
#
###################################################################################################
#
# Further informations
#
# DWD's data format is unpleasant to read, 
# since the data columns change depending on the available data
# (e.g. the SSS column for snow disappears when there is no snow).
# It's also in ISO8859-1, i.e. it contains non-ASCII characters. To
# avoid problems, we need some conversion subs in this program.
#
# Höhe  : m über NN
# Luftd.: reduzierter Luftdruck auf Meereshöhe in hPa
# TT    : Lufttemperatur in Grad Celsius
# Tn12  : Minimum der Lufttemperatur, 18 UTC Vortag bis 06 UTC heute, Grad Celsius
# Tx12  : Maximum der Lufttemperatur, 18 UTC Vortag bis 06 UTC heute, Grad Celsius
# Tg24  : Temperaturminimum 5cm ¸ber Erdboden, 22.05.2014 00 UTC bis 24 UTC, Grad Celsius
# Tn24  : Minimum der Lufttemperatur, 22.05.2014 00 UTC bis 24 UTC, Grad Celsius
# Tm24  : Mittel der Lufttemperatur, 22.05.2014 00 UTC bis 24 UTC, Grad Celsius
# Tx24  : Maximum der Lufttemperatur, 22.05.2014 00 UTC bis 24 UTC, Grad Celsius
# Tmin  : Minimum der Lufttemperatur, 06 UTC Vortag bis 06 UTC heute, Grad Celsius
# Tmax  : Maximum der Lufttemperatur, 06 UTC Vortag bis 06 UTC heute, Grad Celsius
# RR1   : Niederschlagsmenge, einstündig, mm = l/qm
# RR12  : Niederschlagsmenge, 12st¸ndig, 18 UTC Vortag bis 06 UTC heute, mm = l/qm
# RR24  : Niederschlagsmenge, 24stündig, 06 UTC Vortag bis 06 UTC heute, mm = l/qm
# SSS   : Gesamtschneehöhe in cm
# SSS24 : Sonnenscheindauer 22.05.2014 in Stunden
# SGLB24: Tagessumme Globalstrahlung am 22.05.2014 in J/qcm 
# DD    : Windrichtung 
# FF    : Windgeschwindigkeit letztes 10-Minutenmittel in km/h
# FX    : höchste Windspitze im Bezugszeitraum in km/h
# ---   : Wert nicht vorhanden
#
###################################################################################################
=cut

# commandref documentation
=pod
=item device
=begin html

<a name="GDS"></a>
<h3>GDS</h3>
<ul>

	<b>Prerequesits</b>
	<ul>
	
		<br/>
		Module uses following additional Perl modules:<br/><br/>
		<code>Net::FTP, XML::Simple, Archive::Extract, Archive::Zip</code><br/><br/>
		If not already installed in your environment, 
		please install them using appropriate commands from your environment.

	</ul>
	<br/><br/>
	
	<a name="GDSdefine"></a>
	<b>Define</b>
	<ul>

		<br>
		<code>define &lt;name&gt; GDS &lt;username&gt; &lt;password&gt; [&lt;host&gt;]</code><br>
		<br>
		This module provides connection to <a href="http://www.dwd.de/grundversorgung">GDS service</a> 
		generated by <a href="http://www.dwd.de">DWD</a><br>
		<br/>
		Optional paramater host is used to overwrite default host "ftp-outgoing2.dwd.de".<br/>
		<br>
	</ul>
	<br/><br/>

	<a name="GDSset"></a>
	<b>Set-Commands</b><br/>
	<ul>

		<br/>
		<code>set &lt;name&gt; clear alerts|conditions|forecasts|all</code>
		<br/><br/>
		<ul>
			<li>alerts: Delete all a_* readings</li>
			<li>all: Delete all a_*, c_*, g_* and fc_* readings</li>
		</ul>
		<br/>

		<code>set &lt;name&gt; forecasts &lt;region&gt;/&lt;stationName&gt;</code>
		<br/><br/>
		<ul>Retrieve forecasts for today and the following 3 days for selected station.<br/>
		    Data will be updated periodically.</ul>
		<br/>

		<code>set &lt;name&gt; help</code>
		<br/><br/>
		<ul>Show a help text with available commands</ul>
		<br/>

		<code>set &lt;name&gt; update</code>
		<br/><br/>
		<ul>Update forecasts readings at selected station and restart update-timer</ul>
		<br/>

		<li>forecast readings generated by SET use prefix "fcd_" and a postfix of "hh"<br/> 
		   with d=relative day (0=today) and hh=last hour of forecast (exclusive)</li>
		<li>readings generated by SET will be updated automatically every 20 minutes</li>

	</ul>
	<br/><br/>

	<a name="GDSget"></a>
	<b>Get-Commands</b><br/>
	<ul>

		<br/>
		<code>get &lt;name&gt; alerts &lt;region&gt;</code>
		<br/><br/>
		<ul>Retrieve alert message for selected region from previously read alert file (see rereadcfg)</ul>
		<br/>

		<code>get &lt;name&gt; conditionsmap &lt;region&gt;</code>
		<br/><br/>
		<ul>Retrieve map (imagefile) showing current conditions at selected station</ul>
		<br/>

		<code>get &lt;name&gt; forecasts &lt;region&gt;</code>
		<br/><br/>
		<ul>Retrieve forecasts for today and the following 3 days for selected region as text</ul>
		<br/>

		<code>get &lt;name&gt; forecastsmap &lt;stationName&gt;</code>
		<br/><br/>
		<ul>Retrieve map (imagefile) showing forecasts for selected region</ul>
		<br/>

		<code>get &lt;name&gt; headlines [separator]</code>
		<br/><br/>
		<ul>Returns a string, containing all alert headlines. <br/>
		    Default separator is | but can be overriden.</ul>
		<br/>

		<code>get &lt;name&gt; help</code>
		<br/><br/>
		<ul>Show a help text with available commands</ul>
		<br/>

		<code>get &lt;name&gt; list capstations</code>
		<br/><br/>
		<ul>
			<li><b>capstations:</b> Retrieve list showing all defined warning regions. 
			    You can find your WARNCELLID with this list.</li>
		</ul>
		<br/>

		<code>get &lt;name&gt; radarmap &lt;region&gt;</code>
		<br/><br/>
		<ul>Retrieve map (imagefile) containig radar view from selected region</ul>
		<br/>

		<code>get &lt;name&gt; rereadcfg</code>
		<br/><br/>
		<ul>Reread all required data from DWD Server manually: station list and CAP data</ul>
		<br/>

		<code>get &lt;name&gt; warnings &lt;region&gt;</code>
		<br/><br/>
		<ul>Retrieve current warnings report for selected region
			<ul>
				<br/>
				<li>report type VHDL30 = regular report, issued daily</li>
				<li>report type VHDL31 = regular report, issued before weekend or national holiday</li>
				<li>report type VHDL32 = preliminary report, issued on special conditions</li>
				<li>report type VHDL33 = cancel report, issued if necessary to cancel VHDL32</li>
			</ul>
		</ul>
		<br/>

		<code>get &lt;name&gt; warningssmap &lt;region&gt;</code>
		<br/><br/>
		<ul>Retrieve map (imagefile) containig current warnings for selected region marked with symbols</ul>
		<br/><br/>
		<b>All downloaded mapfiles</b> can be found inside "GDS Files" area in left navigation bar.

	</ul>
	<br/><br/>

	<a name="GDSattr"></a>
	<b>Attributes</b><br/><br/>
	<ul>
		<li><a href="#do_not_notify">do_not_notify</a></li>
		<li><a href="#readingFnAttributes">readingFnAttributes</a></li>
		<br/>
		<li><b>disable</b> - if set, gds will not try to connect to internet</li>
		<li><b>gdsAll</b> - defines filter for "all data" from alert message</li>
		<li><b>gdsDebug</b> - defines filter for debug informations</li>
		<li><b>gdsSetForecast</b> - defines forecasts region/station to be used after system restart</li>
		<li><b>gdsLong</b> - show long text fields "description" and "instruction" from alert message in readings</li>
		<li><b>gdsPolygon</b> - show polygon data from alert message in a reading</li>
		<li><b>gdsHideFiles</b> - if set to 1, the "GDS Files" menu in the left navigation bar will not be shown</li>
		<br/>
		<li><b>gdsPassiveFtp</b> - set to 1 to use passive FTP transfer</li>
		<li><b>gdsFwName</b> - define firewall hostname in format &lt;hostname&gt;:&lt;port&gt;</li>
		<li><b>gdsFwType</b> - define firewall type in a value 0..7 please refer to
		    <a href="http://search.cpan.org/~gbarr/libnet-1.22/Net/Config.pm#NetConfig_VALUES">cpan documentation</a> 
		    for further informations regarding firewall settings.</li>
	</ul>
	<br/><br/>

	<b>Generated Readings:</b>
	<br/><br/>
	<ul>
		<li><b>_&lt;readingName&gt;</b> - debug informations</li>
		<li><b>a_X_&lt;readingName&gt;</b> - weather data from CAP alert messages. Readings will NOT be updated automatically<br/>
			a_ readings contain a set of alert inforamtions, X represents a numeric set identifier starting with 0<br/>
			that will be increased for every valid alert message in selected area<br/></li>
		<li><b>a_count</b> - number of currently valid alert messages, can be used for own loop iterations on alert messages</li>
		<li><b>a_valid</b> - returns 1 if at least one of decoded alert messages is valid</li>
		<li><b>fc?_&lt;readingName&gt;??</b> - weather data from SET weather forecasts, 
		    prefix by relative day and postfixed by last hour. Readings will be updated every 20 minutes.<br>
			<i><ul>
				<li>0_weather06 and ?_weather12 (with ? greater 0) is the weather in the morning</li>
				<li>0_weather12 is the weather at noon</li>
				<li>0_weather18 and ?_weather24 (with ? greater 0) is the weather in the afternoon</li>
				<li>0_weather24 is the weather at midnight</li>
				<li>0_windGust06 and ?_windGust12 (with ? greater 0) is the wind in the morning</li>
				<li>0_windGust12 is the wind at noon</li>
				<li>0_windGust18 and ?_windGust24 (with ? greater 0) is the wind in the afternoon</li>
				<li>0_windGust24 is the wind at midnight</li>
				<li>?_tMinAir is minimum temperature in the morning</li>
				<li>0_tAvgAir12 is the average temperature at noon</li>
				<li>?_tMaxAir is the maximum temperature in the afternoon</li>
				<li>0_tAvgAir24 is the average temperature at midnight</li>        
			</ul></i>
		</li>
	</ul>
	<br/><br/>

	<b>Other events:</b>
	<br/><br/>
	<ul>
		<li><b>REREAD</b> - start of rereadcfg</li>
		<li><b>REREADFILE</b> - end of file read</li>
		<li><b>REREADALERTS</b> - end of alerts read and analyzing</li>
		<li><b>REREADFORECAST</b> - end of forecast read and analyzing</li>
	</ul>
	<br/><br/>

	<b>Author's notes</b><br/><br/>
	<ul>

		<li>Module uses following additional Perl modules:<br/><br/>
		<code>Net::FTP, XML::Simple, Archive::Extract, Archive::Zip</code><br/><br/>
		If not already installed in your environment, please install them using appropriate commands from your environment.</li>
		<br/><br/>
		<li>Have fun!</li><br/>

	</ul>

</ul>

=end html
=begin html_DE

<a name="GDS"></a>
<h3>GDS</h3>
<ul>
Sorry, keine deutsche Dokumentation vorhanden.<br/><br/>
Die englische Doku gibt es hier: <a href='http://fhem.de/commandref.html#GDS'>GDS</a><br/>
</ul>
=end html_DE
=cut
