From 6fbed7844962facb1b362cff1a524cfc187fc515 Mon Sep 17 00:00:00 2001 From: jensb <> Date: Fri, 28 Sep 2018 18:54:58 +0000 Subject: [PATCH] 55_DWD_OpenData.pm: CSV forecast replaced by KML forecast, several readings have changed, attr forecastProperties must be updated (see recent messages in forum #83097), DWDODweblink.pm is incompatible and must be removed git-svn-id: https://svn.fhem.de/fhem/trunk@17420 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 2 + fhem/FHEM/55_DWD_OpenData.pm | 1746 +++++++++++++++++++--------------- 2 files changed, 998 insertions(+), 750 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index dfb278068..e74cf0015 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,7 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - change: 55_DWD_OpenData: KML based forecast, readings have changed, + attr forecastProperties must be updated (forum #83097) - bugfix: 12_HProtocolGateway: Filllevel conversion corrected - bugfix: 57_Calendar.pm: make ical file work again - change: 34_ESPEasy: get/set cmd handling rewritten, more supported ESP diff --git a/fhem/FHEM/55_DWD_OpenData.pm b/fhem/FHEM/55_DWD_OpenData.pm index 8fb65395a..530480ef6 100644 --- a/fhem/FHEM/55_DWD_OpenData.pm +++ b/fhem/FHEM/55_DWD_OpenData.pm @@ -1,44 +1,49 @@ -=pod encoding UTF-8 (äöüÄÖÜ€) -######################################################################################## -# +# ----------------------------------------------------------------------------- # $Id$ -# -# FHEM module for DWD Open Data Server -# -######################################################################################## -# -# LICENSE AND COPYRIGHT -# -# Copyright (C) 2018 jensb -# Copyright (C) 2018 JoWiemann (use of HttpUtils instead of LWP::Simple) -# -# All rights reserved -# -# This script 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. -# -# The GNU General Public License can be found at -# http://www.gnu.org/copyleft/gpl.html. -# A copy is found in the textfile GPL.txt and important notices to the license -# from the author is found in LICENSE.txt distributed with these scripts. -# -# This script 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. -# -# This copyright notice MUST APPEAR in all copies of the script! -# -######################################################################################## +# ----------------------------------------------------------------------------- + +=encoding UTF-8 + +=head1 NAME + +DWD_OpenData - A FHEM Perl module to retrieve forecasts and alerts from the +DWD Open Data Server. + +=head1 LICENSE AND COPYRIGHT + +Copyright (C) 2018 Jens B. + +Copyright (C) 2018 JoWiemann (use of HttpUtils instead of LWP::Simple) + +All rights reserved + +This script 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. + +The GNU General Public License can be found at + +http://www.gnu.org/copyleft/gpl.html. + +A copy is found in the textfile GPL.txt and important notices to the license +from the author is found in LICENSE.txt distributed with these scripts. + +This script 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. + +This copyright notice MUST APPEAR in all copies of the script! + =cut -package main; +package DWD_OpenData; use strict; use warnings; +use Encode; use File::Temp qw(tempfile); use IO::Uncompress::Unzip qw(unzip $UnzipError); use POSIX; @@ -49,180 +54,174 @@ use Time::Piece; use Blocking; use HttpUtils; -use SetExtensions; - use feature qw(switch); no if $] >= 5.017011, warnings => 'experimental'; -use constant DWD_OD_UPDATE_DISTRICTS => -1; -use constant DWD_OD_UPDATE_COMMUNEUNIONS => -2; -use constant DWD_OD_UPDATE_ALL => -3; +use constant UPDATE_DISTRICTS => -1; +use constant UPDATE_COMMUNEUNIONS => -2; +use constant UPDATE_ALL => -3; -my @dwd_dayProperties; -my @dwd_hourProperties; -my @dwd_wwdText; +require Exporter; +our $VERSION = 1.010.002; +our @ISA = qw(Exporter); +our @EXPORT = qw(GetForecast GetAlerts UpdateAlerts UPDATE_DISTRICTS UPDATE_COMMUNEUNIONS UPDATE_ALL); +our @EXPORT_OK = qw(IsCommuneUnionWarncellId); -my @dwd_alerts = [ undef, undef ]; -my @dwd_alerts_received = [ undef, undef ]; -my @dwd_alerts_updating = [ undef, undef ]; +my %forecastPropertyAliases = ( 'TX' => 'Tx', 'TN' => 'Tn', 'TG' => 'Tg', 'TM' => 'Tm' ); -=item DWD_OpenData_Initialize($) +my %forecastPropertyPeriods = ( + 'DD' => 1, 'DRR1' => 1, 'E_DD' => 1, 'E_FF' => 1, 'E_PPP' => 1, 'E_Td' => 1, 'E_TTT' => 1, 'FF' => 1, 'FX1' => 1, 'FX3' => 1, 'FX625' => 1, 'FX640' => 1, 'FX655' => 1, 'FXh' => 1, 'FXh25' => 1, 'FXh40' => 1, 'FXh55' => 1, 'N' => 1, 'N05' => 1, 'Neff' => 1, 'Nh' => 1, 'Nl' => 1, 'Nlm' => 1, 'Nm' => 1, 'PPPP' => 1, 'R101' => 1, 'R102' => 1, 'R103' => 1, 'R105' => 1, 'R107' => 1, 'R110' => 1, 'R120' => 1, 'R130' => 1, 'R150' => 1, 'R600' => 1, 'R602' => 1, 'R610' => 1, 'R650' => 1, 'RR1c' => 1, 'RR1o1' => 1, 'RR1u1' => 1, 'RR1w1' => 1, 'RR3c' => 1, 'RR6c' => 1, 'RRL1c' => 1, 'RRS1c' => 1, 'RRS3c' => 1, 'RRad1' => 1, 'Rad1h' => 1, 'RRhc' => 1, 'Rh00' => 1, 'Rh02' => 1, 'Rh10' => 1, 'Rh50' => 1, 'SunD1' => 1, 'SunD3' => 1, 'T5cm' => 1, 'Td' => 1, 'TTT' => 1, 'VV' => 1, 'VV10' => 1, 'W1W2' => 1, 'WPc11' => 1, 'WPc31' => 1, 'WPc61' => 1, 'WPcd1' => 1, 'WPch1' => 1, 'ww' => 1, 'ww3' => 1, 'wwC' => 1, 'wwC6' => 1, 'wwCh' => 1, 'wwD' => 1, 'wwD6' => 1, 'wwDh' => 1, 'wwF' => 1, 'wwF6' => 1, 'wwFh' => 1, 'wwL' => 1, 'wwL6' => 1, 'wwLh' => 1, 'wwM' => 1, 'wwM6' => 1, 'wwMd' => 1, 'wwMh' => 1, 'wwP' => 1, 'wwP6' => 1, 'wwPd' => 1, 'wwPh' => 1, 'wwS' => 1, 'wwS6' => 1, 'wwSh' => 1, 'wwT' => 1, 'wwT6' => 1, 'wwTd' => 1, 'wwTh' => 1, 'wwZ' => 1, 'wwZ6' => 1, 'wwZh' => 1, + 'PEvap' => 24, 'PSd00' => 24, 'PSd30' => 24, 'PSd60' => 24, 'RRdc' => 24, 'RSunD' => 24, 'Rd00' => 24, 'Rd02' => 24, 'Rd10' => 24, 'Rd50' => 24, 'SunD' => 24, 'Tg' => 24, 'Tm' => 24, 'Tn' => 24, 'Tx' => 24 + ); - @param hash hash of DWD_OpenData device +my %forecastDefaultProperties = ( + 'Tg' => 1, 'Tn' => 1, 'Tx' => 1, 'DD' => 1, 'FX1' => 1, 'Neff' => 1, 'RR6c' => 1, 'RRhc' => 1, 'Rh00' => 1, 'TTT' => 1, 'ww' => 1 + ); - description - FHEM module initialization function +# 1 = temperature in K, 2 = integer value, 3 = wind speed in m/s, 4 = pressure in Pa +my %forecastPropertyTypes = ( + 'Tx' => 1, 'Tn' => 1, 'Tg' => 1, 'Tm'=> 1, 'Td' => 1, 'T5cm' => 1, 'TTT' => 1, + 'DD' => 2, 'Neff' => 2, 'Nh' => 2, 'Nl' => 2, 'Nlm' => 2, 'Nm' => 2, 'Rh00' => 2, 'ww' => 2, 'ww3' => 2, 'WPc11' => 2, 'WPc31' => 2, 'WPc61' => 2, 'WPch1' => 2, 'WPcd1' => 2, + 'FF' => 3, 'FX1' => 3, 'FX3' => 3, 'FXh' => 3, + 'PPPP' => 4 + ); + +my @wwdText = ('Bewölkungsentwicklung nicht beobachtet', + 'Bewölkung abnehmend', + 'Bewölkung unverändert', + 'Bewölkung zunehmend', + # 4 Dunst, Rauch, Staub oder Sand + 'Sicht durch Rauch oder Asche vermindert', + 'trockener Dunst (relative Feuchte < 80 %)', + 'verbreiteter Schwebstaub, nicht vom Wind herangeführt', + 'Staub oder Sand bzw. Gischt, vom Wind herangeführt', + 'gut entwickelte Staub- oder Sandwirbel', + 'Staub- oder Sandsturm im Gesichtskreis, aber nicht an der Station', + # 10 Trockenereignisse + 'feuchter Dunst (relative Feuchte > 80 %)', + 'Schwaden von Bodennebel', + 'durchgehender Bodennebel', + 'Wetterleuchten sichtbar, kein Donner gehört', + 'Niederschlag im Gesichtskreis, nicht den Boden erreichend', + 'Niederschlag in der Ferne (> 5 km), aber nicht an der Station', + 'Niederschlag in der Nähe (< 5 km), aber nicht an der Station', + 'Gewitter (Donner hörbar), aber kein Niederschlag an der Station', + 'Markante Böen im Gesichtskreis, aber kein Niederschlag an der Station', + 'Tromben (trichterförmige Wolkenschläuche) im Gesichtskreis', + # 20 Ereignisse der letzten Stunde, aber nicht zur Beobachtungszeit + 'nach Sprühregen oder Schneegriesel', + 'nach Regen', + 'nach Schneefall', + 'nach Schneeregen oder Eiskörnern', + 'nach gefrierendem Regen', + 'nach Regenschauer', + 'nach Schneeschauer', + 'nach Graupel- oder Hagelschauer', + 'nach Nebel', + 'nach Gewitter', + # 30 Staubsturm, Sandsturm, Schneefegen oder -treiben + 'leichter oder mäßiger Sandsturm, an Intensität abnehmend', + 'leichter oder mäßiger Sandsturm, unveränderte Intensität', + 'leichter oder mäßiger Sandsturm, an Intensität zunehmend', + 'schwerer Sandsturm, an Intensität abnehmend', + 'schwerer Sandsturm, unveränderte Intensität', + 'schwerer Sandsturm, an Intensität zunehmend', + 'leichtes oder mäßiges Schneefegen, unter Augenhöhe', + 'starkes Schneefegen, unter Augenhöhe', + 'leichtes oder mäßiges Schneetreiben, über Augenhöhe', + 'starkes Schneetreiben, über Augenhöhe', + # 40 Nebel oder Eisnebel + 'Nebel in einiger Entfernung', + 'Nebel in Schwaden oder Bänken', + 'Nebel, Himmel erkennbar, dünner werdend', + 'Nebel, Himmel nicht erkennbar, dünner werdend', + 'Nebel, Himmel erkennbar, unverändert', + 'Nebel, Himmel nicht erkennbar, unverändert', + 'Nebel, Himmel erkennbar, dichter werdend', + 'Nebel, Himmel nicht erkennbar, dichter werdend', + 'Nebel mit Reifansatz, Himmel erkennbar', + 'Nebel mit Reifansatz, Himmel nicht erkennbar', + # 50 Sprühregen + 'unterbrochener leichter Sprühregen', + 'durchgehend leichter Sprühregen', + 'unterbrochener mäßiger Sprühregen', + 'durchgehend mäßiger Sprühregen', + 'unterbrochener starker Sprühregen', + 'durchgehend starker Sprühregen', + 'leichter gefrierender Sprühregen', + 'mäßiger oder starker gefrierender Sprühregen', + 'leichter Sprühregen mit Regen', + 'mäßiger oder starker Sprühregen mit Regen', + # 60 Regen + 'unterbrochener leichter Regen oder einzelne Regentropfen', + 'durchgehend leichter Regen', + 'unterbrochener mäßiger Regen', + 'durchgehend mäßiger Regen', + 'unterbrochener starker Regen', + 'durchgehend starker Regen', + 'leichter gefrierender Regen', + 'mäßiger oder starker gefrierender Regen', + 'leichter Schneeregen', + 'mäßiger oder starker Schneeregen', + # 70 Schnee + 'unterbrochener leichter Schneefall oder einzelne Schneeflocken', + 'durchgehend leichter Schneefall', + 'unterbrochener mäßiger Schneefall', + 'durchgehend mäßiger Schneefall', + 'unterbrochener starker Schneefall', + 'durchgehend starker Schneefall', + 'Eisnadeln (Polarschnee)', + 'Schneegriesel', + 'Schneekristalle', + 'Eiskörner (gefrorene Regentropfen)', + # 80 Schauer + 'leichter Regenschauer', + 'mäßiger oder starker Regenschauer', + 'äußerst heftiger Regenschauer', + 'leichter Schneeregenschauer', + 'mäßiger oder starker Schneeregenschauer', + 'leichter Schneeschauer', + 'mäßiger oder starker Schneeschauer', + 'leichter Graupelschauer', + 'mäßiger oder starker Graupelschauer', + 'leichter Hagelschauer', + 'mäßiger oder starker Hagelschauer', + # 90 Gewitter + 'Gewitter in der letzten Stunde, zurzeit leichter Regen', + 'Gewitter in der letzten Stunde, zurzeit mäßiger oder starker Regen', + 'Gewitter in der letzten Stunde, zurzeit leichter Schneefall/Schneeregen/Graupel/Hagel', + 'Gewitter in der letzten Stunde, zurzeit mäßiger oder starker Schneefall/Schneeregen/Graupel/Hagel', + 'leichtes oder mäßiges Gewitter mit Regen oder Schnee', + 'leichtes oder mäßiges Gewitter mit Graupel oder Hagel', + 'starkes Gewitter mit Regen oder Schnee', + 'starkes Gewitter mit Sandsturm', + 'starkes Gewitter mit Graupel oder Hagel'); + +my @alerts_data = [ undef, undef ]; +my @alerts_received = [ undef, undef ]; +my @alerts_updating = [ undef, undef ]; + + +=head1 FHEM CALLBACK FUNCTIONS + +=head2 Define($$) + +FHEM I + +=over + +=item * param hash: hash of DWD_OpenData device + +=item * param def: module define parameters, will be ignored + +=item * return undef on success or error message + +=back =cut -sub DWD_OpenData_Initialize($) { - my ($hash) = @_; - my $name = $hash->{NAME}; - - $hash->{DefFn} = "DWD_OpenData_Define"; - $hash->{UndefFn} = "DWD_OpenData_Undef"; - $hash->{ShutdownFn} = 'DWD_OpenData_Shutdown'; - $hash->{AttrFn} = "DWD_OpenData_Attr"; - $hash->{GetFn} = "DWD_OpenData_Get"; - - $hash->{AttrList} = "disable:0,1 " - ."forecastStation forecastDays forecastResolution:3,6 forecastWW2Text:0,1 " - ."forecastProperties:uzsuSelect,Tx,Tn,Tm,Tg,TT,Td,dd,ff,fx,RR6,RRp6,RR12,RRp12,RR24,RRp24,ev,ww,VV,Nf,NL,NM,NH,SS24,PPPP " - ."alertArea alertLanguage:DE,EN " - ."timezone " - .$readingFnAttributes; - - @dwd_dayProperties = ( 'Tx', 'Tn', 'Tg', 'Tm', 'ev', 'SS24' ); - - @dwd_hourProperties = ( 'TT', 'Td', 'RR6', 'RRp6', 'RR12', 'RRp12', 'RR24', 'RRp24', - 'ww', 'Nf', 'NL', 'NM', 'NH', 'dd', 'ff', 'fx', 'VV', 'PPPP' ); - - @dwd_wwdText = ( "Bewölkungsentwicklung nicht beobachtet", - "Bewölkung abnehmend", - "Bewölkung unverändert", - "Bewölkung zunehmend", - # 4 Dunst, Rauch, Staub oder Sand - "Sicht durch Rauch oder Asche vermindert", - "trockener Dunst (relative Feuchte < 80 %)", - "verbreiteter Schwebstaub, nicht vom Wind herangeführt", - "Staub oder Sand bzw. Gischt, vom Wind herangeführt", - "gut entwickelte Staub- oder Sandwirbel", - "Staub- oder Sandsturm im Gesichtskreis, aber nicht an der Station", - # 10 Trockenereignisse - "feuchter Dunst (relative Feuchte > 80 %)", - "Schwaden von Bodennebel", - "durchgehender Bodennebel", - "Wetterleuchten sichtbar, kein Donner gehört", - "Niederschlag im Gesichtskreis, nicht den Boden erreichend", - "Niederschlag in der Ferne (> 5 km), aber nicht an der Station", - "Niederschlag in der Nähe (< 5 km), aber nicht an der Station", - "Gewitter (Donner hörbar), aber kein Niederschlag an der Station", - "Markante Böen im Gesichtskreis, aber kein Niederschlag an der Station", - "Tromben (trichterförmige Wolkenschläuche) im Gesichtskreis", - # 20 Ereignisse der letzten Stunde, aber nicht zur Beobachtungszeit - "nach Sprühregen oder Schneegriesel", - "nach Regen", - "nach Schneefall", - "nach Schneeregen oder Eiskörnern", - "nach gefrierendem Regen", - "nach Regenschauer", - "nach Schneeschauer", - "nach Graupel- oder Hagelschauer", - "nach Nebel", - "nach Gewitter", - # 30 Staubsturm, Sandsturm, Schneefegen oder -treiben - "leichter oder mäßiger Sandsturm, an Intensität abnehmend", - "leichter oder mäßiger Sandsturm, unveränderte Intensität", - "leichter oder mäßiger Sandsturm, an Intensität zunehmend", - "schwerer Sandsturm, an Intensität abnehmend", - "schwerer Sandsturm, unveränderte Intensität", - "schwerer Sandsturm, an Intensität zunehmend", - "leichtes oder mäßiges Schneefegen, unter Augenhöhe", - "starkes Schneefegen, unter Augenhöhe", - "leichtes oder mäßiges Schneetreiben, über Augenhöhe", - "starkes Schneetreiben, über Augenhöhe", - # 40 Nebel oder Eisnebel - "Nebel in einiger Entfernung", - "Nebel in Schwaden oder Bänken", - "Nebel, Himmel erkennbar, dünner werdend", - "Nebel, Himmel nicht erkennbar, dünner werdend", - "Nebel, Himmel erkennbar, unverändert", - "Nebel, Himmel nicht erkennbar, unverändert", - "Nebel, Himmel erkennbar, dichter werdend", - "Nebel, Himmel nicht erkennbar, dichter werdend", - "Nebel mit Reifansatz, Himmel erkennbar", - "Nebel mit Reifansatz, Himmel nicht erkennbar", - # 50 Sprühregen - "unterbrochener leichter Sprühregen", - "durchgehend leichter Sprühregen", - "unterbrochener mäßiger Sprühregen", - "durchgehend mäßiger Sprühregen", - "unterbrochener starker Sprühregen", - "durchgehend starker Sprühregen", - "leichter gefrierender Sprühregen", - "mäßiger oder starker gefrierender Sprühregen", - "leichter Sprühregen mit Regen", - "mäßiger oder starker Sprühregen mit Regen", - # 60 Regen - "unterbrochener leichter Regen oder einzelne Regentropfen", - "durchgehend leichter Regen", - "unterbrochener mäßiger Regen", - "durchgehend mäßiger Regen", - "unterbrochener starker Regen", - "durchgehend starker Regen", - "leichter gefrierender Regen", - "mäßiger oder starker gefrierender Regen", - "leichter Schneeregen", - "mäßiger oder starker Schneeregen", - # 70 Schnee - "unterbrochener leichter Schneefall oder einzelne Schneeflocken", - "durchgehend leichter Schneefall", - "unterbrochener mäßiger Schneefall", - "durchgehend mäßiger Schneefall", - "unterbrochener starker Schneefall", - "durchgehend starker Schneefall", - "Eisnadeln (Polarschnee)", - "Schneegriesel", - "Schneekristalle", - "Eiskörner (gefrorene Regentropfen)", - # 80 Schauer - "leichter Regenschauer", - "mäßiger oder starker Regenschauer", - "äußerst heftiger Regenschauer", - "leichter Schneeregenschauer", - "mäßiger oder starker Schneeregenschauer", - "leichter Schneeschauer", - "mäßiger oder starker Schneeschauer", - "leichter Graupelschauer", - "mäßiger oder starker Graupelschauer", - "leichter Hagelschauer", - "mäßiger oder starker Hagelschauer", - # 90 Gewitter - "Gewitter in der letzten Stunde, zurzeit leichter Regen", - "Gewitter in der letzten Stunde, zurzeit mäßiger oder starker Regen", - "Gewitter in der letzten Stunde, zurzeit leichter Schneefall/Schneeregen/Graupel/Hagel", - "Gewitter in der letzten Stunde, zurzeit mäßiger oder starker Schneefall/Schneeregen/Graupel/Hagel", - "leichtes oder mäßiges Gewitter mit Regen oder Schnee", - "leichtes oder mäßiges Gewitter mit Graupel oder Hagel", - "starkes Gewitter mit Regen oder Schnee", - "starkes Gewitter mit Sandsturm", - "starkes Gewitter mit Graupel oder Hagel"); -} - -=item DWD_OpenData_Define($$) - - @param hash hash of DWD_OpenData device - @param def module define parameters, will be ignored - - @return undef on success or error message - - description - FHEM module DefFn - -=cut - -sub DWD_OpenData_Define($$) { +sub Define($$) { my ($hash, $def) = @_; my $name = $hash->{NAME}; @@ -234,50 +233,57 @@ sub DWD_OpenData_Define($$) { } # cache timezone attribute - $hash->{'.TZ'} = AttrVal($hash, 'timezone', $hash->{FHEM_TZ}); + $hash->{'.TZ'} = ::AttrVal($hash, 'timezone', $hash->{FHEM_TZ}); - readingsSingleUpdate($hash, 'state', IsDisabled($name)? 'disabled' : 'defined', 1); - InternalTimer(gettimeofday() + 3, 'DWD_OpenData_Timer', $hash, 0); + ::readingsSingleUpdate($hash, 'state', ::IsDisabled($name)? 'disabled' : 'defined', 1); + ::InternalTimer(gettimeofday() + 3, 'DWD_OpenData::Timer', $hash, 0); return undef; } -=item DWD_OpenData_Undef($$) +=head2 Undef($$) - @param hash hash of DWD_OpenData device - @param arg module undefine arguments, will be ignored +FHEM I - description - FHEM module UndefFn ($hash is DWD_OpenData) +=over + +=item * param hash: hash of DWD_OpenData device + +=item * param arg: module undefine arguments, will be ignored + +=back =cut -sub DWD_OpenData_Undef($$) { +sub Undef($$) { my ($hash, $arg) = @_; my $name = $hash->{NAME}; - RemoveInternalTimer($hash); + ::RemoveInternalTimer($hash); return undef; } -=item DWD_OpenData_Undef($) +=head2 Shutdown($) - @param hash hash of DWD_OpenData device +FHEM I - description - FHEM module ShutdownFn ($hash is DWD_OpenData) +=over + +=item * param hash: hash of DWD_OpenData device + +=back =cut -sub DWD_OpenData_Shutdown($) { +sub Shutdown($) { my ($hash) = @_; my $name = $hash->{NAME}; - RemoveInternalTimer($hash); + ::RemoveInternalTimer($hash); if (defined($hash->{".alertsBlockingCall"})) { - BlockingKill($hash->{".alertsBlockingCall"}); + ::BlockingKill($hash->{".alertsBlockingCall"}); } if (defined($hash->{".alertsFile"})) { @@ -289,23 +295,29 @@ sub DWD_OpenData_Shutdown($) { return undef; } -=item DWD_OpenData_Attr(@) +=head2 Attr(@) - @param command "set" or "del" - @param name name of DWD_OpenData device - @param attribute attribute name - @param value attribute value +FHEM I - @return undef on success or error message +=over - description: - FHEM module AttrFn +=item * param command: "set" or "del" + +=item * param name: name of DWD_OpenData device + +=item * param attribute: attribute name + +=item * param value: attribute value + +=item * return C on success or error message + +=back =cut -sub DWD_OpenData_Attr(@) { +sub Attr(@) { my ($command, $name, $attribute, $value) = @_; - my $hash = $defs{$name}; + my $hash = $::defs{$name}; given($command) { when("set") { @@ -314,17 +326,17 @@ sub DWD_OpenData_Attr(@) { # enable/disable polling if ($main::init_done) { if ($value) { - RemoveInternalTimer($hash); - readingsSingleUpdate($hash, 'state', 'disabled', 1); + ::RemoveInternalTimer($hash); + ::readingsSingleUpdate($hash, 'state', 'disabled', 1); } else { - readingsSingleUpdate($hash, 'state', 'defined', 1); - InternalTimer(gettimeofday() + 3, 'DWD_OpenData_Timer', $hash, 0); + ::readingsSingleUpdate($hash, 'state', 'defined', 1); + ::InternalTimer(gettimeofday() + 3, 'DWD_OpenData::Timer', $hash, 0); } } } when("forecastWW2Text") { if (!$value) { - CommandDeleteReading(undef, "$name fc.*wwd"); + ::CommandDeleteReading(undef, "$name fc.*wwd"); } } when("timezone") { @@ -340,11 +352,11 @@ sub DWD_OpenData_Attr(@) { when("del") { given($attribute) { when("disable") { - readingsSingleUpdate($hash, 'state', 'defined', 1); - InternalTimer(gettimeofday() + 3, 'DWD_OpenData_Timer', $hash, 0); + ::readingsSingleUpdate($hash, 'state', 'defined', 1); + ::InternalTimer(gettimeofday() + 3, 'DWD_OpenData::Timer', $hash, 0); } when("forecastWW2Text") { - CommandDeleteReading(undef, "$name fc.*wwd"); + ::CommandDeleteReading(undef, "$name fc.*wwd"); } when("timezone") { $hash->{'.TZ'} = $hash->{FHEM_TZ}; @@ -356,24 +368,23 @@ sub DWD_OpenData_Attr(@) { return undef; } -sub DWD_OpenData_GetForecast($$); -sub DWD_OpenData_GetAlerts($$); -sub DWD_OpenData_UpdateAlerts($$); -sub DWD_OpenData_IsCommuneUnionWarncellId($); +=head2 Get($@) -=item DWD_OpenData_Get($@) +FHEM I - @param hash hash of DWD_OpenData device - @param a array of FHEM command line arguments, min. length 2, a[1] holds get command +=over - @return requested data or error message +=item * param hash: hash of DWD_OpenData device - description: - FHEM module GetFn +=item * param a: array of FHEM command line arguments, min. length 2, a[1] holds get command + +=item * return requested data or error message + +=back =cut -sub DWD_OpenData_Get($@) +sub Get($@) { my ($hash, @a) = @_; my $name = $hash->{NAME}; @@ -383,18 +394,18 @@ sub DWD_OpenData_Get($@) given($command) { when("alerts") { my $warncellId = $a[2]; - $warncellId = AttrVal($name, 'alertArea', undef) if (!defined($warncellId)); + $warncellId = ::AttrVal($name, 'alertArea', undef) if (!defined($warncellId)); if (defined($warncellId)) { - my $communeUnion = DWD_OpenData_IsCommuneUnionWarncellId($warncellId); - if (defined($dwd_alerts_updating[$communeUnion]) && (time() - $dwd_alerts_updating[$communeUnion] < 60)) { + my $communeUnion = IsCommuneUnionWarncellId($warncellId); + if (defined($alerts_updating[$communeUnion]) && (time() - $alerts_updating[$communeUnion] < 60)) { # abort if update is in progress $result = "alerts cache update in progress, please wait and try again"; - } elsif (defined($dwd_alerts_received[$communeUnion]) && (time() - $dwd_alerts_received[$communeUnion] < 900)) { + } elsif (defined($alerts_received[$communeUnion]) && (time() - $alerts_received[$communeUnion] < 900)) { # use cache if not older than 15 minutes - $result = DWD_OpenData_UpdateAlerts($hash, $warncellId); + $result = UpdateAlerts($hash, $warncellId); } else { # update cache if older than 15 minutes - $result = DWD_OpenData_GetAlerts($hash, $warncellId); + $result = GetAlerts($hash, $warncellId); } } else { $result = "warncell id required for $name get $command"; @@ -403,9 +414,9 @@ sub DWD_OpenData_Get($@) when("forecast") { my $station = $a[2]; - $station = AttrVal($name, 'forecastStation', undef) if (!defined($station)); + $station = ::AttrVal($name, 'forecastStation', undef) if (!defined($station)); if (defined($station)) { - $result = DWD_OpenData_GetForecast($hash, $station); + $result = GetForecast($hash, $station); } else { $result = "station code required for $name get $command"; } @@ -416,25 +427,25 @@ sub DWD_OpenData_Get($@) my $option = lc($a[2]); given($option) { when("communeunions") { - $updateMode = DWD_OD_UPDATE_COMMUNEUNIONS; + $updateMode = UPDATE_COMMUNEUNIONS; } when("districts") { - $updateMode = DWD_OD_UPDATE_DISTRICTS; + $updateMode = UPDATE_DISTRICTS; } when("all") { - $updateMode = DWD_OD_UPDATE_ALL; + $updateMode = UPDATE_ALL; } default { return "update mode 'communeUnions', 'districts' or 'all' required for $name get $command"; } } - my $communeUnion = DWD_OpenData_IsCommuneUnionWarncellId($updateMode); - if (defined($dwd_alerts_updating[$communeUnion]) && (time() - $dwd_alerts_updating[$communeUnion] < 60)) { + my $communeUnion = IsCommuneUnionWarncellId($updateMode); + if (defined($alerts_updating[$communeUnion]) && (time() - $alerts_updating[$communeUnion] < 60)) { # abort if update is in progress $result = "alerts cache update in progress, please wait and try again"; } else { # update cache if older than 15 minutes - $result = DWD_OpenData_GetAlerts($hash, $updateMode); + $result = GetAlerts($hash, $updateMode); } } @@ -446,16 +457,75 @@ sub DWD_OpenData_Get($@) return $result; } -=item DWD_OpenData_Timelocal($$) +=head2 Timer($) - @param hash hash of DWD_OpenData device - ta localtime array in device timezone +FHEM I function - @return epoch seconds +=over + +=item * param hash: hash of DWD_OpenData device + +=back =cut -sub DWD_OpenData_Timelocal($@) { +sub Timer($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + ::Log3 $name, 5, "$name: Timer START"; + + my $time = time(); + my ($tSec, $tMin, $tHour, $tMday, $tMon, $tYear, $tWday, $tYday, $tIsdst) = Localtime($hash, $time); + my $actQuarter = int($tMin/15); + + if ($actQuarter == 0) { + my $forecastStation = ::AttrVal($name, 'forecastStation', undef); + if (defined($forecastStation)) { + my $result = GetForecast($hash, $forecastStation); + if (defined($result)) { + ::Log3 $name, 4, "$name: error retrieving forecast: $result"; + } + } + } + + my $warncellId = ::AttrVal($name, 'alertArea', undef); + if (defined($warncellId)) { + # skip update if already in progress + my $communeUnion = IsCommuneUnionWarncellId($warncellId); + if (!defined($alerts_updating[$communeUnion]) || (time() - $alerts_updating[$communeUnion] >= 60)) { + my $result = GetAlerts($hash, $warncellId); + if (defined($result)) { + ::Log3 $name, 4, "$name: error retrieving alerts: $result"; + } + } + } + + # schedule next for 5 seconds past next quarter + my $nextQuarterSeconds = Timelocal($hash, 0, $actQuarter*15, $tHour, $tMday, $tMon, $tYear) + 905; + ::InternalTimer($nextQuarterSeconds, 'DWD_OpenData::Timer', $hash, 0); + + ::Log3 $name, 5, "$name: Timer END"; +} + +=head1 MODULE FUNCTIONS + +=head2 Timelocal($$) + +=over + +=item * param hash: hash of DWD_OpenData device + +=item * param ta: localtime array in device timezone + +=item * return epoch seconds + +=back + +=cut + +sub Timelocal($@) { my ($hash, @ta) = @_; if (defined($hash->{'.TZ'})) { $ENV{"TZ"} = $hash->{'.TZ'}; @@ -469,16 +539,21 @@ sub DWD_OpenData_Timelocal($@) { return $t; } -=item DWD_OpenData_Localtime(@) +=head2 Localtime(@) - @param hash hash of DWD_OpenData device - @param t epoch seconds +=over - @return localtime array in device timezone +=item * param hash: hash of DWD_OpenData device + +=item * param t: epoch seconds + +=item * return localtime array in device timezone + +=back =cut -sub DWD_OpenData_Localtime(@) { +sub Localtime(@) { my ($hash, $t) = @_; if (defined($hash->{'.TZ'})) { $ENV{"TZ"} = $hash->{'.TZ'}; @@ -492,195 +567,259 @@ sub DWD_OpenData_Localtime(@) { return @ta; } -=item DWD_OpenData_FormatDateTimeLocal($$) +=head2 FormatDateTimeLocal($$) - @param hash hash of DWD_OpenData device - @param t epoch seconds +=over - @return date time string with with format "YYYY-MM-DD HH:MM:SS" in device timezone +=item * param hash: hash of DWD_OpenData device + +=item * param t: epoch seconds + +=item * return date time string with with format "YYYY-MM-DD HH:MM:SS" in device timezone + +=back =cut -sub DWD_OpenData_FormatDateTimeLocal($$) { - return strftime('%Y-%m-%d %H:%M:%S', DWD_OpenData_Localtime(@_)); +sub FormatDateTimeLocal($$) { + return strftime('%Y-%m-%d %H:%M:%S', Localtime(@_)); } -=item DWD_OpenData_FormatDateLocal($$) +=head2 FormatDateLocal($$) - @param hash hash of DWD_OpenData device - @param t epoch seconds +=over - @return date string with with format "YYYY-MM-DD" in device timezone +=item * param hash: hash of DWD_OpenData device + +=item * param t: epoch seconds + +=item * return date string with with format "YYYY-MM-DD" in device timezone + +=back =cut -sub DWD_OpenData_FormatDateLocal($$) { - return strftime('%Y-%m-%d', DWD_OpenData_Localtime(@_)); +sub FormatDateLocal($$) { + return strftime('%Y-%m-%d', Localtime(@_)); } -=item DWD_OpenData_FormatTimeLocal($$) +=head2 FormatTimeLocal($$) - @param hash hash of DWD_OpenData device - @param t epoch seconds +=over - @return time string with format "HH:MM" in device timezone +=item * param hash: hash of DWD_OpenData device + +=item * param t: epoch seconds + +=item * return time string with format "HH:MM" in device timezone + +=back =cut -sub DWD_OpenData_FormatTimeLocal($$) { - return strftime('%H:%M', DWD_OpenData_Localtime(@_)); +sub FormatTimeLocal($$) { + return strftime('%H:%M', Localtime(@_)); } -=item DWD_OpenData_FormatWeekdayLocal($$) +=head2 FormatWeekdayLocal($$) - @param hash hash of DWD_OpenData device - @param t epoch seconds +=over - @return abbreviated weekday name in device timezone +=item * param hash: hash of DWD_OpenData device + +=item * param t: epoch seconds + +=item * return abbreviated weekday name in device timezone + +=back =cut -sub DWD_OpenData_FormatWeekdayLocal($$) { - return strftime('%a', DWD_OpenData_Localtime(@_)); +sub FormatWeekdayLocal($$) { + return strftime('%a', Localtime(@_)); } -=item DWD_OpenData_ParseDateTimeLocal($$) +=head2 ParseDateTimeLocal($$) - @param hash hash of DWD_OpenData device - s date string with format "YYYY-MM-DD HH:MM:SS" in device timezone +=over - @return epoch seconds or undef on error +=item * param hash: hash of DWD_OpenData device + +=item * param s: date string with format "YYYY-MM-DD HH:MM:SS" in device timezone + +=item * return epoch seconds or C on error + +=back =cut -sub DWD_OpenData_ParseDateTimeLocal($$) { +sub ParseDateTimeLocal($$) { my ($hash, $s) = @_; my $t; - eval { $t = DWD_OpenData_Timelocal($hash, strptime($s, '%Y-%m-%d %H:%M:%S')) }; + eval { $t = Timelocal($hash, ::strptime($s, '%Y-%m-%d %H:%M:%S')) }; return $t; } -=item DWD_OpenData_ParseDateLocal($$) +=head2 ParseDateLocal($$) - @param hash hash of DWD_OpenData device - s date string with format "YYYY-MM-DD" in device timezone +=over - @return epoch seconds or undef on error +=item * param hash: hash of DWD_OpenData device + +=item * param s: date string with format "YYYY-MM-DD" in device timezone + +=item * return epoch seconds or C on error + +=back =cut -sub DWD_OpenData_ParseDateLocal($$) { +sub ParseDateLocal($$) { my ($hash, $s) = @_; my $t; - eval { $t = DWD_OpenData_Timelocal($hash, strptime($s, '%Y-%m-%d')) }; + eval { $t = Timelocal($hash, ::strptime($s, '%Y-%m-%d')) }; return $t; } -=item DWD_OpenData_ParseCAPTime($) +=head2 ParseCAPTime($) - @param t time string with format "YYYY-MM-DDThh:mm:ssZZZ:ZZ" +=over - @return epoch seconds +=item * param s: time string with format "YYYY-MM-DDThh:mm:ssZZZ:ZZ" + +=item * return epoch seconds + +=back =cut -sub DWD_OpenData_ParseCAPTime($) { +sub ParseCAPTime($) { my ($s) = @_; $s =~ s|(.+):|$1|; # remove colon from time zone offset - #Log 1, "DWD_OpenData_ParseCAPTime: " . $s; + #Log 1, "ParseCAPTime: " . $s; return Time::Piece->strptime($s, '%Y-%m-%dT%H:%M:%S%z')->epoch; } -=item DWD_OpenData_IsCommuneUnionWarncellId($) +=head2 ParseKMLTime($) - @param $warncellId numeric wanrcell id +=over - @return true if warncell id belongs to commune union group +=item * param s: time string with format "YYYY-MM-DDThh:mm:ss.000Z" + +=item * return epoch seconds + +=back =cut -sub DWD_OpenData_IsCommuneUnionWarncellId($) { - my ($warncellId) = @_; - return int($warncellId/100000000) == 5 || int($warncellId/100000000) == 8 - || $warncellId == DWD_OD_UPDATE_COMMUNEUNIONS || $warncellId == DWD_OD_UPDATE_ALL? 1 : 0; +sub ParseKMLTime($) { + my ($s) = @_; + $s =~ s|(.+)\.000Z|$1|; # remove milliseconds and timezone + return Time::Piece->strptime($s, '%Y-%m-%dT%H:%M:%S')->epoch; } -=item DWD_OpenData_RotateForecast($$;$) +=head2 IsCommuneUnionWarncellId($) - @param $hash hash of DWD_OpenData device - @param $station station name, string - @param $today epoch of today 00:00, optional +=over - @return count of available forecast days +=item * param warncellId: numeric wanrcell id + +=item * return true if warncell id belongs to commune union group + +=back =cut -sub DWD_OpenData_RotateForecast($$;$) +sub IsCommuneUnionWarncellId($) { + my ($warncellId) = @_; + return int($warncellId/100000000) == 5 || int($warncellId/100000000) == 8 + || $warncellId == UPDATE_COMMUNEUNIONS || $warncellId == UPDATE_ALL? 1 : 0; +} + +=head2 RotateForecast($$;$) + +=over + +=item * param hash: hash of DWD_OpenData device + +=item * param station: station name, string + +=item * param today: epoch of today 00:00, optional + +=item * return count of available forecast days + +=back + +=cut + +sub RotateForecast($$;$) { my ($hash, $station, $today) = @_; my $name = $hash->{NAME}; my $daysAvailable = 0; - while (defined(ReadingsVal($name, 'fc'.$daysAvailable.'_date', undef))) { + while (defined(::ReadingsVal($name, 'fc'.$daysAvailable.'_date', undef))) { $daysAvailable++; } - #Log3 $name, 5, "$name: A $daysAvailable"; + #::Log3 $name, 5, "$name: A $daysAvailable"; - my $oT = ReadingsVal($name, 'fc0_date', undef); - my $oldToday = defined($oT)? DWD_OpenData_ParseDateLocal($hash, $oT) : undef; + my $oT = ::ReadingsVal($name, 'fc0_date', undef); + my $oldToday = defined($oT)? ParseDateLocal($hash, $oT) : undef; - my $stationChanged = ReadingsVal($name, 'fc_station', '') ne $station; + my $stationChanged = ::ReadingsVal($name, 'fc_station', '') ne $station; if ($stationChanged) { - # different station, delete existing readings - CommandDeleteReading(undef, "$name fc.*"); + # different station, delete all existing readings + ::CommandDeleteReading(undef, "$name fc.*"); $daysAvailable = 0; } elsif (defined($oldToday)) { # same station, shift existing readings if (!defined($today)) { my $time = time(); - my ($tSec, $tMin, $tHour, $tMday, $tMon, $tYear, $tWday, $tYday, $tIsdst) = DWD_OpenData_Localtime($hash, $time); - $today = DWD_OpenData_Timelocal($hash, 0, 0, 0, $tMday, $tMon, $tYear); + my ($tSec, $tMin, $tHour, $tMday, $tMon, $tYear, $tWday, $tYday, $tIsdst) = Localtime($hash, $time); + $today = Timelocal($hash, 0, 0, 0, $tMday, $tMon, $tYear); } - my $daysForward = sprintf("%.0f", $today - $oldToday); # Perl equivalent for round() + my $daysForward = sprintf("%0.0f", $today - $oldToday); # round() if ($daysForward > 0) { # different day if ($daysForward < $daysAvailable) { - # shift readings forward by days - my @shiftProperties = ( 'date' ); - foreach my $property (@dwd_dayProperties) { - push(@shiftProperties, $property); + my @shiftProperties = ( 'date', 'weekday' ); + my $forecastResolution = ::AttrVal($name, 'forecastResolution', 6); + while (my($property, $period) = each %forecastPropertyPeriods) { + if ($period == 24) { + push(@shiftProperties, $property); + } else { + for (my $s=0; $s<24/$forecastResolution; $s++) { + push(@shiftProperties, $s.'_'.$property); + } + } } - for (my $s=0; $s<7; $s++) { + for (my $s=0; $s<24/$forecastResolution; $s++) { push(@shiftProperties, $s.'_time'); push(@shiftProperties, $s.'_wwd'); } - foreach my $property (@dwd_hourProperties) { - for (my $s=0; $s<7; $s++) { - push(@shiftProperties, $s.'_'.$property); - } - } + # shift readings forward by days for (my $d=0; $d<($daysAvailable - $daysForward); $d++) { my $sourcePrefix = 'fc'.($daysForward + $d).'_'; my $destinationPrefix = 'fc'.$d.'_'; foreach my $property (@shiftProperties) { - my $value = ReadingsVal($name, $sourcePrefix.$property, undef); + my $value = ::ReadingsVal($name, $sourcePrefix.$property, undef); if (defined($value)) { - readingsBulkUpdate($hash, $destinationPrefix.$property, $value); + ::readingsBulkUpdate($hash, $destinationPrefix.$property, $value); } else { - CommandDeleteReading(undef, $destinationPrefix.$property); + ::CommandDeleteReading(undef, $destinationPrefix.$property); } } } # delete existing readings of all days that have not been written for (my $d=($daysAvailable - $daysForward); $d<$daysAvailable; $d++) { - CommandDeleteReading(undef, "$name fc".$d."_.*"); + ::CommandDeleteReading(undef, "$name fc".$d."_.*"); } $daysAvailable -= $daysForward; } else { # nothing to shift, delete existing readings - CommandDeleteReading(undef, "$name fc.*"); + ::CommandDeleteReading(undef, "$name fc.*"); $daysAvailable = 0; } } @@ -689,76 +828,73 @@ sub DWD_OpenData_RotateForecast($$;$) return $daysAvailable; } -sub DWD_OpenData_ProcessForecast($$$); +sub ProcessForecast($$$); -=item DWD_OpenData_GetForecast($$) +=head2 GetForecast($$) - @param $hash hash of DWD_OpenData device - @param $station station name, string +=over + +=item * param hash: hash of DWD_OpenData device + +=item * param station: station name, string + +=back =cut -sub DWD_OpenData_GetForecast($$) +sub GetForecast($$) { my ($hash, $station) = @_; my $name = $hash->{NAME}; - if (!IsDisabled($name)) { - # test perl module Text::CSV_XS + if (!::IsDisabled($name)) { + # test if XML module is available eval { - require Text::CSV_XS; # 0.40 or higher required - Text::CSV_XS->new(); + require XML::LibXML; }; if ($@) { - my $message = "$name: Perl module Text::CSV_XS not found, see commandref for details how to fix"; - return $message; - } - my $textCsvXsVersion = $Text::CSV_XS::VERSION; - if ($textCsvXsVersion < 0.40) { - my $message = "$name: Perl module Text::CSV_XS has incompatible version $textCsvXsVersion, see commandref for details how to fix"; - return $message; + return "$name: Perl module XML::LibXML not found, see commandref for details how to fix"; } - # station name must be 5 chars, extend - my $fileName = $station; - while (length($fileName) < 5) { - $fileName .= '_'; - } - $fileName .= '-MOSMIX.csv'; - - # @TODO move DWD_OpenData_RotateForecast + # @TODO move RotateForecast # get forecast for station from DWD server - readingsSingleUpdate($hash, 'state', 'fetching', 0); - my $url = 'https://opendata.dwd.de/weather/local_forecasts/poi/' . $fileName; + ::readingsSingleUpdate($hash, 'state', 'fetching', 0); + my $url = 'https://opendata.dwd.de/weather/local_forecasts/mos/MOSMIX_L/single_stations/' . $station . '/kml/MOSMIX_L_LATEST_' . $station . '.kmz '; my $param = { url => $url, method => "GET", timeout => 10, - callback => \&DWD_OpenData_ProcessForecast, + callback => \&ProcessForecast, hash => $hash, station => $station }; - Log3 $name, 5, "$name: DWD_OpenData_GetForecast START (PID $$): $url"; - HttpUtils_NonblockingGet($param); + ::Log3 $name, 5, "$name: GetForecast START (PID $$): $url"; + ::HttpUtils_NonblockingGet($param); - Log3 $name, 5, "$name: DWD_OpenData_GetForecast END"; + ::Log3 $name, 5, "$name: GetForecast END"; } else { return "disabled"; } } -=item DWD_OpenData_ProcessForecast($$$) +=head2 ProcessForecast($$$) - @param param parameter hash from call to HttpUtils_NonblockingGet - @param httpError nothing or HTTP error string - @param fileContent data retrieved from URL +=over - @return undef on success or error message +=item * param param: parameter hash from call to HttpUtils_NonblockingGet + +=item * param httpError: nothing or HTTP error string + +=item * param fileContent: data retrieved from URL + +=item * return C on success or error message + +=back =cut -sub DWD_OpenData_ProcessForecast($$$) +sub ProcessForecast($$$) { my ($param, $httpError, $fileContent) = @_; my $hash = $param->{hash}; @@ -767,14 +903,14 @@ sub DWD_OpenData_ProcessForecast($$$) my $code = $param->{code}; my $station = $param->{station}; - Log3 $name, 5, "$name: DWD_OpenData_ProcessForecast START"; + ::Log3 $name, 5, "$name: ProcessForecast START"; # preprocess existing readings - readingsBeginUpdate($hash); + ::readingsBeginUpdate($hash); my $time = time(); - my ($tSec, $tMin, $tHour, $tMday, $tMon, $tYear, $tWday, $tYday, $tIsdst) = DWD_OpenData_Localtime($hash, $time); - my $today = DWD_OpenData_Timelocal($hash, 0, 0, 0, $tMday, $tMon, $tYear); - my $daysAvailable = DWD_OpenData_RotateForecast($hash, $station, $today); + my ($tSec, $tMin, $tHour, $tMday, $tMon, $tYear, $tWday, $tYday, $tIsdst) = Localtime($hash, $time); + my $today = Timelocal($hash, 0, 0, 0, $tMday, $tMon, $tYear); + my $daysAvailable = RotateForecast($hash, $station, $today); my $relativeDay = 0; eval { @@ -788,131 +924,190 @@ sub DWD_OpenData_ProcessForecast($$$) die "no data retrieved from URL '$url'"; } - #Log3 $name, 5, "$name: DWD_OpenData_ProcessForecast: $code >$fileContent<"; - - # create memory mapped file form received data and parse as CSV - readingsBulkUpdate($hash, 'state', 'parsing', 0); - my $csv = Text::CSV_XS->new({ sep_char => ';' }); - if (!defined($csv)) { - die "error creating CSV parser: ".Text::CSV_XS->error_diag(); - } - open my $fileHandle, '<', \$fileContent; - - # parse file content - my @columnNames = @{$csv->getline($fileHandle)}; - if (!@columnNames) { - die "error parsing header line"; - } - - #foreach my $columnName (@columnNames) { - # Log3 $name, 5, "$name: DWD_OpenData_ProcessForecast cn: $columnName"; - #} - - $csv->column_names(@columnNames); - my @aoh; - while (my $row = $csv->getline_hr($fileHandle)) { - #Log3 $name, 5, "$name: DWD_OpenData_ProcessForecast r: $row"; - push(@aoh, $row); - } - - # abort if no data - if (scalar(@aoh) == 0) { - die "file does not contain valid data"; - } + ::Log3 $name, 5, "$name: ProcessForecast: data received, $daysAvailable days currently exist with readings"; # prepare processing - readingsBulkUpdate($hash, 'state', 'processing'); - my $forecastWW2Text = AttrVal($name, 'forecastWW2Text', 0); - my $forecastDays = AttrVal($name, 'forecastDays', 14); - my $forecastResolution = AttrVal($name, 'forecastResolution', 6); - my $forecastProperties = AttrVal($name, 'forecastProperties', undef); + ::readingsBulkUpdate($hash, 'state', 'processing'); + my $forecastWW2Text = ::AttrVal($name, 'forecastWW2Text', 0); + my $forecastDays = ::AttrVal($name, 'forecastDays', 6); + my $forecastResolution = ::AttrVal($name, 'forecastResolution', 6); + my $forecastProperties = ::AttrVal($name, 'forecastProperties', undef); my @properties = split(',', $forecastProperties) if (defined($forecastProperties)); - my @selectedDayProperties; - my @selectedHourProperties; + my %selectedProperties; if (!@properties) { - # no selection: default to all properties - @selectedDayProperties = @dwd_dayProperties; - @selectedHourProperties = @dwd_hourProperties; + # no selection: use defaults + %selectedProperties = %forecastDefaultProperties; } else { - # split selected properties in day and hour properties + # use selected properties foreach my $property (@properties) { - if (grep(/^$property$/, @dwd_dayProperties)) { - push(@selectedDayProperties, $property); - } else { - push(@selectedHourProperties, $property); - } + $property =~ s/^\s+|\s+$//g; # trim + $selectedProperties{$property} = 1; } } - readingsBulkUpdate($hash, "fc_station", $station); - readingsBulkUpdate($hash, "fc_copyright", "Datenbasis: Deutscher Wetterdienst"); + # create memory mapped file from received data and unzip + open my $zipFileHandle, '<', \$fileContent; + my @xmlStrings; + unzip($zipFileHandle => \@xmlStrings, MultiStream => 1) or die "unzip failed: $UnzipError\n"; - # process received data: row 0 holds physical units, row 1 holds comment, row 2 hold first data - my $rowIndex = 0; - my $reportHour; - foreach my $row (@aoh) - { - if ($rowIndex == 0) { - # 1st column of row 0 holds the hour and timezone the report was created - $reportHour = (split(' ', $row->{forecast}, 2))[1]; - $reportHour =~ s/UTC/GMT/g; + ::readingsBulkUpdate($hash, "fc_station", $station); + + # parse XML strings (files from zip) + foreach my $xmlString (@xmlStrings) { + if (substr(${$xmlString}, 0, 2) eq 'PK') { + # empty string, skip + next; } - elsif ($rowIndex >= 2) { - if ($rowIndex == 2) { - #my $reportTime = Time::Piece->strptime($row->{forecast}.' '.$reportHour, '%d.%m.%y %H %Z'); - #readingsBulkUpdate($hash, "fc_time", DWD_OpenData_FormatDateTimeLocal($hash, $reportTime->epoch)); - readingsBulkUpdate($hash, "fc_time", DWD_OpenData_FormatDateTimeLocal($hash, $time)); - } - # analyse date relation between forecast and today - my $forecastTime = Time::Piece->strptime($row->{forecast}.' '.$row->{parameter}.' GMT', '%d.%m.%y %H:%M %Z'); - my ($fcSec, $fcMin, $fcHour, $fcMday, $fcMon, $fcYear, $fcWday, $fcYday, $fcIsdst) = DWD_OpenData_Localtime($hash, $forecastTime->epoch); - my $forecastDate = DWD_OpenData_Timelocal($hash, 0, 0, 0, $fcMday, $fcMon, $fcYear); - my $nextRelativeDay = sprintf("%.0f", ($forecastDate - $today)/(24*60*60)); # Perl equivalent for round() - if ($nextRelativeDay > $forecastDays) { - # max. number of days processed, done - last; - } - if ($nextRelativeDay < 0) { - # forecast is older than today, skip - next; - } - $relativeDay = $nextRelativeDay; - # write data - my $destinationPrefix = 'fc'.$relativeDay.'_'; - #Log3 $name, 5, "$name: $row->{forecast} $row->{parameter} -> $forecastTime -> $fcMday.$fcMon.$fcYear $fcHour:$fcMin -> $forecastDate -> $destinationPrefix"; - readingsBulkUpdate($hash, $destinationPrefix.'date', DWD_OpenData_FormatDateLocal($hash, $forecastTime->epoch)); - readingsBulkUpdate($hash, $destinationPrefix.'weekday', DWD_OpenData_FormatWeekdayLocal($hash, $forecastTime->epoch)); - foreach my $property (@selectedDayProperties) { - my $value = $row->{$property}; - $value = undef if ($value eq "---"); - if (defined($value)) { - $value =~ s/,/./g; - #Log3 $name, 5, "$name: $property = $value"; - readingsBulkUpdate($hash, $destinationPrefix.$property, $value) if (defined($value)); - } - } - #Log3 $name, 5, "$name: $rowIndex/$today/$row->{forecast}/$relativeDay/$row->{parameter}"; - my $hourUTC = (split(':', $row->{parameter}))[0]; - if ($forecastResolution == 3 || ($hourUTC eq "00" || $hourUTC eq "06" || $hourUTC eq "12" || $hourUTC eq "18")) { - #Log3 $name, 5, "$name: $rowIndex/$today/$row->{forecast}/$relativeDay/$row->{parameter}/$fcHour"; - $destinationPrefix .= int($fcHour/$forecastResolution).'_'; - readingsBulkUpdate($hash, $destinationPrefix.'time', DWD_OpenData_FormatTimeLocal($hash, $forecastTime->epoch)); - foreach my $property (@selectedHourProperties) { - my $label = $property; - $label =~ s/^RRp/RR%/g; - my $value = $row->{$label}; - $value = undef if (defined($value) && ($value eq "---")); - if (defined($value)) { - $value =~ s/,/./g; - readingsBulkUpdate($hash, $destinationPrefix.$property, $value); - if ($forecastWW2Text && ($property eq 'ww') && length($value) > 0) { - readingsBulkUpdate($hash, $destinationPrefix.'wwd', $dwd_wwdText[$value]); + + # parse XML string + ::Log3 $name, 5, "$name: ProcessForecast: parsing XML document"; + my $dom = XML::LibXML->load_xml(string => $xmlString); + if (!$dom) { + die "parsing XML failed"; + } + + ::Log3 $name, 5, "$name: ProcessForecast: extracting data"; + + # extract header + my @timestamps; + my $defaultUndefSign = '-'; + my $productDefinitionNodeList = $dom->getElementsByLocalName('ProductDefinition'); + if ($productDefinitionNodeList->size()) { + my $productDefinitionNode = $productDefinitionNodeList->get_node(1); + foreach my $productDefinitionChildNode ($productDefinitionNode->nonBlankChildNodes()) { + if ($productDefinitionChildNode->nodeName() eq 'dwd:Issuer') { + my $issuer = $productDefinitionChildNode->textContent(); + ::readingsBulkUpdate($hash, "fc_copyright", "Datenbasis: $issuer"); + } elsif ($productDefinitionChildNode->nodeName() eq 'dwd:IssueTime') { + my $issueTime = $productDefinitionChildNode->textContent(); + # ignore issue time, use now + ::readingsBulkUpdate($hash, "fc_time", FormatDateTimeLocal($hash, $time)); + } elsif ($productDefinitionChildNode->nodeName() eq 'dwd:ForecastTimeSteps') { + foreach my $forecastTimeStepsChildNode ($productDefinitionChildNode->nonBlankChildNodes()) { + if ($forecastTimeStepsChildNode->nodeName() eq 'dwd:TimeStep') { + my $forecastTimeSteps = $forecastTimeStepsChildNode->textContent(); + push(@timestamps, ParseKMLTime($forecastTimeSteps)); + } + } + } elsif ($productDefinitionChildNode->nodeName() eq 'dwd:FormatCfg') { + foreach my $formatCfgChildNode ($productDefinitionChildNode->nonBlankChildNodes()) { + if ($formatCfgChildNode->nodeName() eq 'dwd:DefaultUndefSign') { + $defaultUndefSign = $formatCfgChildNode->textContent(); + } + } + } + } + } + + # extract data + my %properties; + my $placemarkNodeList = $dom->getElementsByLocalName('Placemark'); + if ($placemarkNodeList->size()) { + my $placemarkNode = $placemarkNodeList->get_node(1); + foreach my $placemarkChildNode ($placemarkNode->nonBlankChildNodes()) { + if ($placemarkChildNode->nodeName() eq 'kml:description') { + my $description = $placemarkChildNode->textContent(); + ::readingsBulkUpdate($hash, "fc_description", encode('UTF-8', $description)); + } elsif ($placemarkChildNode->nodeName() eq 'kml:ExtendedData') { + foreach my $extendedDataChildNode ($placemarkChildNode->nonBlankChildNodes()) { + if ($extendedDataChildNode->nodeName() eq 'dwd:Forecast') { + my $elementName = $extendedDataChildNode->getAttribute('dwd:elementName'); + # convert some elements names for backward compatibility + my $alias = $forecastPropertyAliases{$elementName}; + if (defined($alias)) { $elementName = $alias }; + my $selectedProperty = $selectedProperties{$elementName}; + if (defined($selectedProperty)) { + my $textContent = $extendedDataChildNode->nonBlankChildNodes()->get_node(1)->textContent(); + $textContent =~ s/^\s+|\s+$//g; # trim outside + $textContent =~ s/\s+/ /g; # trim inside + my @values = split(' ',$textContent); + $properties{$elementName} = \@values; + } + } + } + } elsif ($placemarkChildNode->nodeName() eq 'kml:Point') { + my $coordinates = $placemarkChildNode->nonBlankChildNodes()->get_node(1)->textContent(); + ::readingsBulkUpdate($hash, "fc_coordinates", $coordinates); + } + } + } + + ::Log3 $name, 5, "$name: ProcessForecast: creating readings"; + + # create readings + my $lastDayPrefix = ''; + for my $i (0 .. $#timestamps) { + # analyse date relation between forecast and today + my $forecastTime = $timestamps[$i]; + my ($fcSec, $fcMin, $fcHour, $fcMday, $fcMon, $fcYear, $fcWday, $fcYday, $fcIsdst) = Localtime($hash, $forecastTime); + my $forecastDate = Timelocal($hash, 0, 0, 0, $fcMday, $fcMon, $fcYear); + $relativeDay = sprintf("%.0f", ($forecastDate - $today)/(24*60*60)); # Perl equivalent for round() + if ($relativeDay > $forecastDays) { + # max. number of days processed, done + last; + } + if ($relativeDay < 0) { + # forecast is older than today, skip + next; + } + # write data + my $dayPrefix = 'fc'.$relativeDay.'_'; + if ($dayPrefix ne $lastDayPrefix) { + ::readingsBulkUpdate($hash, $dayPrefix.'date', FormatDateLocal($hash, $forecastTime)); + ::readingsBulkUpdate($hash, $dayPrefix.'weekday', FormatWeekdayLocal($hash, $forecastTime)); + $lastDayPrefix = $dayPrefix; + } + # some values are only available every 3, 6 or 12 hours relative to 00:00 UTC + my $hourPrefix = undef; + my $fcHourUTC = (gmtime($forecastTime))[2]; + #::Log3 $name, 5, "$name: fcHourUTC $fcHourUTC"; + if ($fcHourUTC%$forecastResolution == 0) { + $hourPrefix = int($fcHour/$forecastResolution).'_'; + #::Log3 $name, 5, "$name: hourPrefix $hourPrefix"; + ::readingsBulkUpdate($hash, $dayPrefix.$hourPrefix.'time', FormatTimeLocal($hash, $forecastTime)); + } + while (my($property, $values) = each %properties) { + #::Log3 $name, 5, "$name: $property vs=" . scalar(@$values) . " ts=" . $#timestamps . " -> " . $values->[$i]; + if (defined($values->[$i])) { + my $value = $values->[$i]; + if ($value ne $defaultUndefSign) { + $value =~ s/,/./g; # decimal point + my $forecastPropertyType = $forecastPropertyTypes{$property}; + if (defined($forecastPropertyType)) { + if ($forecastPropertyType == 1) { + $value -= 273.15; # K -> °C + if (length($value) > 6) { + $value = sprintf('%0.2f', $value); # round to compensate floating point granularity + } + } + elsif ($forecastPropertyType == 2) { + $value = sprintf('%0.0f', $value); # round() + if ($forecastWW2Text && ($property eq 'ww') && defined($hourPrefix) && length($value) > 0) { + ::readingsBulkUpdate($hash, $dayPrefix.$hourPrefix.'wwd', $wwdText[$value]); + } + } + elsif ($forecastPropertyType == 3) { + $value *= 3.6; # m/s -> km/h + $value = sprintf('%0.0f', $value); # round() + } + elsif ($forecastPropertyType == 4) { + $value /= 100; # Pa -> hPa + $value = sprintf('%0.1f', $value); # round(1) + } + } + #::Log3 $name, 5, "$name: $fcHour $dayPrefix $hourPrefix | $property -> $value | $forecastPropertyType"; + my $forecastPropertyPeriod = $forecastPropertyPeriods{$property}; + if ($forecastPropertyPeriod == 24) { + # day property + ::readingsBulkUpdate($hash, $dayPrefix.$property, $value); + } elsif (defined($hourPrefix)) { + # hour property + ::readingsBulkUpdate($hash, $dayPrefix.$hourPrefix.$property, $value); } } } } } - $rowIndex++; } }; @@ -920,48 +1115,51 @@ sub DWD_OpenData_ProcessForecast($$$) if ($@) { my @parts = split(' at ', $@); if (@parts) { - readingsBulkUpdate($hash, 'state', "forecast error: $parts[0]"); - Log3 $name, 4, "$name: DWD_OpenData_ProcessForecast error: $parts[0]"; + ::readingsBulkUpdate($hash, 'state', "forecast error: $parts[0]"); + ::Log3 $name, 4, "$name: ProcessForecast error: $parts[0]"; } else { - readingsBulkUpdate($hash, 'state', "forecast error: $@"); - Log3 $name, 4, "$name: DWD_OpenData_ProcessForecast error: $@"; + ::readingsBulkUpdate($hash, 'state', "forecast error: $@"); + ::Log3 $name, 4, "$name: ProcessForecast error: $@"; } - readingsEndUpdate($hash, 1); + ::readingsEndUpdate($hash, 1); return @parts? $parts[0] : $@; } # delete existing readings of all days that have not been written - #Log3 $name, 5, "$name: B $relativeDay $daysAvailable"; - for (my $d=($relativeDay + 1); $d<$daysAvailable; $d++) { - CommandDeleteReading(undef, "$name fc".$d."_.*"); + if ($daysAvailable > $relativeDay + 1) { + ::Log3 $name, 5, "$name: deleting days with index " . ($relativeDay + 1) . " to " . ($daysAvailable - 1); + for (my $d=($relativeDay + 1); $d<$daysAvailable; $d++) { + ::CommandDeleteReading(undef, "$name fc".$d."_.*"); + } } - readingsBulkUpdate($hash, 'state', 'forecast updated'); - readingsEndUpdate($hash, 1); + ::readingsBulkUpdate($hash, 'state', 'forecast updated'); + ::readingsEndUpdate($hash, 1); - Log3 $name, 5, "$name: DWD_OpenData_ProcessForecast END"; + ::Log3 $name, 5, "$name: ProcessForecast END"; return undef; } -sub DWD_OpenData_GetAlertsBlockingFn($); -sub DWD_OpenData_GetAlertsFinishFn(;$$$$); -sub DWD_OpenData_GetAlertsAbortFn($); +=head2 GetAlerts($$) -=item DWD_OpenData_GetAlerts($$) +=over - @param $hash hash of DWD_OpenData device - @param $warncellId id of warncell, number +=item * param hash: hash of DWD_OpenData device + +=item * param warncellId: numeric id of warncell, may also be C, C or C + +=back =cut -sub DWD_OpenData_GetAlerts($$) +sub GetAlerts($$) { my ($hash, $warncellId) = @_; my $name = $hash->{NAME}; - if (!IsDisabled($name)) { - Log3 $name, 5, "$name: DWD_OpenData_GetAlerts START (PID $$)"; + if (!::IsDisabled($name)) { + ::Log3 $name, 5, "$name: GetAlerts START (PID $$)"; # test if XML module is available eval { @@ -974,7 +1172,7 @@ sub DWD_OpenData_GetAlerts($$) # @TODO delete expired alerts? # download, unzip and parse using BlockingCall - my $communeUnion = DWD_OpenData_IsCommuneUnionWarncellId($warncellId); + my $communeUnion = IsCommuneUnionWarncellId($warncellId); if (defined($hash->{".alertsFile".$communeUnion})) { # delete old temp file close($hash->{".alertsFileHandle".$communeUnion}); @@ -984,47 +1182,51 @@ sub DWD_OpenData_GetAlerts($$) $hash->{".warncellId"} = $warncellId; if (defined($hash->{".alertsBlockingCall".$communeUnion})) { # kill old blocking call - BlockingKill($hash->{".alertsBlockingCall".$communeUnion}); + ::BlockingKill($hash->{".alertsBlockingCall".$communeUnion}); } - $hash->{".alertsBlockingCall".$communeUnion} = BlockingCall("DWD_OpenData_GetAlertsBlockingFn", $hash, "DWD_OpenData_GetAlertsFinishFn", 60, "DWD_OpenData_GetAlertsAbortFn", $hash); + $hash->{".alertsBlockingCall".$communeUnion} = ::BlockingCall("DWD_OpenData::GetAlertsStart", $hash, "DWD_OpenData::GetAlertsFinish", 60, "DWD_OpenData::GetAlertsAbort", $hash); - $dwd_alerts_updating[$communeUnion] = time(); + $alerts_updating[$communeUnion] = time(); - readingsSingleUpdate($hash, 'state', 'updating alerts cache', 1); + ::readingsSingleUpdate($hash, 'state', 'updating alerts cache', 1); - Log3 $name, 5, "$name: DWD_OpenData_GetAlerts END"; + ::Log3 $name, 5, "$name: GetAlerts END"; return undef; } else { return "disabled"; } } -sub DWD_OpenData_ProcessAlerts($$$); +sub ProcessAlerts($$$); -=item DWD_OpenData_GetAlertsBlockingFn($) +=head2 GetAlertsStart($) - BlockingStart BlockingFn callback +BlockingCall I callback - @param $hash hash of DWD_OpenData device +=over - @return result for DWD_OpenData_GetAlertsFinishFn +=item * param hash: hash of DWD_OpenData device - ATTENTION: This method is executed in a different process than FHEM. - The device hash is from the time of the process initiation. - Any changes to the device hash or readings are not visible - in FHEM. +=item * return result required by function L + +=back + +ATTENTION: This method is executed in a different process than FHEM. + The device hash is from the time of the process initiation. + Any changes to the device hash or readings are not visible + in FHEM. =cut -sub DWD_OpenData_GetAlertsBlockingFn($) +sub GetAlertsStart($) { my ($hash) = @_; my $name = $hash->{NAME}; my $warncellId = $hash->{".warncellId"}; # get communion (5, 8) or district (1, 9) alerts for Germany from DWD server - my $communeUnion = DWD_OpenData_IsCommuneUnionWarncellId($warncellId); - my $alertLanguage = AttrVal($name, 'alertLanguage', 'DE'); + my $communeUnion = IsCommuneUnionWarncellId($warncellId); + my $alertLanguage = ::AttrVal($name, 'alertLanguage', 'DE'); my $url = 'https://opendata.dwd.de/weather/alerts/cap/'.($communeUnion? 'COMMUNEUNION' : 'DISTRICT').'_CELLS_STAT/Z_CAP_C_EDZW_LATEST_PVW_STATUS_PREMIUMCELLS_'.($communeUnion? 'COMMUNEUNION' : 'DISTRICT').'_'.$alertLanguage.'.zip'; my $param = { url => $url, @@ -1033,31 +1235,35 @@ sub DWD_OpenData_GetAlertsBlockingFn($) hash => $hash, warncellId => $warncellId }; - Log3 $name, 5, "$name: DWD_OpenData_GetAlertsBlockingFn START (PID $$): $url"; - my ($httpError, $fileContent) = HttpUtils_BlockingGet($param); + ::Log3 $name, 5, "$name: GetAlertsStart START (PID $$): $url"; + my ($httpError, $fileContent) = ::HttpUtils_BlockingGet($param); # process retrieved data - my $result = DWD_OpenData_ProcessAlerts($param, $httpError, $fileContent); + my $result = ProcessAlerts($param, $httpError, $fileContent); - Log3 $name, 5, "$name: DWD_OpenData_GetAlertsBlockingFn END"; + ::Log3 $name, 5, "$name: GetAlertsStart END"; return $result; } -=item DWD_OpenData_ProcessAlerts($$$) +=head2 ProcessAlerts($$$) - @param $hash hash of DWD_OpenData device +=over - @return result for DWD_OpenData_GetAlertsFinishFn +=item * param hash: hash of DWD_OpenData device - ATTENTION: This method is executed in a different process than FHEM. - The device hash is from the time of the process initiation. - Any changes to the device hash or readings are not visible - in FHEM. +=item * return result required by function L + +=back + +ATTENTION: This method is executed in a different process than FHEM. + The device hash is from the time of the process initiation. + Any changes to the device hash or readings are not visible + in FHEM. =cut -sub DWD_OpenData_ProcessAlerts($$$) +sub ProcessAlerts($$$) { my ($param, $httpError, $fileContent) = @_; my $time = time(); @@ -1067,7 +1273,7 @@ sub DWD_OpenData_ProcessAlerts($$$) my $code = $param->{code}; my $warncellId = $param->{warncellId}; - Log3 $name, 5, "$name: DWD_OpenData_ProcessAlerts START (PID $$)"; + ::Log3 $name, 5, "$name: ProcessAlerts START (PID $$)"; my %alerts; eval { @@ -1081,7 +1287,7 @@ sub DWD_OpenData_ProcessAlerts($$$) die "no data retrieved from URL '$url'"; } - Log3 $name, 5, "$name: DWD_OpenData_ProcessAlerts: data received"; + ::Log3 $name, 5, "$name: ProcessAlerts: data received"; # create memory mapped file from received data and unzip open my $zipFileHandle, '<', \$fileContent; @@ -1094,8 +1300,8 @@ sub DWD_OpenData_ProcessAlerts($$$) # empty string, skip next; } - # parse XML strings - Log3 $name, 5, "$name: DWD_OpenData_ProcessAlerts: parsing XML document"; + # parse XML string + ::Log3 $name, 5, "$name: ProcessAlerts: parsing XML document"; my $dom = XML::LibXML->load_xml(string => $xmlString); if (!$dom) { die "parsing XML failed"; @@ -1105,10 +1311,10 @@ sub DWD_OpenData_ProcessAlerts($$$) my $alert = {}; my $alertNode = $dom->documentElement(); foreach my $alertChildNode ($alertNode->nonBlankChildNodes()) { - #Log3 $name, 5, "$name: DWD_OpenData_ProcessAlerts child node: " . $alertChildNode->nodeName(); + #::Log3 $name, 5, "$name: ProcessAlerts child node: " . $alertChildNode->nodeName(); if ($alertChildNode->nodeName() eq 'identifier') { $alert->{identifier} = $alertChildNode->textContent(); - #Log3 $name, 5, "$name: DWD_OpenData_ProcessAlerts identifier: " . $alert->{identifier}; + #::Log3 $name, 5, "$name: ProcessAlerts identifier: " . $alert->{identifier}; } elsif ($alertChildNode->nodeName() eq 'status') { $alert->{status} = $alertChildNode->textContent(); } elsif ($alertChildNode->nodeName() eq 'msgType') { @@ -1125,7 +1331,7 @@ sub DWD_OpenData_ProcessAlerts($$$) } } elsif ($alertChildNode->nodeName() eq 'info') { foreach my $infoChildNode ($alertChildNode->nonBlankChildNodes()) { - #Log3 $name, 5, "$name: DWD_OpenData_ProcessAlerts child node: '" . $infoChildNode->nodeName() . "'"; + #::Log3 $name, 5, "$name: ProcessAlerts child node: '" . $infoChildNode->nodeName() . "'"; if ($infoChildNode->nodeName() eq 'category') { $alert->{category} = $infoChildNode->textContent(); } elsif ($infoChildNode->nodeName() eq 'event') { @@ -1150,9 +1356,9 @@ sub DWD_OpenData_ProcessAlerts($$$) $alert->{areaColor} =~ s/ /, /g; } } elsif ($infoChildNode->nodeName() eq 'onset') { - $alert->{onset} = DWD_OpenData_ParseCAPTime($infoChildNode->textContent()); + $alert->{onset} = ParseCAPTime($infoChildNode->textContent()); } elsif ($infoChildNode->nodeName() eq 'expires') { - $alert->{expires} = DWD_OpenData_ParseCAPTime($infoChildNode->textContent()); + $alert->{expires} = ParseCAPTime($infoChildNode->textContent()); } elsif ($infoChildNode->nodeName() eq 'headline') { $alert->{headline} = $infoChildNode->textContent(); } elsif ($infoChildNode->nodeName() eq 'description') { @@ -1169,7 +1375,7 @@ sub DWD_OpenData_ProcessAlerts($$$) $alert->{altitude} = []; $alert->{ceiling} = []; } - #Log3 $name, 5, "$name: DWD_OpenData_ProcessAlerts warncellid: " . $xpc->findvalue("./cap:geocode/cap:value"); + #::Log3 $name, 5, "$name: ProcessAlerts warncellid: " . $xpc->findvalue("./cap:geocode/cap:value"); push(@{$alert->{warncellid}}, $xpc->findvalue("./cap:geocode/cap:value")); push(@{$alert->{areaDesc}}, $xpc->findvalue("./cap:areaDesc")); push(@{$alert->{altitude}}, $xpc->findvalue("./cap:altitude")); @@ -1179,7 +1385,7 @@ sub DWD_OpenData_ProcessAlerts($$$) } } } - #Log3 $name, 5, "$name: DWD_OpenData_ProcessAlerts header: $alert->{identifier}, $alert->{status}, $alert->{msgType}: $alert->{headline}, $alert->{warncellids}[0]"; + #::Log3 $name, 5, "$name: ProcessAlerts header: $alert->{identifier}, $alert->{status}, $alert->{msgType}: $alert->{headline}, $alert->{warncellids}[0]"; if ($alert->{status} ne 'Test' && $alert->{responseType} ne 'Monitor') { $alerts{$alert->{identifier}} = $alert; } @@ -1192,29 +1398,29 @@ sub DWD_OpenData_ProcessAlerts($$$) my @parts = split(/ at |\n/, $@); # discard anything after " at " or newline if (@parts) { $errorMessage = $parts[0]; - Log3 $name, 4, "$name: DWD_OpenData_ProcessAlerts error: $parts[0]"; + ::Log3 $name, 4, "$name: ProcessAlerts error: $parts[0]"; } else { $errorMessage = $@; - Log3 $name, 4, "$name: DWD_OpenData_ProcessAlerts error: $@"; + ::Log3 $name, 4, "$name: ProcessAlerts error: $@"; } } else { # alerts parsed successfully - my $communeUnion = DWD_OpenData_IsCommuneUnionWarncellId($warncellId); + my $communeUnion = IsCommuneUnionWarncellId($warncellId); if (defined($hash->{".alertsFile".$communeUnion})) { if (open(my $file, ">", $hash->{".alertsFile".$communeUnion})) { # write alerts to temp file binmode($file); my $frozenAlerts = freeze(\%alerts); - Log3 $name, 5, "$name: DWD_OpenData_ProcessAlerts temp file " . $hash->{".alertsFile".$communeUnion} . " alerts " . keys(%alerts) . " size " . length($frozenAlerts); + ::Log3 $name, 5, "$name: ProcessAlerts temp file " . $hash->{".alertsFile".$communeUnion} . " alerts " . keys(%alerts) . " size " . length($frozenAlerts); print($file $frozenAlerts); close($file); } else { $errorMessage = $!; - Log3 $name, 3, "$name: DWD_OpenData_ProcessAlerts error opening temp file: $errorMessage"; + ::Log3 $name, 3, "$name: ProcessAlerts error opening temp file: $errorMessage"; } } else { $errorMessage = 'result file name not defined'; - Log3 $name, 3, "$name: DWD_OpenData_ProcessAlerts error: temp file name not defined"; + ::Log3 $name, 3, "$name: ProcessAlerts error: temp file name not defined"; } } @@ -1222,32 +1428,42 @@ sub DWD_OpenData_ProcessAlerts($$$) $errorMessage =~ s/\n/; /g; $errorMessage =~ s/,/;/g; - Log3 $name, 5, "$name: DWD_OpenData_ProcessAlerts END"; + ::Log3 $name, 5, "$name: ProcessAlerts END"; return [$name, $errorMessage, $warncellId, $time]; } -=item DWD_OpenData_GetAlertsFinishFn(;$$$$) +=head2 GetAlertsFinish(@) - BlockingStart FinishFn callback +BlockingCall I callback, expects array returned by function L as single parameter - @param $result return value from DWD_OpenData_GetAlertsBlockingFn +=over + +=item * param name: name of DWD_OpenData device + +=item * param errorMessage: empty string or processing error message + +=item * param warncellId: numeric warncell id for which alers have been requested, may also be C, C or C + +=item * param time: epoch time when alerts where received + +=back =cut -sub DWD_OpenData_GetAlertsFinishFn(;$$$$) +sub GetAlertsFinish(@) { my ($name, $errorMessage, $warncellId, $time) = @_; if (defined($name)) { - Log3 $name, 5, "$name: DWD_OpenData_GetAlertsFinishFn START (PID $$)"; + ::Log3 $name, 5, "$name: GetAlertsFinish START (PID $$)"; - my $hash = $defs{$name}; - my $communeUnion = DWD_OpenData_IsCommuneUnionWarncellId($warncellId); + my $hash = $::defs{$name}; + my $communeUnion = IsCommuneUnionWarncellId($warncellId); if (defined($errorMessage) && length($errorMessage) > 0) { - $dwd_alerts_updating[$communeUnion] = undef; - readingsSingleUpdate($hash, 'state', "alerts error: $errorMessage", 1); + $alerts_updating[$communeUnion] = undef; + ::readingsSingleUpdate($hash, 'state', "alerts error: $errorMessage", 1); } elsif (defined($hash->{".alertsFile".$communeUnion})) { # deserialize alerts my $fh = $hash->{".alertsFileHandle".$communeUnion}; @@ -1258,7 +1474,7 @@ sub DWD_OpenData_GetAlertsFinishFn(;$$$$) close($hash->{".alertsFileHandle".$communeUnion}); unlink($hash->{".alertsFile".$communeUnion}); my %newAlerts = %{thaw($frozenAlerts)}; - Log3 $name, 5, "$name: DWD_OpenData_GetAlertsFinishFn temp file " . $hash->{".alertsFile".$communeUnion} . " alerts " . keys(%newAlerts) . " size " . length($frozenAlerts); + ::Log3 $name, 5, "$name: GetAlertsFinish temp file " . $hash->{".alertsFile".$communeUnion} . " alerts " . keys(%newAlerts) . " size " . length($frozenAlerts); delete($hash->{".alertsFile".$communeUnion}); # @TODO delete global alert list when no differential updates are available @@ -1268,7 +1484,7 @@ sub DWD_OpenData_GetAlertsFinishFn(;$$$$) foreach my $alert (values(%newAlerts)) { my $indentifierExists = defined($alerts->{$alert->{identifier}}); if ($indentifierExists) { - Log3 $name, 5, "$name: DWD_OpenData_ProcessAlerts identifier " . $alert->{identifier} . " already known, data not updated"; + ::Log3 $name, 5, "$name: ProcessAlerts identifier " . $alert->{identifier} . " already known, data not updated"; } elsif ($alert->{msgType} eq 'Alert') { # add new alert $alerts->{$alert->{identifier}} = $alert; @@ -1286,76 +1502,85 @@ sub DWD_OpenData_GetAlertsFinishFn(;$$$$) } } } - $dwd_alerts[$communeUnion] = $alerts; - $dwd_alerts_received[$communeUnion] = $time; - $dwd_alerts_updating[$communeUnion] = undef; + $alerts_data[$communeUnion] = $alerts; + $alerts_received[$communeUnion] = $time; + $alerts_updating[$communeUnion] = undef; if ($warncellId >= 0) { # update alert readings for warncell id - DWD_OpenData_UpdateAlerts($hash, $warncellId); - } elsif ($warncellId == DWD_OD_UPDATE_ALL) { - if (!defined($dwd_alerts_updating[0]) || (time() - $dwd_alerts_updating[0] >= 60)) { + UpdateAlerts($hash, $warncellId); + } elsif ($warncellId == UPDATE_ALL) { + if (!defined($alerts_updating[0]) || (time() - $alerts_updating[0] >= 60)) { # communeunions cache updated, start district cache update; - DWD_OpenData_GetAlerts($hash, DWD_OD_UPDATE_DISTRICTS); + GetAlerts($hash, UPDATE_DISTRICTS); } } else { - readingsSingleUpdate($hash, 'state', "alerts cache updated", 1); + ::readingsSingleUpdate($hash, 'state', "alerts cache updated", 1); } } else { - readingsSingleUpdate($hash, 'state', "alerts error: result file name not defined", 1); - Log3 $name, 3, "$name: DWD_OpenData_GetAlertsFinishFn error: temp file name not defined"; + ::readingsSingleUpdate($hash, 'state', "alerts error: result file name not defined", 1); + ::Log3 $name, 3, "$name: GetAlertsFinish error: temp file name not defined"; } - $hash->{ALERTS_IN_CACHE} = (ref($dwd_alerts[0]) eq 'HASH'? scalar(keys(%{$dwd_alerts[0]})) : 0) + (ref($dwd_alerts[1]) eq 'HASH'? scalar(keys(%{$dwd_alerts[1]})) : 0); + $hash->{ALERTS_IN_CACHE} = (ref($alerts_data[0]) eq 'HASH'? scalar(keys(%{$alerts_data[0]})) : 0) + (ref($alerts_data[1]) eq 'HASH'? scalar(keys(%{$alerts_data[1]})) : 0); - Log3 $name, 5, "$name: DWD_OpenData_GetAlertsFinishFn END"; + ::Log3 $name, 5, "$name: GetAlertsFinish END"; } else { - Log 3, "DWD_OpenData_GetAlertsFinishFn error: device name missing"; + ::Log 3, "GetAlertsFinish error: device name missing"; } } -=item DWD_OpenData_GetAlertsAbortFn($) +=head2 GetAlertsAbort($) - BlockingStart AbortFn callback +BlockingCall I callback - @param $hash hash of DWD_OpenData device +=over + +=item * param hash: hash of DWD_OpenData device + +=back =cut -sub DWD_OpenData_GetAlertsAbortFn($) +sub GetAlertsAbort($) { my ($hash, $errorMessage) = @_; my $name = $hash->{NAME}; - Log3 $name, 3, "$name: DWD_OpenData_GetAlertsAbortFn error: retrieving weather alerts failed, $errorMessage"; + ::Log3 $name, 3, "$name: GetAlertsAbort error: retrieving weather alerts failed, $errorMessage"; - readingsSingleUpdate($hash, 'state', "alerts error: retrieving weather alerts failed, $errorMessage", 1); + ::readingsSingleUpdate($hash, 'state', "alerts error: retrieving weather alerts failed, $errorMessage", 1); } -=item DWD_OpenData_UpdateAlerts($$) +=head2 UpdateAlerts($$) - update alert readings for given warncell id from global alerts list +update alert readings for given warncell id from global alerts list - @param $hash hash of DWD_OpenData device - @param $warncellId numeric warncell id +=over - @return undef or error message +=item * param hash: hash of DWD_OpenData device + +=item * param warncellId: numeric warncell id greater zero + +=item * return C or error message + +=back =cut -sub DWD_OpenData_UpdateAlerts($$) +sub UpdateAlerts($$) { my ($hash, $warncellId) = @_; my $name = $hash->{NAME}; # delete existing alert readings - CommandDeleteReading(undef, "$name a_.*"); + ::CommandDeleteReading(undef, "$name a_.*"); - readingsBeginUpdate($hash); + ::readingsBeginUpdate($hash); # order alerts by onset - my $communeUnion = DWD_OpenData_IsCommuneUnionWarncellId($warncellId); - my $alerts = $dwd_alerts[$communeUnion]; + my $communeUnion = IsCommuneUnionWarncellId($warncellId); + my $alerts = $alerts_data[$communeUnion]; my @identifiers = sort { $alerts->{$a}->{onset} <=> $alerts->{$b}->{onset} } keys(%{$alerts}); my $index = 0; foreach my $identifier (@identifiers) { @@ -1366,23 +1591,23 @@ sub DWD_OpenData_UpdateAlerts($$) if ($wcId == $warncellId) { # alert found, create readings my $prefix = 'a_'.$index.'_'; - readingsBulkUpdate($hash, $prefix.'category', $alert->{category}); - readingsBulkUpdate($hash, $prefix.'event', $alert->{eventCode}); - readingsBulkUpdate($hash, $prefix.'eventDesc', encode('UTF-8', $alert->{event})); - readingsBulkUpdate($hash, $prefix.'eventGroup', $alert->{eventGroup}); - readingsBulkUpdate($hash, $prefix.'responseType', $alert->{responseType}); - readingsBulkUpdate($hash, $prefix.'urgency', $alert->{urgency}); - readingsBulkUpdate($hash, $prefix.'severity', $alert->{severity}); - readingsBulkUpdate($hash, $prefix.'areaColor', $alert->{areaColor}); - readingsBulkUpdate($hash, $prefix.'onset', DWD_OpenData_FormatDateTimeLocal($hash, $alert->{onset})); - readingsBulkUpdate($hash, $prefix.'expires', DWD_OpenData_FormatDateTimeLocal($hash, $alert->{expires})); - readingsBulkUpdate($hash, $prefix.'headline', encode('UTF-8', $alert->{headline})); - readingsBulkUpdate($hash, $prefix.'description', encode('UTF-8', $alert->{description})); - readingsBulkUpdate($hash, $prefix.'instruction', encode('UTF-8', $alert->{instruction})); - readingsBulkUpdate($hash, $prefix.'area', $alert->{warncellid}[$areaIndex]); - readingsBulkUpdate($hash, $prefix.'areaDesc', encode('UTF-8', $alert->{areaDesc}[$areaIndex])); - readingsBulkUpdate($hash, $prefix.'altitude', floor(0.3048*$alert->{altitude}[$areaIndex] + 0.5)); - readingsBulkUpdate($hash, $prefix.'ceiling', floor(0.3048*$alert->{ceiling}[$areaIndex] + 0.5)); + ::readingsBulkUpdate($hash, $prefix.'category', $alert->{category}); + ::readingsBulkUpdate($hash, $prefix.'event', $alert->{eventCode}); + ::readingsBulkUpdate($hash, $prefix.'eventDesc', encode('UTF-8', $alert->{event})); + ::readingsBulkUpdate($hash, $prefix.'eventGroup', $alert->{eventGroup}); + ::readingsBulkUpdate($hash, $prefix.'responseType', $alert->{responseType}); + ::readingsBulkUpdate($hash, $prefix.'urgency', $alert->{urgency}); + ::readingsBulkUpdate($hash, $prefix.'severity', $alert->{severity}); + ::readingsBulkUpdate($hash, $prefix.'areaColor', $alert->{areaColor}); + ::readingsBulkUpdate($hash, $prefix.'onset', FormatDateTimeLocal($hash, $alert->{onset})); + ::readingsBulkUpdate($hash, $prefix.'expires', FormatDateTimeLocal($hash, $alert->{expires})); + ::readingsBulkUpdate($hash, $prefix.'headline', encode('UTF-8', $alert->{headline})); + ::readingsBulkUpdate($hash, $prefix.'description', encode('UTF-8', $alert->{description})); + ::readingsBulkUpdate($hash, $prefix.'instruction', encode('UTF-8', $alert->{instruction})); + ::readingsBulkUpdate($hash, $prefix.'area', $alert->{warncellid}[$areaIndex]); + ::readingsBulkUpdate($hash, $prefix.'areaDesc', encode('UTF-8', $alert->{areaDesc}[$areaIndex])); + ::readingsBulkUpdate($hash, $prefix.'altitude', floor(0.3048*$alert->{altitude}[$areaIndex] + 0.5)); + ::readingsBulkUpdate($hash, $prefix.'ceiling', floor(0.3048*$alert->{ceiling}[$areaIndex] + 0.5)); $index++; last(); } @@ -1391,116 +1616,131 @@ sub DWD_OpenData_UpdateAlerts($$) # license if ($index == 1 && defined($alert->{license})) { - readingsBulkUpdate($hash, 'a_copyright', encode('UTF-8', $alert->{license})); + ::readingsBulkUpdate($hash, 'a_copyright', encode('UTF-8', $alert->{license})); } } # alert count and receive time - readingsBulkUpdate($hash, 'a_count', $index); - readingsBulkUpdate($hash, "a_time", DWD_OpenData_FormatDateTimeLocal($hash, $dwd_alerts_received[$communeUnion])); - readingsBulkUpdate($hash, 'state', "alerts updated"); + ::readingsBulkUpdate($hash, 'a_count', $index); + ::readingsBulkUpdate($hash, "a_time", FormatDateTimeLocal($hash, $alerts_received[$communeUnion])); + ::readingsBulkUpdate($hash, 'state', "alerts updated"); - readingsEndUpdate($hash, 1); + ::readingsEndUpdate($hash, 1); return undef; } -=item DWD_OpenData_Timer($) +# ----------------------------------------------------------------------------- - @param $hash hash of DWD_OpenData device +package main; + + +=head1 FHEM INIT FUNCTION + +=head2 DWD_OpenData_Initialize($) + +FHEM I function + +=over + +=item * param hash: hash of DWD_OpenData device + +=back =cut -sub DWD_OpenData_Timer($) -{ +sub DWD_OpenData_Initialize($) { my ($hash) = @_; my $name = $hash->{NAME}; - Log3 $name, 5, "$name: DWD_OpenData_Timer START"; + $hash->{DefFn} = 'DWD_OpenData::Define'; + $hash->{UndefFn} = 'DWD_OpenData::Undef'; + $hash->{ShutdownFn} = 'DWD_OpenData::Shutdown'; + $hash->{AttrFn} = 'DWD_OpenData::Attr'; + $hash->{GetFn} = 'DWD_OpenData::Get'; - my $time = time(); - my ($tSec, $tMin, $tHour, $tMday, $tMon, $tYear, $tWday, $tYday, $tIsdst) = DWD_OpenData_Localtime($hash, $time); - my $actQuarter = int($tMin/15); - - if ($actQuarter == 0) { - my $forecastStation = AttrVal($name, 'forecastStation', undef); - if (defined($forecastStation)) { - my $result = DWD_OpenData_GetForecast($hash, $forecastStation); - if (defined($result)) { - Log3 $name, 4, "$name: error retrieving forecast: $result"; - } - } - } - - my $warncellId = AttrVal($name, 'alertArea', undef); - if (defined($warncellId)) { - # skip update if already in progress - my $communeUnion = DWD_OpenData_IsCommuneUnionWarncellId($warncellId); - if (!defined($dwd_alerts_updating[$communeUnion]) || (time() - $dwd_alerts_updating[$communeUnion] >= 60)) { - my $result = DWD_OpenData_GetAlerts($hash, $warncellId); - if (defined($result)) { - Log3 $name, 4, "$name: error retrieving alerts: $result"; - } - } - } - - # schedule next for 5 seconds past next quarter - my $nextQuarterSeconds = DWD_OpenData_Timelocal($hash, 0, $actQuarter*15, $tHour, $tMday, $tMon, $tYear) + 905; - InternalTimer($nextQuarterSeconds, 'DWD_OpenData_Timer', $hash, 0); - - Log3 $name, 5, "$name: DWD_OpenData_Timer END"; + $hash->{AttrList} = 'disable:0,1 ' + .'forecastStation forecastDays forecastProperties forecastResolution:3,6 forecastWW2Text:0,1 ' + .'alertArea alertLanguage:DE,EN ' + .'timezone ' + .$readingFnAttributes; } # ----------------------------------------------------------------------------- 1; +# ----------------------------------------------------------------------------- +# +# CHANGES +# +# 22.09.2018 jensb +# feature: forecast rotation for offline update reenabled +# +# 20.09.2018 jensb +# feature: CSV based forecast replaced by KML based forecast +# +# 04.07.2018 jensb +# bugfix: mark strptime as non package function in ParseDateTimeLocal and ParseDateLocal +# +# 23.06.2018 jensb +# bugfix: added use for package Encode +# +# 16.06.2018 jensb +# enhancement: trim alert values +# +# 14.06.2018 jensb +# coding: functions converted to package DWD_OpenData +# +# 13.05.2018 jensb +# bugfix: total alerts in cache +# +# 06.05.2018 jensb +# feature: detect empty alerts zip file +# bugfix: preprocess exception messages from ProcessAlerts because Blocking FinishFn parameter content may not contain commas or newlines +# +# 22.04.2018 jensb +# feature: relaxed installation prerequisites (Text::CSV_XS now forecast specific, TZ does not need to be defined) +# +# 16.04.2018 jensb +# bugfix: alerts push on scalar +# +# 13.04.2018 jensb +# feature: forecast weekday reading +# +# 28.03.2018 jensb +# feature: support for CAP alerts +# +# 22.03.2018 jensb +# bugfix: replaced trunc with round when calculating delta days to cope with summertime +# +# 18.02.2018 jensb +# feature: LWP::Simple replaced by HttpUtils_NonblockingGet (provided by JoWiemann) +# # ----------------------------------------------------------------------------- -=pod +# ----------------------------------------------------------------------------- +# +# @TODO forecast: if a property is not available for a given hour the value of the previous or next hour is to be used/interpolated +# @TODO alerts: queue get commands while cache is updating +# @TODO history: https://opendata.dwd.de/weather/weather_reports/poi/ +# +# ----------------------------------------------------------------------------- - CHANGES +=head1 FHEM COMMANDREF METADATA - 13.05.2018 jensb - bugfix: total alerts in cache +=over - 06.05.2018 jensb - feature: detect empty alerts zip file - bugfix: preprocess exception messages from ProcessAlerts because Blocking FinishFn parameter content may not contain commas or newlines - - 22.04.2018 jensb - feature: relaxed installation prerequisites (Text::CSV_XS now forecast specific, TZ does not need to be defined) - - 16.04.2018 jensb - bugfix: alerts push on scalar - - 13.04.2018 jensb - feature: forecast weekday reading - - 28.03.2018 jensb - feature: support for CAP alerts - - 22.03.2018 jensb - bugfix: replaced trunc with round when calculating delta days to cope with summertime - - 18.02.2018 jensb - feature: LWP::Simple replaced by HttpUtils_NonblockingGet (provided by JoWiemann) - -=cut - -=pod - - @TODO forecast: if a property is not available for a given hour the value of the previous or next hour is to be used/interpolated - @TODO alerts: queue get commands while cache is updating - @TODO history: https://opendata.dwd.de/weather/weather_reports/poi/ - -=cut - - -=pod =item device -=item summary DWD Open Data weather forecast -=item summary_DE DWD Open Data Wettervorhersage + +=item summary DWD Open Data weather alerts and forecast + +=item summary_DE DWD Open Data Wetterwarnungen und Wettervorhersage + +=back + +=head1 INSTALLATION AND CONFIGURATION + =begin html @@ -1511,7 +1751,7 @@ sub DWD_OpenData_Timer($) This modules provides two elements of the available data:

  • weather forecasts: - Individual stations of local forecasts of WMO, national and interpolated stations (MOSMIX). The stations are worldwide POIs and the German DWD network. This data is updated by the DWD typically every 12 hours.

    + Total lists of local forecasts of WMO, national and interpolated stations, all variables, 3, 9, 15, 21 UTC. More than 70 properties are available for worldwide POIs and the German DWD network. This data typically spans 10 days and is updated by the DWD every 6 hours.

    You can request forecasts for different stations in sequence using the command get forecast <station code> or for one station continuously using the attribute forecastStation. To get continuous mode for more than one station you need to create separate DWD_OpenData devices.

    @@ -1530,8 +1770,6 @@ sub DWD_OpenData_Timer($) Installation notes:

      -
    • This module requires the additional Perl module Text::CSV_XS (0.40 or higher) for weather forecasts. It can be installed depending on your OS and your preferences (e.g. sudo apt-get install libtext-csv-xs-perl or using CPAN).

    • -
    • This module requires the additional Perl module XML::LibXML for weather alerts. It can be installed depending on your OS and your preferences (e.g. sudo apt-get install libxml-libxml-perl or using CPAN).

    • Data is fetched from the DWD Open Data Server using the FHEM module HttpUtils. If you use a proxy for internet access you need to set the global attribute proxy to a suitable value in the format myProxyHost:myProxyPort.

    • @@ -1546,6 +1784,8 @@ sub DWD_OpenData_Timer($)
    • Like some other Perl modules this module temporarily modifies the TZ environment variable for timezone conversions. This may cause unexpected results in multi threaded environments.

    • The forecast reading names do not contain absolute days or hours to keep them independent of summertime adjustments. Forecast days are counted relative to "today" of the timezone defined by the attribute of the same name or the timezone specified by the Perl TZ environment variable if undefined.

    • + +
    • Starting on 17.09.2018 the forecast data is no longer available in CSV format and is based on the KML format instead. While most of the properties of the CSV format are still available in KML format, their names have changed and you will have to adjust your existing installation accordingly.


    @@ -1574,7 +1814,7 @@ sub DWD_OpenData_Timer($) This command can be used before querying several warncells in sequence or to force a higher update frequency than the built-in 15 minutes. Note that all DWD_OpenData devices share a single alerts cache so updating the cache via one of the devices is sufficient.


- + Attributes
@@ -1582,33 +1822,34 @@ sub DWD_OpenData_Timer($)
  • disable {0|1}, default: 0
    Disable fetching data.

  • -
  • timezone , default: OS dependent
    +
  • timezone <tz>, default: OS dependent
    IANA TZ string for date and time readings (e.g. "Europe/Berlin"), can be used to assume the perspective of a station that is in a different timezone or if your OS timezone settings do not match your local timezone. Alternatively you may use tzselect on the Linux command line to find a valid timezone string.

  • - forecast related: + forecast related:

    • forecastStation <station code>, default: none
      Setting forecastStation enables automatic updates every hour. The station code is either a 5 digit WMO station code or an alphanumeric DWD station code from the MOSMIX station catalogue.

    • -
    • forecastDays <n>, default: none
      - Limits number of forecast days. Setting 0 will still provide forecast data for today. +
    • forecastDays <n>, default: 6
      + Limits number of forecast days. Setting 0 will still provide forecast data for today. The maximum value is 9 (for today and 9 future days).

    • forecastResolution {3|6}, default: 6 h
      Time resolution (number of hours between 2 samples).

    • -
    • forecastProperties [<p1>[,<p2>]...] , default: none
      - If defined limits the number of properties to the given list. If you remove a property from the list existing readings must be deleted manually in continuous mode.
      - Note: Not all selectable properties are available for all stations and for all hours. +
    • forecastProperties [<p1>[,<p2>]...] , default: Tx, Tn, Tg, TTT, DD, FX1, Neff, RR6c, RRhc, Rh00, ww
      + A list of the properties available can be found here. + If you remove a property from the list existing readings must be deleted manually in continuous mode.
      + Note: Not all properties are available for all stations and for all hours.

    • forecastWW2Text {0|1}, default: 0
      Create additional wwd readings containing the weather code as a descriptive text in German language.

    - alert related: + alert related:

    • alertArea <warncell id>, default: none
      Setting alertArea enables automatic updates of the alerts cache every 15 minutes. @@ -1627,46 +1868,48 @@ sub DWD_OpenData_Timer($) fc<day>_[<sample>_]<property>

      + A description of the more than 70 properties available and their units of measurement can be found here. The units of measurement for temperatures and wind speeds are converted to °C and km/h respectively. Only a few choice properties are listed in the following paragraphs:

      +
        -
      • day - relative day (0 .. 7) based on the timezone attribute where 0 is today

      • +
      • day - relative day (0 .. 9) based on the timezone attribute where 0 is today

      • sample - relative time (0 .. 3 or 7) equivalent to multiples of 6 or 3 hours UTC depending on the forecastHours attribute

      • -
      • day properties (see raw data of station for time relation) +
      • day properties (typically for 06:00 station time, see raw data of station for time relation)
          -
        • date - date based on the timezone attribute
        • -
        • weekday - abbreviated weekday based on the timezone attribute in the language of your FHEM system
        • -
        • Tn [°C] - minimum temperature of previous 24 hours (typically until 07:00 station time)
        • -
        • Tx [°C] - maximum temperature of previous 24 hours (typically until 19:00 station time)
        • -
        • Tm [°C] - average temperature of previous 24 hours
        • -
        • Tg [°C] - minimum temperature 5 cm above ground of previous 24 hours
        • -
        • ev [kg/m2] - evapotranspiration of previous 24 hours
        • -
        • SS24 [h] - total sunshine duration of previous 24 hours
        • +
        • date - date based on the timezone attribute
        • +
        • weekday - abbreviated weekday based on the timezone attribute in the language of your FHEM system
        • +
        • Tn [°C] - minimum temperature of previous 24 hours
        • +
        • Tx [°C] - maximum temperature of previous 24 hours (typically for 18:00 station time)
        • +
        • Tm [°C] - average temperature of previous 24 hours
        • +
        • Tg [°C] - minimum temperature 5 cm above ground of previous 24 hours
        • +
        • PEvap [kg/m2] - evapotranspiration of previous 24 hours
        • +
        • SunD [s] - total sunshine duration of previous 24 hours

      • hour properties
          -
        • time - hour based the timezone attribute
        • -
        • TT [°C] - dry bulb temperature at 2 meter above ground
        • -
        • Td [°C] - dew point temperature at 2 meter above ground
        • -
        • dd [°] - average wind direction 10 m above ground
        • -
        • ff [km/h] - average wind speed 10 m above ground
        • -
        • fx [km/h] - maximum wind speed in the last hour
        • -
        • RR6 [mm] - precipitation amount in the last 6 hours
        • -
        • RRp6 [%] - probability of rain in the last 6 hours
        • -
        • RR12 [mm] - precipitation amount in the last 12 hours
        • -
        • RRp12 [%] - probability of rain in the last 12 hours
        • -
        • RR24 [mm] - precipitation amount in the last 24 hours
        • -
        • RRp24 [%] - probability of rain in the last 24 hours
        • -
        • ww - weather code (see WMO 4680/4677, SYNOP)
        • -
        • wwd - German weather code description
        • -
        • VV [m] - horizontal visibility
        • -
        • Nf [1/8] - effective cloud cover
        • -
        • NL [1/8] - lower level cloud cover
        • -
        • NM [1/8] - medium level cloud cover
        • -
        • NH [1/8] - high level cloud cover
        • -
        • PPPP [hPa] - pressure equivalent at sea level
        • +
        • time - hour based the timezone attribute
        • +
        • TTT [°C] - dry bulb temperature at 2 meter above ground
        • +
        • Td [°C] - dew point temperature at 2 meter above ground
        • +
        • DD [°] - average wind direction 10 m above ground
        • +
        • FF [km/h] - average wind speed 10 m above ground
        • +
        • FX1 [km/h] - maximum wind speed in the last hour
        • +
        • RR6c [kg/m2] - precipitation amount in the last 6 hours
        • +
        • R600 [%] - probability of rain in the last 6 hours
        • +
        • RRhc [kg/m2] - precipitation amount in the last 12 hours
        • +
        • Rh00 [%] - probability of rain in the last 12 hours
        • +
        • RRdc [kg/m2] - precipitation amount in the last 24 hours
        • +
        • Rd00 [%] - probability of rain in the last 24 hours
        • +
        • ww - weather code (see WMO 4680/4677, SYNOP)
        • +
        • wwd - German weather code description
        • +
        • VV [m] - horizontal visibility
        • +
        • Neff [%] - effective cloud cover
        • +
        • Nl [%] - lower level cloud cover below 2000 m
        • +
        • Nm [%] - medium level cloud cover below 7000 m
        • +
        • Nh [%] - high level cloud cover obove 7000 m
        • +
        • PPPP [hPa] - pressure equivalent at sea level

      @@ -1674,9 +1917,11 @@ sub DWD_OpenData_Timer($) Additionally there are global forecast readings:
          -
        • fc_station - forecast station code (WMO or DWD)
        • -
        • fc_time - time the forecast updated was downloaded based on the timezone attribute
        • -
        • fc_copyright - legal information, must be displayed with forecast data, see DWD usage conditions
        • +
        • fc_station - forecast station code (WMO or DWD)
        • +
        • fc_description - station description
        • +
        • fc_coordinates - world coordinat and height of station
        • +
        • fc_time - time the forecast updated was downloaded based on the timezone attribute
        • +
        • fc_copyright - legal information, must be displayed with forecast data, see DWD usage conditions


      @@ -1711,12 +1956,13 @@ sub DWD_OpenData_Timer($)

    - Additionally there are global alert readings: + Additionally there are some global alert readings:

    +
        -
      • a_time - time the last alert update was downloaded based on the timezone attribute
      • -
      • a_count - number of alerts available for selected warncell id
      • -
      • a_copyright - legal information, must be displayed with forecast data, see DWD usage conditions, not available if count is zero
      • +
      • a_time - time the last alert update was downloaded based on the timezone attribute
      • +
      • a_count - number of alerts available for selected warncell id
      • +
      • a_copyright - legal information, must be displayed with forecast data, see DWD usage conditions, not available if count is zero