# $Id$ # 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 . # ################################################################################################### =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 GDS []" 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 list "; 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= ; 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") . "\n\n"; foreach my $def (sort keys %defs) { if($defs{$def}{TYPE} eq "GDS") { $name = $defs{$def}{NAME}; $url = __GDS_getURL(); $html .= "$name
\n\n\n"; } } $html.="\n" . __GDS_HTMLTail(); return ("text/html; charset=utf-8", $html); } sub __GDS_HTMLHead($) { my ($title) = @_; my $doctype = ''; my $xmlns = 'xmlns="http://www.w3.org/1999/xhtml"'; my $code = "$doctype\n\n\n$title\n\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 ""; } ################################################################################################### # # 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 clear alerts|all\n". "set forecasts /\n". "set help\n". "set rereadcfg\n". "set update\n"; } sub getHelp(){ return "Use one of the following commands:\n". sepLine(35)."\n". "get alerts \n". "get forecasts \n". "get help\n". "get list capstations|data\n". "get rereadcfg\n". "get warnings \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)); readingsBulkUpdate($hash, $k, latin1ToUtf8($v)); } # 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)); readingsBulkUpdate($hash, 'a_'.$anum.'_eventCode_AREA_COLOR_hex', $v); } 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 = ''; push (@alertsArray,$xmlHeader); push (@alertsArray,""); 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 = "" if($l =~ m/^\.*/); $countInfo++ if($l =~ m/^\/); push (@alertsArray,$l); } } push (@alertsArray,""); # 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 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 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

GDS

    Prerequesits

      Module uses following additional Perl modules:

      Net::FTP, XML::Simple, Archive::Extract, Archive::Zip

      If not already installed in your environment, please install them using appropriate commands from your environment.


    Define

      define <name> GDS <username> <password> [<host>]

      This module provides connection to GDS service generated by DWD

      Optional paramater host is used to overwrite default host "ftp-outgoing2.dwd.de".



    Set-Commands

      set <name> clear alerts|conditions|forecasts|all

      • alerts: Delete all a_* readings
      • all: Delete all a_*, c_*, g_* and fc_* readings

      set <name> forecasts <region>/<stationName>

        Retrieve forecasts for today and the following 3 days for selected station.
        Data will be updated periodically.

      set <name> help

        Show a help text with available commands

      set <name> update

        Update forecasts readings at selected station and restart update-timer

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


    Get-Commands

      get <name> alerts <region>

        Retrieve alert message for selected region from previously read alert file (see rereadcfg)

      get <name> conditionsmap <region>

        Retrieve map (imagefile) showing current conditions at selected station

      get <name> forecasts <region>

        Retrieve forecasts for today and the following 3 days for selected region as text

      get <name> forecastsmap <stationName>

        Retrieve map (imagefile) showing forecasts for selected region

      get <name> headlines [separator]

        Returns a string, containing all alert headlines.
        Default separator is | but can be overriden.

      get <name> help

        Show a help text with available commands

      get <name> list capstations

      • capstations: Retrieve list showing all defined warning regions. You can find your WARNCELLID with this list.

      get <name> radarmap <region>

        Retrieve map (imagefile) containig radar view from selected region

      get <name> rereadcfg

        Reread all required data from DWD Server manually: station list and CAP data

      get <name> warnings <region>

        Retrieve current warnings report for selected region

        • report type VHDL30 = regular report, issued daily
        • report type VHDL31 = regular report, issued before weekend or national holiday
        • report type VHDL32 = preliminary report, issued on special conditions
        • report type VHDL33 = cancel report, issued if necessary to cancel VHDL32

      get <name> warningssmap <region>

        Retrieve map (imagefile) containig current warnings for selected region marked with symbols


      All downloaded mapfiles can be found inside "GDS Files" area in left navigation bar.


    Attributes

    • do_not_notify
    • readingFnAttributes

    • disable - if set, gds will not try to connect to internet
    • gdsAll - defines filter for "all data" from alert message
    • gdsDebug - defines filter for debug informations
    • gdsSetForecast - defines forecasts region/station to be used after system restart
    • gdsLong - show long text fields "description" and "instruction" from alert message in readings
    • gdsPolygon - show polygon data from alert message in a reading
    • gdsHideFiles - if set to 1, the "GDS Files" menu in the left navigation bar will not be shown

    • gdsPassiveFtp - set to 1 to use passive FTP transfer
    • gdsFwName - define firewall hostname in format <hostname>:<port>
    • gdsFwType - define firewall type in a value 0..7 please refer to cpan documentation for further informations regarding firewall settings.


    Generated Readings:

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


    Other events:

    • REREAD - start of rereadcfg
    • REREADFILE - end of file read
    • REREADALERTS - end of alerts read and analyzing
    • REREADFORECAST - end of forecast read and analyzing


    Author's notes

    • Module uses following additional Perl modules:

      Net::FTP, XML::Simple, Archive::Extract, Archive::Zip

      If not already installed in your environment, please install them using appropriate commands from your environment.


    • Have fun!

=end html =begin html_DE

GDS

    Sorry, keine deutsche Dokumentation vorhanden.

    Die englische Doku gibt es hier: GDS
=end html_DE =cut