diff --git a/fhem/contrib/55_GDS.2015/55_GDS.pm b/fhem/contrib/55_GDS.2015/55_GDS.pm index d8af55bf7..0e4e064e7 100644 --- a/fhem/contrib/55_GDS.2015/55_GDS.pm +++ b/fhem/contrib/55_GDS.2015/55_GDS.pm @@ -6,9 +6,10 @@ # An FHEM Perl module to retrieve data from "Deutscher Wetterdienst" # # Copyright: betateilchen ® -# some patches provided by jensb@FHEM_Forum -# - forecasts as readings or text -# - weather weblinks +# +# some patches provided by jensb +# forecasts provided by jensb +# weblinks provided by jensb # # This file is part of fhem. # @@ -44,7 +45,7 @@ my ($bulaList, $cmapList, %rmapList, $fmapList, %bula2bulaShort, %bulaShort2dwd, #################################################################################################### # -# Main routines +# Main routines # #################################################################################################### @@ -145,9 +146,9 @@ sub GDS_Set($@) { given($command) { when("clear"){ CommandDeleteReading(undef, "$name a_.*"); - CommandDeleteReading(undef, "$name c_.*") if(defined($parameter) && $parameter eq "all"); - CommandDeleteReading(undef, "$name f_.*") if(defined($parameter) && $parameter eq "all"); - CommandDeleteReading(undef, "$name g_.*") if(defined($parameter) && $parameter eq "all"); + CommandDeleteReading(undef, "$name c_.*") if(defined($parameter) && $parameter eq "all"); + CommandDeleteReading(undef, "$name fc.?_.*") if(defined($parameter) && $parameter eq "all"); + CommandDeleteReading(undef, "$name g_.*") if(defined($parameter) && $parameter eq "all"); } when("help"){ @@ -164,9 +165,9 @@ sub GDS_Set($@) { retrieveFile($hash,"alerts"); ($aList, undef) = buildCAPList($hash); }; -# eval { -# $fList = getListForecastStationsDropdown($hash); -# }; + eval { + $fList = getListForecastStationsDropdown($hash); + }; break; } @@ -187,11 +188,14 @@ sub GDS_Set($@) { break; } -# when("forecasts"){ -# retrieveForecasts($hash, "f", @a); -# $attr{$name}{gdsSetForecast} = ReadingsVal($name,'f_stationName',undef); -# break; -# } + when("forecasts"){ + retrieveForecasts($hash, "fc", @a); + my $station = ReadingsVal($name, 'fc_stationName', undef); + if (defined($station)) { + $attr{$name}{gdsSetForecast} = $station; + } + break; + } default { return $usage; }; } @@ -291,6 +295,9 @@ sub GDS_Get($@) { } else { $result = "Keine Warnmeldung für die gesuchte Region vorhanden."; } + my $_gdsAll = AttrVal($name,"gdsAll", 0); + my $_gdsDebug = AttrVal($name,"gdsDebug", 0); + readingsSingleUpdate($hash,'_lastAlertCheck','see timestamp ->',1) if($_gdsAll || $_gdsDebug); break; } @@ -312,9 +319,9 @@ sub GDS_Get($@) { retrieveFile($hash,"conditions"); }; initDropdownLists($hash); -# eval { -# $fList = getListForecastStationsDropdown($hash); -# }; + eval { + $fList = getListForecastStationsDropdown($hash); + }; break; } @@ -402,21 +409,53 @@ sub GDS_GetUpdate($) { my ($hash) = @_; my $name = $hash->{NAME}; my (@a, $next); + + my $interval = $hash->{helper}{INTERVAL}; + my $forcastsStationName = ReadingsVal($name, "fc_stationName", undef); - if(IsDisabled($name)) { - readingsSingleUpdate($hash, 'state', 'disabled', 0); - Log3 ($name, 2, "GDS $name is disabled, data update cancelled."); - } else { - readingsSingleUpdate($hash, 'state', 'active', 0); - push @a, undef; - push @a, undef; - push @a, ReadingsVal($name, "c_stationName", ""); - retrieveConditions($hash, "c", @a); - } + if(IsDisabled($name)) { + readingsSingleUpdate($hash, 'state', 'disabled', 0); + Log3 ($name, 2, "GDS $name is disabled, data update cancelled."); + } else { + readingsSingleUpdate($hash, 'state', 'active', 0); - $next = gettimeofday()+$hash->{helper}{INTERVAL}; - readingsSingleUpdate($hash, "c_nextUpdate", localtime($next), 1); - InternalTimer($next, "GDS_GetUpdate", $hash, 1); + # schedule only one ftp fetch per update call to avoid blocking FHEM for extended periods + if (!defined($hash->{helper}{UPDATE_CYCLE}) || !defined($forcastsStationName)) { + $hash->{helper}{UPDATE_CYCLE} = 0; + } else { + $hash->{helper}{UPDATE_CYCLE} = ++$hash->{helper}{UPDATE_CYCLE}%11; + } + + # perform one ftp fetch + if ($hash->{helper}{UPDATE_CYCLE} == 0) { + push @a, undef; + push @a, undef; + push @a, ReadingsVal($name, "c_stationName", ""); + retrieveConditions($hash, "c", @a); + } else { + push @a, undef; + push @a, undef; + push @a, $forcastsStationName; + push @a, $hash->{helper}{UPDATE_CYCLE}; + retrieveForecasts($hash, "fc", @a); + } + + # vary interval for staggered fetching and waiting + if (defined($forcastsStationName)) { + if ($hash->{helper}{UPDATE_CYCLE} < 10) { + $interval = 1; # use short interval to get next forecast + } else { + $interval -= 16; # cut back approximate staggered retrieval time from interval + } + } + } + + # schedule next update + $next = gettimeofday() + $interval; + if ($interval > 1) { + readingsSingleUpdate($hash, "c_nextUpdate", localtime($next), 1); + } + InternalTimer($next, "GDS_GetUpdate", $hash, 1); return 1; } @@ -561,7 +600,7 @@ sub getListStationsText($){ sub getListCapStations($$){ my ($hash, $command) = @_; my $name = $hash->{NAME}; - my (%capHash, $file, $csv, @columns, $err, $key, $cList); + my (%capHash, $file, $csv, @columns, $err, $key, $cList, $found); $file = $tempDir.'capstations.csv'; $csv = Text::CSV->new( { binary => 1 } ); @@ -570,27 +609,37 @@ sub getListCapStations($$){ # prüfen, ob CSV schon vorhanden, # falls nicht: vom Server holen if (!-e $tempDir."caplist.csv"){ - retrieveFile($hash, $command); - } - - # CSV öffnen und parsen - open (CSV, "<", $file) or die $!; - while () { - next if ($. == 1); - if ($csv->parse($_)) { - @columns = $csv->fields(); - $capHash{latin1ToUtf8($columns[4])} = $columns[0]; - } else { - $err = $csv->error_input; - print "Failed to parse line: $err"; + (undef, $found) = retrieveFile($hash, $command); + if(!$found){ + $cList = "Error: Unable to retrieve capstation list!"; + Log3($name, 2, "GDS $name: $cList"); } } - close CSV; - # Ausgabe sortieren und zusammenstellen - foreach $key (sort keys %capHash) { - $cList .= $capHash{$key}."\t".$key."\n"; + if (!defined($cList)) { + # CSV öffnen und parsen + if (open (CSV, "<", $file)) { + while () { + next if ($. == 1); + if ($csv->parse($_)) { + @columns = $csv->fields(); + $capHash{latin1ToUtf8($columns[4])} = $columns[0]; + } else { + $err = $csv->error_input; + print "Failed to parse line: $err"; + } + } + close CSV; + } else { + Log3($name, 4, "GDS $name: Error: unable to open capstations file: $!!"); + } + + # Ausgabe sortieren und zusammenstellen + foreach $key (sort keys %capHash) { + $cList .= $capHash{$key}."\t".$key."\n"; + } } + return $cList; } @@ -612,7 +661,7 @@ sub buildCAPList($){ %capCityHash = (); %capCellHash = (); $alertsXml = undef; - $aList = "please_use_rereadcfg_first"; + $aList = "please_use_rereadcfg_first"; my $xml = new XML::Simple; my $area = 0; @@ -622,11 +671,7 @@ sub buildCAPList($){ my $destinationDirectory = $tempDir.$name."_alerts.dir"; # make XML array and analyze data - my ($err,$cF,$countInfo) = mergeCapFile($hash); - if (defined($err) && $err) { - Log3($name,1,"GDS: merge error: $err - aborting..."); - return(undef,undef); - } + my ($countInfo,$cF) = mergeCapFile($hash); eval { $alertsXml = $xml->XMLin($cF, KeyAttr => {}, ForceArray => [ 'info', 'eventCode', 'area', 'geocode' ]); }; @@ -1092,24 +1137,6 @@ sub _rgbd2h($) { return $output; } -sub createIndexFile($){ - my ($hash) = @_; - my $name = $hash->{NAME}; - - if($name){ - my $text = "". - "Aktuelle Wetterkarte: Wetterlage
". - "Aktuelle Wetterkarte: Vorhersage
". - "Aktuelle Wetterkarte: Warnungen
". - "Aktuelle Radarkarte
". - ""; - open (DATEI, ">".$tempDir.$name.".html") or die $!; - print DATEI $text; - close (DATEI); - } - return; -} - sub fillMappingTables($){ $tempDir = "/tmp/"; @@ -1389,18 +1416,582 @@ sub mergeCapFile($) { } push (@alertsArray,""); - # write the big XML file - my $cF = $destinationDirectory."/gds_alerts"; - unlink $cF if -e $cF; - my $err = FileWrite({ FileName=>$cF,ForceType=>"file" },@alertsArray); - return ($err,$cF,$countInfo); + # 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); +} + +#################################################################################################### +# +# forecast retrieval +# provided by jensb +# +#################################################################################################### + +sub retrieveForecasts($$@){ +# +# parameter: hash, prefix, region/station, forecast index (0 .. 10) +# + my ($hash, $prefix, @a) = @_; + my $name = $hash->{NAME}; + my $user = $hash->{helper}{USER}; + my $pass = $hash->{helper}{PASS}; + + # extract region and station name + if (!defined($a[2])) { + return; + } + my $i = index($a[2], '/'); + if ($i <= 0 ) { + return; + } + my $area = utf8ToLatin1(substr($a[2], 0, $i)); + my $station = utf8ToLatin1(substr($a[2], $i+1)); + $station =~ s/_/ /g; # replace underscore in station name by space + my $searchLen = length($station); + + my ($dataFile, $found, $line, %fread, $k, $v); + + # 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; + } + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); + + %fread = (); + + # 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'; + } + } + } + + # define forecast date (based on "now" + day) + my $fcEpoch = time() + $day*24*60*60; + if ($fc == 3) { + # night continues at next day + $fcEpoch += 24*60*60; + } + 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; + + # FTP retrieve + my $noDataFound = 1; + Log3($name, 4, "GDS $name: Retrieving forecasts data for day $day: $areaAndTime"); + ($dataFile, $found) = retrieveFile($hash, "forecasts", $areaAndTime, undef); + if (open WXDATA, $tempDir.$name."_forecasts") { + while (!eof(WXDATA) && chomp($line = )) { + if (index($line, $fcDate) > 0) { + # forecast date found + $fcDateFound = 1; + } + if (index(substr(lc($line),0,$searchLen), substr(lc($station),0,$searchLen)) != -1) { + # station found + last; + } + } + close WXDATA; + + # 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; + } + } + } + + 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, 1) eq '-') { + if (defined($copyTimeLabel)) { + $fread{$prefix.$day.$tempLabel} = utf8ToLatin1(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, 1) eq '-') { + if (defined($copyTimeLabel)) { + $fread{$prefix.$day."_weather".$timeLabel} = utf8ToLatin1(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, 1) eq '-') { + if (defined($copyTimeLabel)) { + $fread{$prefix.$day."_windGust".$timeLabel} = utf8ToLatin1(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 + if (defined($v) && substr($v, 0, 1) ne '-') { + readingsBulkUpdate($hash, $k, latin1ToUtf8($v)); + } + } + readingsEndUpdate($hash, 1); +} + +sub getListForecastStationsDropdown($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my @a; + my @regions = keys(%rmapList); + foreach (@regions) { + my $areaAndTime = $_.'_morgen_spaet'; + my ($dataFile, $found) = retrieveFile($hash, "forecasts", $areaAndTime, undef); + if (open WXDATA, $tempDir.$name."_forecasts") { + my $lineCount = 0; + while (chomp(my $line = )) { + # skip header lines + $lineCount++; + if ($lineCount > 2) { + if (length($line) == 0 || substr($line, 0, 3) eq ' ') { + # empty line, done + last; + } else { + # line with station name found + $line = latin1ToUtf8($line); + $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 + push @a, $_.'/'.$b[0]; # concat region name and station name (1st column) + } + } + } + close WXDATA; + } + } + + if (!@a) { + Log3($name, 4, "GDS $name: Error: unable to open forecast file!"); + } + + @a = sort(@a); + my $liste = join(",", @a); + $liste =~ s/\s+,/,/g; # replace multiple spaces followed by comma with comma + $liste =~ s/\s/_/g; # replace spaces in stationName with underscore for list in frontend + + return $liste; +} + +#################################################################################################### +# +# create weblinks +# provided by jensb +# +#################################################################################################### + +# weather description to icon name mapping +my %GDSDayWeatherIconMap = ( + 'bedeckt' => 'overcast', + 'bewölkt' => 'mostlycloudy', + 'Dunst oder flacher Nebel' => 'haze', + 'gefrierender Nebel' => 'icy', + 'gering bewölkt' => 'partlycloudy', + 'Gewitter' => 'thunderstorm', + 'Glatteisbildung' => 'icy', + 'Graupelschauer' => 'snow', + 'Hagelschauer' => 'snow', + 'heiter' => 'partlycloudy', + 'in Wolken' => 'mostlycloudy', + 'kein signifikantes Wetter' => 'na', + 'kräftiger Graupelschauer' => 'heavysnow', + 'kräftiger Hagelschauer' => 'heavysnow', + 'kräftiger Regen' => 'heavyrain', + 'kräftiger Regenschauer' => 'scatteredshowers', + 'kräftiger Schneefall' => 'heavysnow', + 'kräftiger Schneeregen' => 'rainsnow', + 'kräftiger Schneeregenschauer' => 'rainsnow', + 'kräftiger Schneeschauer' => 'heavysnow', + 'leicht bewölkt' => 'partlycloudy', + 'leichter Regen' => 'mist', + 'leichter Schneefall' => 'snow', + 'leichter Schneeregen' => 'rainsnow', + 'Nebel' => 'fog', + 'Regen' => 'rain', + 'Regenschauer' => 'scatteredshowers', + 'Sandsturm' => 'dust', + 'Schneefall' => 'snow', + 'Schneefegen' => 'snow', + 'Schneeregen' => 'rainsnow', + 'Schneeregenschauer' => 'rainsnow', + 'Schneeschauer' => 'snow', + 'schweres Gewitter' => 'thunderstorm', + 'stark bewölkt' => 'mostlycloudy', + 'starkes Gewitter' => 'thunderstorm', + 'wolkenlos' => 'sunny', + '---' => 'mostlycloudy', + ); + +my %GDSNightWeatherIconMap = ( + 'bedeckt' => 'overcast', + 'bewölkt' => 'mostlycloudy_night', + 'Dunst oder flacher Nebel' => 'haze_night', + 'gefrierender Nebel' => 'icy', + 'gering bewölkt' => 'partlycloudy_night', + 'Gewitter' => 'thunderstorm', + 'Glatteisbildung' => 'icy', + 'Graupelschauer' => 'snow', + 'Hagelschauer' => 'snow', + 'heiter' => 'partlycloudy_night', + 'in Wolken' => 'mostlycloudy_night', + 'kein signifikantes Wetter' => 'na', + 'kräftiger Graupelschauer' => 'heavysnow', + 'kräftiger Hagelschauer' => 'heavysnow', + 'kräftiger Regen' => 'heavyrain', + 'kräftiger Regenschauer' => 'scatteredshowers_night', + 'kräftiger Schneefall' => 'heavysnow', + 'kräftiger Schneeregen' => 'rainsnow', + 'kräftiger Schneeregenschauer' => 'rainsnow', + 'kräftiger Schneeschauer' => 'heavysnow', + 'leicht bewölkt' => 'partlycloudy_night', + 'leichter Regen' => 'mist', + 'leichter Schneefall' => 'snow', + 'leichter Schneeregen' => 'rainsnow', + 'Nebel' => 'fog', + 'Regen' => 'rain', + 'Regenschauer' => 'scatteredshowers_night', + 'Sandsturm' => 'dust', + 'Schneefall' => 'snow', + 'Schneefegen' => 'snow', + 'Schneeregen' => 'rainsnow', + 'Schneeregenschauer' => 'rainsnow', + 'Schneeschauer' => 'snow', + 'schweres Gewitter' => 'thunderstorm', + 'stark bewölkt' => 'mostlycloudy_night', + 'starkes Gewitter' => 'thunderstorm', + 'wolkenlos' => 'sunny_night', + '---' => 'mostlycloudy_night', + ); + +# icon parameters +use constant ICONHIGHT => 120; +use constant ICONWIDTH => 175; +use constant ICONSCALE => 0.5; + +sub GDSIsDay($$) { +# check if it is day at given time +# +# @param: time +# @param: altitude, see documentation of module SUNRISE_EL + my ($time, $altitude) = @_; + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($time); + my $t = ($hour*60 + $min) + $sec; + + my (undef, $srHour, $srMin, $srSec, undef) = GetTimeSpec(sunrise_abs_dat($time, $altitude)); + my $sunrise = ($srHour*60 + $srMin) + $srSec; + + my (undef, $ssHour, $ssMin, $ssSec, undef) = GetTimeSpec(sunset_abs_dat($time, $altitude)); + my $sunset = ($ssHour*60 + $ssMin) + $ssSec; + + return $t >= $sunrise && $t <= $sunset; +} + +sub GDSIconIMGTag($;$) { +# get FHEM weather icon +# +# @param: weather description +# @param: time of weather description or 1 for night, optional, defaults to daytime icons + my $width = int(ICONSCALE*ICONWIDTH); + my ($weather, $time) = @_; + my $icon; + if (!defined($time) || (defined($time) && $time > 1 && GDSIsDay($time, "REAL"))) { + $icon = $GDSDayWeatherIconMap{$weather}; + } else { + $icon = $GDSNightWeatherIconMap{$weather}; + } + if (defined($icon)) { + my $url= FW_IconURL("weather/$icon"); + my $style= " width=$width"; + return "\"$icon\""; + } else { + return ""; + } +} + +sub GDSAsHtmlV($;$) { +# create forecast in a vertical HTML table +# +# @param: device name +# @param: number of icons, optional, default 8 + my ($d,$items) = @_; + $d = "" if(!$d); + $items = $items? $items - 1 : 7; + return "$d is not a GDS instance
" + if(!$defs{$d} || $defs{$d}{TYPE} ne "GDS"); + + my $width = int(ICONSCALE*ICONWIDTH); + + my $ret = sprintf('', $width); + $ret .= sprintf('', + $width, + GDSIconIMGTag(ReadingsVal($d, "c_weather", "?"), time_str2num(ReadingsTimestamp($d, "c_weather", TimeNow()))), + ReadingsVal($d, "c_weather", "?"), + ReadingsVal($d, "c_temperature", "?"), + ReadingsVal($d, "c_windSpeed", "?"), ReadingsVal($d, "c_windDir", "?")); + + # get time of last forecast + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time_str2num(ReadingsTimestamp($d, "fc3_weather24", TimeNow()))); + + for(my $i=0; $i<$items; $i++) { + my $day = int(($i + 1)/2); + my $timeLabel = $i == 0? ($hour < 17? '18' : '24') : ($i - 1)%2 == 0? '12' : '24'; + my $weekday = $i == 0? ($hour < 17? 'Spät' : 'Nachts') : ($i - 1)%2 == 0? ReadingsVal($d, "fc".$day."_weekday", "?").' früh' : ReadingsVal($d, "fc".$day."_weekday", "?").' spät'; + + if (($i - 1)%2 == 0) { + $ret .= sprintf('', + $width, + GDSIconIMGTag(ReadingsVal($d, "fc".$day."_weather".$timeLabel, "?")), + $weekday, + ReadingsVal($d, "fc".$day."_weather".$timeLabel, "?"), + ReadingsVal($d, "fc".$day."_tMinAir", "?"), + ReadingsVal($d, "fc".$day."_windGust".$timeLabel, "")); + } else { + if ($i == 0 && $hour >= 17) { + $ret .= sprintf('', + $width, + GDSIconIMGTag(ReadingsVal($d, "fc".$day."_weather".$timeLabel, "?"), 1), + $weekday, + ReadingsVal($d, "fc".$day."_weather".$timeLabel, "?"), + ReadingsVal($d, "fc".$day."_tAvgAir".$timeLabel, "?"), + ReadingsVal($d, "fc".$day."_windGust".$timeLabel, "")); + } else { + $ret .= sprintf('', + $width, + GDSIconIMGTag(ReadingsVal($d, "fc".$day."_weather".$timeLabel, "?")), + $weekday, + ReadingsVal($d, "fc".$day."_weather".$timeLabel, "?"), + ReadingsVal($d, "fc".$day."_tMaxAir", "?"), + ReadingsVal($d, "fc".$day."_windGust".$timeLabel, "")); + } + } + } + + $ret .= "
%sAktuell: %s
%s°C
Wind %s km/h %s
%s%s: %s
min %s°C
%s
%s%s: %s
%s°C
%s
%s%s: %s
max %s°C
%s
"; + return $ret; +} + +sub GDSAsHtmlH($;$) { +# create forecast in a horizontal HTML table +# +# @param: device name +# @param: number of icons, optional, default 8 + my ($d, $items) = @_; + $d = "" if(!$d); + $items = $items? $items - 1 : 7; + return "$d is not a GDS instance
" + if(!$defs{$d} || $defs{$d}{TYPE} ne "GDS"); + + my $width = 110; + + my $ret = ''; + + # get time of last forecast + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time_str2num(ReadingsTimestamp($d, "fc3_weather24", TimeNow()))); + + # weekday / time + $ret .= sprintf(''); + for(my $i=0; $i<$items; $i++) { + my $day = int(($i + 1)/2); + my $timeLabel = $i == 0? ($hour < 17? '18' : '24') : ($i - 1)%2 == 0? '12' : '24'; + my $weekday = $i == 0? ($hour < 17? 'Spät' : 'Nachts') : ($i - 1)%2 == 0? ReadingsVal($d, "fc".$day."_weekday", "?").' früh' : ReadingsVal($d, "fc".$day."_weekday", "?").' spät'; + $ret .= sprintf('', $weekday); + } + $ret .= ''; + + # condition icon + $ret .= sprintf('', $width, GDSIconIMGTag(ReadingsVal($d, "c_weather", "?"), time_str2num(ReadingsTimestamp($d, "c_weather", TimeNow())))); + for(my $i=0; $i<$items; $i++) { + my $day = int(($i + 1)/2); + my $timeLabel = $i == 0? ($hour < 17? '18' : '24') : ($i - 1)%2 == 0? '12' : '24'; + $ret .= sprintf('', $width, GDSIconIMGTag(ReadingsVal($d, "fc".$day."_weather".$timeLabel, "?"), $i==0 && $hour >= 17? 1 : undef)); + } + $ret .= ''; + + # condition text + $ret .= sprintf('', ReadingsVal($d, "c_weather", "?")); + for(my $i=0; $i<$items; $i++) { + my $day = int(($i + 1)/2); + my $timeLabel = $i == 0? ($hour < 17? '18' : '24') : ($i - 1)%2 == 0? '12' : '24'; + $ret .= sprintf('', ReadingsVal($d, "fc".$day."_weather".$timeLabel, "?")); + } + $ret .= ''; + + # temperature / min temperature + $ret .= sprintf('', ReadingsVal($d, "c_temperature", "?")); + for(my $i=0; $i<$items; $i++) { + my $day = int(($i + 1)/2); + my $timeLabel = $i == 0? ($hour < 17? '18' : '24') : ($i - 1)%2 == 0? '12' : '24'; + if (($i - 1)%2 == 0) { + $ret .= sprintf('', ReadingsVal($d, "fc".$day."_tMinAir", "?")); + } else { + if ($i == 0 && $hour >= 17) { + $ret .= sprintf('', ReadingsVal($d, "fc".$day."_tAvgAir".$timeLabel, "?")); + } else { + $ret .= sprintf('', ReadingsVal($d, "fc".$day."_tMaxAir", "?")); + } + } + } + $ret .= ''; + + # wind + $ret .= sprintf('', ReadingsVal($d, "c_windSpeed", "?"), ReadingsVal($d, "c_windDir", "?")); + for(my $i=0; $i<$items; $i++) { + my $day = int(($i + 1)/2); + my $timeLabel = $i == 0? ($hour < 17? '18' : '24') : ($i - 1)%2 == 0? '12' : '24'; + $ret .= sprintf('', ReadingsVal($d, "fc".$day."_windGust".$timeLabel, "")); + } + $ret .= "
Aktuell%s
%s%s
%s%s
%s°Cmin %s°C%s°Cmax %s°C
%s km/h %s%s
"; + + return $ret; +} + +sub GDSAsHtmlD($;$) { +# create forecast in a horizontal or vertical HTML table depending on the display orientation +# +# @param: device name +# @param: number of icons, optional, default 8 + my ($d,$i) = @_; + if($FW_ss) { + GDSAsHtmlV($d,$i); + } else { + GDSAsHtmlH($d,$i); + } } 1; #################################################################################################### # -# Documentation +# Documentation # #################################################################################################### # @@ -1476,10 +2067,15 @@ sub mergeCapFile($) { # 2015-10-08 changed added mergeCapFile() # code cleanup in buildCAPList() # use system call "unzip" instead of Archive::Zip -# added NotifyFn for rereadcfg after INITIALIZED|REREADCFG +# added NotifyFn for rereadcfg after INITIALIZED # improved startup data retrieval # improved attribute handling # +# 2015-10-09 changed removed createIndexFile(), no longer needed since 2015-01-30 +# added forecast retrieval +# added weblink generator +# done a lot of code cleanup +# #################################################################################################### # # Further informations @@ -1513,22 +2109,6 @@ sub mergeCapFile($) { # --- : Wert nicht vorhanden # #################################################################################################### -# -# sub -# _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]); -# } -# -#################################################################################################### =pod =begin html @@ -1552,11 +2132,20 @@ sub mergeCapFile($) { Define
    -
    - define <name> GDS <username> <password> -

    - This module provides connection to GDS service generated by DWD
    - +
    + define <name> GDS <username> <password>
    +
    + This module provides connection to GDS service generated by DWD
    +
    + It also provides three additional functions GDSAsHtmlV, GDSAsHtmlH and GDSAsHtmlD. + The first function returns the HTML code for a vertically arranged weather forecast. + The second function returns the HTML code for a horizontally arranged weather forecast. + The third function dynamically picks the orientation depending on whether a smallscreen style is set (vertical layout) or not (horizontal layout). + The attributes gdsSetCond and gdsSetForecast must be configured for the functions to work. + Each of these functions accepts an additional parameter to limit the number of icons to display (1...8). + If the attribute gdsSetForecast is not configured this parameter should be set to 1.
    +
    + Example: define MyForecastWeblink weblink htmlCode { GDSAsHtml("MyWeather") } where "MyWeather" is the name of your GDS device.


@@ -1569,7 +2158,7 @@ sub mergeCapFile($) {

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

@@ -1578,6 +2167,11 @@ sub mergeCapFile($) {
    Retrieve current conditions at selected station. Data will be updated periodically.

+ 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
@@ -1585,16 +2179,17 @@ sub mergeCapFile($) { set <name> rereadcfg

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

set <name> update

-
    Update conditions readings at selected station and restart update-timer
+
    Update conditions and forecasts readings at selected station and restart update-timer

  • condition readings generated by SET use prefix "c_"
  • -
  • readings generated by SET will be updated automatically every 60 minutes
  • +
  • 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


  • @@ -1614,6 +2209,11 @@ sub mergeCapFile($) {
      Retrieve 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> conditionsmap <region>

      Retrieve map (imagefile) showing current conditions at selected station
    @@ -1685,8 +2285,10 @@ sub mergeCapFile($) {
  • gdsAll - defines filter for "all data" from alert message
  • gdsDebug - defines filter for debug informations
  • gdsSetCond - defines conditions area to be used after system restart
  • +
  • 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
  • +
  • gdsShowMapFilesMenu - if set to 0, 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>
  • @@ -1703,7 +2305,23 @@ sub mergeCapFile($) { 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
  • -
  • c_<readingName> - weather data from SET weather conditions. Readings will be updated every 20 minutes
  • +
  • c_<readingName> - weather data from SET weather conditions. Readings will be updated every 20 minutes.
  • +
  • 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
    • +
    +
  • g_<readingName> - weather data from GET weather conditions. Readings will NOT be updated automatically