2024-02-25 21:05:29 +00:00
# -----------------------------------------------------------------------------
2024-03-03 17:20:14 +00:00
# $Id: 55_DWD_OpenData.pm 28556 2024-03-02 19:09:00Z jensb $
2024-02-25 21:05:29 +00:00
# -----------------------------------------------------------------------------
= 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 .
Use of HttpUtils instead of LWP:: Simple:
Copyright ( C ) 2018 JoWiemann
see https: // forum . fhem . de /index.php/ topic , 83097 . msg761015 . html #msg761015
2024-02-28 22:44:32 +00:00
MOSMIX S forecast data support:
2024-02-25 21:05:29 +00:00
Copyright ( C ) 2024 DS_Starter + Jens B .
Sun position:
Copyright ( c ) Plataforma Solar de Almerýa , Spain
see http: // www . psa . es /sdg/s unpos . htm
Sunrise and sunset:
see https: // www . aa . quae . nl /en/ reken / zonpositie . html
see https: // en . wikipedia . org /wiki/ Sunrise_equation
Julian date conversion:
Copyright ( C ) 2012 E . G . Richards
see Explanatory Supplement to the Astronomical Almanac , 3 rd edition , S . E Urban and P . K . Seidelmann eds . , chapter 15.11 .3 , Interconverting Dates and Julian Day Numbers , Algorithm 4
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 .
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 .
You should have received a copy of the GNU General Public License
along with this script ; if not , write to the Free Software
Foundation , Inc . , 51 Franklin Street , Fifth Floor , Boston , MA 02110 - 1301 , USA .
A copy of the GNU General Public License , Version 2 can also be found at
http: // www . gnu . org /licenses/o ld - licenses / gpl - 2.0 .
This copyright notice MUST APPEAR in all copies of the script !
= cut
package AstroSun ;
use strict ;
use warnings ;
use Math::Trig qw( pi pi2 asin acos tan ) ;
use POSIX 'floor' ;
use Time::Local 'timegm' ;
use Time::Piece 'gmtime' ;
require Exporter ;
our $ VERSION = '1.000003' ;
our @ ISA = qw( Exporter ) ;
our @ EXPORT = qw( AzimuthElevation RiseSet ) ;
our @ EXPORT_OK = qw( EpochToJulianDate JulianDateToEpoch ) ;
= head2 EpochToJulianDate ( ; $ )
= over
= item * param time : epoch time [ s ] , optional , default: now
= item * return Julian date
= back
= cut
sub EpochToJulianDate {
my ( $ epoch ) = @ _ ;
if ( ! defined ( $ epoch ) ) {
$ epoch = time ( ) ;
}
return gmtime ( $ epoch ) - > julian_day ;
}
= head2 EpochToGreenwichMeanSideralDate ( ; $ )
Copyright ( c ) Plataforma Solar de Almerýa , Spain
simplified algorithm , accurate to within 0.5 minutes of arc for the year 1999 - 2015
= over
= item * param time : epoch time [ s ] , optional , default: now
= item * return Greenwich mean sideral date [ h ]
= back
= cut
sub EpochToGreenwichMeanSideralDate {
my ( $ epoch ) = @ _ ;
if ( ! defined ( $ epoch ) ) {
$ epoch = time ( ) ;
}
my $ elapsedDays = EpochToJulianDate ( $ epoch ) - 2451545 + 0.0008 ;
my ( $ seconds , $ minutes , $ hours , $ day , $ month , $ year , $ wday , $ yday , $ isdst ) = gmtime ( $ epoch ) ;
my $ timeAsHours = $ hours + $ minutes /60.0 + $seconds/ 3600.0 ;
return 6.6974243242 + 0.0657098283 * $ elapsedDays + $ timeAsHours ;
}
= head2 JulianDateToEpoch ( ; $ )
Copyright ( C ) 2012 E . G . Richards
= over
= item * param jd: Julian date [ day ] , optional , default: now
= item * return Gregorian date [ epoch ]
= back
= cut
sub JulianDateToEpoch {
my ( $ jd ) = @ _ ;
if ( ! defined ( $ jd ) ) {
return time ( ) ;
} else {
my $ j = floor ( $ jd ) ;
my $ f = $ j + 1401 ;
$ f += 3 * floor ( floor ( ( 4 * $ jd + 274277.0 ) /146097)/ 4 ) - 38 ;
my $ e = 4 * $ f + 3 ;
my $ g = floor ( ( $ e % 1461 ) / 4 ) ;
my $ h = 5 * $ g + 2 ;
my $ day = floor ( ( $ h % 153 ) / 5 ) + 1 ;
my $ month = ( floor ( $ h / 153 ) + 2 ) % 12 + 1 ;
my $ year = floor ( $ e /1461) - 4716 + floor((14 - $month)/ 12 ) ;
my $ seconds = sprintf ( "%0.0f" , 86400 * ( $ jd - $ j + 0.5 ) ) ; # round()
return timegm ( 0 , 0 , 0 , $ day , $ month - 1 , $ year - 1900 ) + $ seconds ;
}
}
= head2 CelestialPosition ( $ )
Copyright ( c ) Plataforma Solar de Almerýa , Spain
simplified algorithm , accurate to within 0.5 minutes of arc for the year 1999 - 2015
= over
= item * param epoch: epoch time [ s ] , optional , default: now
= item * return array of rightAscension and declination [ rad ]
= back
= cut
sub CelestialPosition {
my ( $ epoch ) = @ _ ;
# Calculate ecliptic coordinates (ecliptic longitude and obliquity of the
# ecliptic in radians but without limiting the angle to 2*pi
# (i.e., the result may be greater than 2*pi)
my $ elapsedDays = EpochToJulianDate ( $ epoch ) - 2451545 + ( 32.184 + 37 ) / 86400 ; # 2019: 37 leap seconds
my $ omega = 2.1429 - 0.0010394594 * $ elapsedDays ; # [rad]
my $ meanLongitude = 4.8950630 + 0.017202791698 * $ elapsedDays ; # [rad]
my $ meanAnomaly = 6.2400600 + 0.0172019699 * $ elapsedDays ; # [rad]
my $ eclipticLongitude = $ meanLongitude + 0.03341607 * sin ( $ meanAnomaly ) + 0.00034894 * sin ( 2 * $ meanAnomaly ) - 0.0001134 - 0.0000203 * sin ( $ omega ) ;
my $ eclipticObliquity = 0.4090928 - 6.2140e-9 * $ elapsedDays + 0.0000396 * cos ( $ omega ) ;
# Calculate celestial coordinates (right ascension and declination) in radians
# but without limiting the angle to 2*pi (i.e., the result may be
# greater than 2*pi)
my $ sinEclipticLongitude = sin ( $ eclipticLongitude ) ;
my $ y1 = cos ( $ eclipticObliquity ) * $ sinEclipticLongitude ;
my $ x1 = cos ( $ eclipticLongitude ) ;
my $ rightAscension = atan2 ( $ y1 , $ x1 ) ;
if ( $ rightAscension < 0.0 ) {
$ rightAscension = $ rightAscension + pi2 ;
}
my $ declination = asin ( sin ( $ eclipticObliquity ) * $ sinEclipticLongitude ) ;
return ( $ rightAscension , $ declination ) ;
}
= head2 AzimuthElevation ( ; $$ $ )
Calculate the azimuth and elevation of the sun for the given time and location .
Copyright ( c ) Plataforma Solar de Almerýa , Spain
simplified algorithm , accurate to within 0.5 minutes of arc for the year 1999 - 2015
= over
= item * param epoch: epoch time [ s ] , optional , default: now
= item * param longitude: geographic longitude [ deg ] , optional , default: global longitude or Frankfurt , Germany
= item * param latitude: geographic latitude [ deg ] , optional , default: global latitude or Frankfurt , Germany
= item * return array of azimuth and elevation [ deg ]
= back
= cut
sub AzimuthElevation {
my ( $ epoch , $ longitudeEast , $ latitudeNorth ) = @ _ ;
if ( ! defined ( $ longitudeEast ) || ! defined ( $ latitudeNorth ) ) {
# undefined: use Frankfurt, Germany
$ longitudeEast = :: AttrVal ( "global" , "longitude" , "8.686" ) ;
$ latitudeNorth = :: AttrVal ( "global" , "latitude" , "50.112" ) ;
}
my ( $ rightAscensionRadians , $ declinationRadians ) = CelestialPosition ( $ epoch ) ;
# Calculate local coordinates (azimuth [deg] and zenith angle [rad])
my $ rad = pi / 180 ;
my $ greenwichMeanSiderealDate = EpochToGreenwichMeanSideralDate ( $ epoch ) ;
my $ localMeanSiderealDateRadians = ( $ greenwichMeanSiderealDate * 15 + $ longitudeEast ) * $ rad ;
my $ hourAngleRadians = $ localMeanSiderealDateRadians - $ rightAscensionRadians ;
my $ cosHourAngle = cos ( $ hourAngleRadians ) ;
my $ latitudeRadians = $ latitudeNorth * $ rad ;
my $ cosLatitude = cos ( $ latitudeRadians ) ;
my $ sinLatitude = sin ( $ latitudeRadians ) ;
my $ zenithAngleRadians = acos ( $ cosLatitude * $ cosHourAngle * cos ( $ declinationRadians ) + sin ( $ declinationRadians ) * $ sinLatitude ) ;
my $ y = - sin ( $ hourAngleRadians ) ;
my $ x = tan ( $ declinationRadians ) * $ cosLatitude - $ sinLatitude * $ cosHourAngle ;
my $ azimuthRadians = atan2 ( $ y , $ x ) ;
if ( $ azimuthRadians < 0.0 ) {
$ azimuthRadians = $ azimuthRadians + pi2 ;
}
my $ azimuth = sprintf ( "%0.1f" , $ azimuthRadians / $ rad ) ; # round(1)
# Parallax correction of zenith angle [deg]
my $ meanEarthRadius = 6371.01 ; # [km]
my $ astronomicalUnit = 149597890 ; # [km]
my $ parallax = ( $ meanEarthRadius / $ astronomicalUnit ) * sin ( $ zenithAngleRadians ) ;
my $ zenithAngle = ( $ zenithAngleRadians + $ parallax ) / $ rad ;
# Elevation [deg]
my $ elevation = 90 - $ zenithAngle ;
$ elevation = sprintf ( "%0.1f" , $ elevation ) ; # round(1)
return ( $ azimuth , $ elevation ) ;
}
= head2 Mod ( $$ )
Calculate the arithmetic remainder of a division including fractions .
= cut
sub Mod {
my ( $ dividend , $ divisor ) = @ _ ;
return 0 if ( $ divisor == 0 ) ;
return $ dividend - int ( $ dividend / $ divisor ) * $ divisor ;
}
= head2 MeanSolarAnomaly ( $ )
Calculate mean solar anomaly for Julian date .
see https: // www . aa . quae . nl /en/ reken / zonpositie . html
= over
= item * param jd: Julian date
= item * return mean solar anomaly [ deg ]
= back
= cut
sub MeanSolarAnomaly {
my ( $ jd ) = @ _ ;
return Mod ( 357.5291 + 0.98560028 * ( $ jd - 2451545 ) , 360 ) ;
}
= head2 EclipticalLongitude ( $ )
Calculate ecliptical longitude of the sun .
see https: // www . aa . quae . nl /en/ reken / zonpositie . html
= over
= item * param meanSolarAnomalyRadians: mean solar anomaly [ rad ]
= item * return ecliptical longitude [ rad ]
= back
= cut
sub EclipticalLongitude {
my ( $ meanSolarAnomalyRadians ) = @ _ ;
my $ rad = pi / 180 ;
my $ equationOfcenter = 1.9148 * sin ( $ meanSolarAnomalyRadians ) + 0.0200 * sin ( 2 * $ meanSolarAnomalyRadians ) + 0.0003 * sin ( 3 * $ meanSolarAnomalyRadians ) ;
return Mod ( $ meanSolarAnomalyRadians / $ rad + $ equationOfcenter + 180 + 102.9372 , 360 ) * $ rad ;
}
= head2 EquatorialCoordinates ( $ )
Calculate equatorial coordinates of the sun .
see https: // www . aa . quae . nl /en/ reken / zonpositie . html
= over
= item * param eclipticLongitudeRadians: ecliptic longitude of the sun [ rad ]
= item * return right ascension and declination [ rad ]
= back
= cut
sub EquatorialCoordinates {
my ( $ eclipticLongitudeRadians ) = @ _ ;
my $ rad = pi / 180 ;
my $ rightAscensionRadians = atan2 ( sin ( $ eclipticLongitudeRadians ) * cos ( 23.4393 * $ rad ) , cos ( $ eclipticLongitudeRadians ) ) ;
my $ declinationRadians = asin ( sin ( $ eclipticLongitudeRadians ) * sin ( 23.4393 * $ rad ) ) ;
return ( $ rightAscensionRadians , $ declinationRadians ) ;
}
= head2 HourAngle ( $$ $ )
Calculate sun hour angle for Julian date .
see https: // www . aa . quae . nl /en/ reken / zonpositie . html
= over
= item * param jd: Julian date
= item * param rightAscension: right ascension of sun [ deg ]
= item * param longitudeEast: geographical longitude [ deg ]
= item * return hour angle of sun [ deg ] , limited to - 180 ... + 180
= back
= cut
sub HourAngle {
my ( $ jd , $ rightAscension , $ longitudeEast ) = @ _ ;
my $ sideralTime = Mod ( 280.1470 + 360.9856235 * ( $ jd - 2451545 ) + $ longitudeEast , 360 ) ;
my $ hourAngle = $ sideralTime - $ rightAscension ;
$ hourAngle -= 360 if ( $ hourAngle > 180 ) ;
$ hourAngle += 360 if ( $ hourAngle < - 180 ) ;
return $ hourAngle ;
}
= head2 Transit ( $$ $ )
Calculate solar transit date for Julian date .
see https: // en . wikipedia . org /wiki/ Sunrise_equation
= over
= item * param jd: Julian date
= item * param meanSolarAnomalyRadians: mean solar anomaly [ rad ]
= item * param eclipticalLongitudeRadians: ecliptical longitude of sun [ rad ]
= item * return date of solar transit [ Julian date ]
= back
= cut
sub Transit {
my ( $ jd , $ meanSolarAnomalyRadians , $ eclipticalLongitudeRadians ) = @ _ ;
return $ jd + 0.0053 * sin ( $ meanSolarAnomalyRadians ) - 0.0069 * sin ( 2 * $ eclipticalLongitudeRadians ) ;
}
= head2 ElevationCorrection ( ; $ )
Calculate upper solar limb elevation offset angle caused by sun diameter , typical atmospheric refraction and altitude of observer for sunrise .
see https: // en . wikipedia . org /wiki/ Sunrise_equation
= over
= item * param altitude: altitude of observer [ m ] , optional , default: 0 m
= item * return elevation offset angle of upper solar limb at sunrise [ deg ]
= back
= cut
sub ElevationCorrection {
my ( $ altitude ) = @ _ ;
if ( ! defined ( $ altitude ) ) {
# undefined: use 0 m (see level)
$ altitude = 0 ;
}
return - 0.83 - 2.076 * sqrt ( $ altitude ) / 60
}
= head2 HourAngleOptimization ( $$ $; $$ $ )
Iteratively improve sun rise ( mode = - 1 ) , sun transit ( mode = 0 ) and sun set ( mode = + 1 ) dates by minimizing hour angle change .
see https: // en . wikipedia . org /wiki/ Sunrise_equation
= over
= item * param mode: sun rise ( mode = - 1 ) , sun transit ( mode = 0 ) and sun set ( mode = + 1 )
= item * param jd: estimated Julian date
= item * param longitudeEast: geographical longitude [ deg ]
= item * param latitudeNorth: geographical latitude [ deg ] , optional , not used for mode = 0
= item * param altitude: altitude of observer [ m ] , optional , not used for mode = 0
= item * param twilightAngle: twilight angle [ deg ] , optional , not used for mode = 0
= item * return array of optimized Julian date and the declination of the sun [ rad ]
= back
= cut
sub HourAngleOptimization {
my ( $ mode , $ jd , $ longitudeEast , $ latitudeNorth , $ altitude , $ twilightAngle ) = @ _ ;
# iteratively improve sun rise date
my $ rad = pi / 180 ;
my $ loops = 0 ;
my $ rightAscensionRadians ;
my $ declinationRadians ;
my $ hourAngleDelta ;
do {
# hour angle
my $ meanSolarAnomalyRadians = MeanSolarAnomaly ( $ jd ) * $ rad ;
my $ eclipticalLongitudeRadians = EclipticalLongitude ( $ meanSolarAnomalyRadians ) ;
( $ rightAscensionRadians , $ declinationRadians ) = EquatorialCoordinates ( $ eclipticalLongitudeRadians ) ;
my $ hourAngle = HourAngle ( $ jd , $ rightAscensionRadians / $ rad , $ longitudeEast ) ;
if ( $ mode ) {
# sun rise/set hour angle
my $ hourAngleRiseSet = acos ( ( sin ( ( ElevationCorrection ( $ altitude ) + $ twilightAngle ) * $ rad ) - sin ( $ latitudeNorth * $ rad ) * sin ( $ declinationRadians ) ) /(cos($latitudeNorth*$rad)*cos($declinationRadians)))/ $ rad ;
# improved sun rise/set date
$ hourAngleDelta = $ hourAngle - $ mode * $ hourAngleRiseSet ;
$ jd -= $ hourAngleDelta / 360 ;
#::Log3 "", 3, "HourAngleOptimization meanSolarAnomaly ". $meanSolarAnomalyRadians/$rad . " eclipticalLongitude " . $eclipticalLongitudeRadians/$rad . " rightAscensionRadians " . $rightAscensionRadians/$rad . " declinationRadians " . $declinationRadians/$rad . " jd $jd hourAngle $hourAngle hourAngleRiseSet $hourAngleRiseSet hourAngleDelta $hourAngleDelta" . JulianDateToEpoch($jd);
} else {
# improved solar transit date
$ hourAngleDelta = $ hourAngle ;
$ jd -= $ hourAngleDelta / 360 ;
#::Log3 "", 3, "HourAngleOptimization meanSolarAnomaly ". $meanSolarAnomalyRadians/$rad . " eclipticalLongitude " . $eclipticalLongitudeRadians/$rad . " rightAscensionRadians " . $rightAscensionRadians/$rad . " declinationRadians " . $declinationRadians/$rad . " jd $jd hourAngle $hourAngle " . JulianDateToEpoch($jd);
}
$ loops + + ;
} while ( abs ( $ hourAngleDelta ) > 0.0005 && $ loops < 5 ) ;
return ( $ jd , $ declinationRadians ) ;
}
= head2 RiseSet ( ; $$ $$ $ )
Calculate time of sunrise , sun transit and sunset for given time and position including corrections for astronomical refraction , solar disc diameter and altitude of observer .
see https: // www . aa . quae . nl /en/ reken / zonpositie . html
see https: // en . wikipedia . org /wiki/ Sunrise_equation
Note: The calculated times belong to the day in the GMT timezone . If your location is in a different timezone you must add your timezone offset to the epoch time to get the same result for all times between 0 : 00 and 23 : 59 of your local time .
Adjust epoch time by time zone offset
= over
= item * param epoch: epoch time [ s ] , optional , default: now
= item * param longitudeEast: geographic longitude [ deg ] , optional , default: global longitude or Frankfurt , Germany
= item * param latitude: geographic latitude [ deg ] , optional , default: global latitude or Frankfurt , Germany
= item * param altitude: altitude of obeserver [ m ] , optional , default: 0 m
= item * param twilightAngle: twilight angle [ deg ] , optional , default: 0 °
= item * return array of sunrise , sun transit and sunset date for a day in the GMT timezone [ epoch ]
= back
= cut
sub RiseSet {
my ( $ epoch , $ longitudeEast , $ latitudeNorth , $ altitude , $ twilightAngle ) = @ _ ;
if ( ! defined ( $ epoch ) ) {
$ epoch = time ( ) ;
}
if ( ! defined ( $ longitudeEast ) || ! defined ( $ latitudeNorth ) ) {
# undefined: use Frankfurt, Germany
$ longitudeEast = :: AttrVal ( "global" , "longitude" , "8.686" ) ;
$ latitudeNorth = :: AttrVal ( "global" , "latitude" , "50.112" ) ;
}
#$longitudeEast = 5;
#$latitudeNorth = 52;
if ( ! defined ( $ altitude ) ) {
# undefined: use 0 m (see level)
$ altitude = 0 ;
}
#$altitude = 0;
if ( ! defined ( $ twilightAngle ) ) {
# undefined: use 0°
$ twilightAngle = 0 ;
}
#$twilightAngle = 0;
# initial estimate of solar transit date
my $ julianDateOffset = 2451545 + ( 32.184 + 37 ) /86400 - $longitudeEast/ 360 ; # 2019: 37 leap seconds
my $ julianCycle = floor ( EpochToJulianDate ( $ epoch ) - $ julianDateOffset + 0.5 ) ;
#$julianCycle = floor(2453097 - $julianDateOffset + 0.5);
my $ solarTransitJD = $ julianCycle + $ julianDateOffset ;
# improve estimate of solar transit date
my $ rad = pi / 180 ;
my $ meanSolarAnomalyRadians = MeanSolarAnomaly ( $ solarTransitJD ) * $ rad ;
my $ eclipticalLongitudeRadians = EclipticalLongitude ( $ meanSolarAnomalyRadians ) ;
$ solarTransitJD = Transit ( $ solarTransitJD , $ meanSolarAnomalyRadians , $ eclipticalLongitudeRadians ) ;
# iteratively improve solar transit date at given longitude
( $ solarTransitJD , my $ declinationRadians ) = HourAngleOptimization ( 0 , $ solarTransitJD , $ longitudeEast ) ;
# initial estimate of sun rise/set hour angle at given latitude and altitude
my $ hourAngleRiseSet = acos ( ( sin ( ( ElevationCorrection ( $ altitude ) + $ twilightAngle ) * $ rad ) - sin ( $ latitudeNorth * $ rad ) * sin ( $ declinationRadians ) ) /(cos($latitudeNorth*$rad)*cos($declinationRadians)))/ $ rad ;
my $ hourAngleRiseSetRatio = $ hourAngleRiseSet / 360 ;
# initial estimate of sun rise and sun set date
my $ sunRiseJD = $ solarTransitJD - $ hourAngleRiseSetRatio ;
my $ sunSetJD = $ solarTransitJD + $ hourAngleRiseSetRatio ;
# iteratively improve sun rise and sun set date
( $ sunRiseJD ) = HourAngleOptimization ( - 1 , $ sunRiseJD , $ longitudeEast , $ latitudeNorth , $ altitude , $ twilightAngle ) ;
( $ sunSetJD ) = HourAngleOptimization ( + 1 , $ sunSetJD , $ longitudeEast , $ latitudeNorth , $ altitude , $ twilightAngle ) ;
#::Log3 "", 3, "RiseSet: sunRiseJD $sunRiseJD solarTransitJD $solarTransitJD sunSetJD $sunSetJD";
return ( JulianDateToEpoch ( $ sunRiseJD ) , JulianDateToEpoch ( $ solarTransitJD ) , JulianDateToEpoch ( $ sunSetJD ) ) ;
}
# -----------------------------------------------------------------------------
package DWD_OpenData ;
use strict ;
use warnings ;
use Encode 'encode' ;
2024-03-03 17:20:14 +00:00
use File::Basename 'dirname' ;
2024-02-25 21:05:29 +00:00
use File::Temp 'tempfile' ;
use IO::Uncompress::Unzip qw( unzip $UnzipError ) ;
2024-03-03 17:20:14 +00:00
#use LWP::UserAgent;
2024-02-25 21:05:29 +00:00
use POSIX qw( floor strftime ) ;
use Scalar::Util 'looks_like_number' ;
use Storable qw( freeze thaw ) ;
use Time::HiRes qw( gettimeofday usleep ) ;
use Time::Local qw( timelocal timegm ) ;
use Time::Piece qw( localtime gmtime ) ;
use Blocking ;
use HttpUtils ;
use feature qw( switch ) ;
no if $] >= 5.017011 , warnings = > 'experimental::smartmatch' ;
use constant UPDATE_DISTRICTS = > - 1 ;
use constant UPDATE_COMMUNEUNIONS = > - 2 ;
use constant UPDATE_ALL = > - 3 ;
2024-03-03 17:20:14 +00:00
use constant DOWNLOAD_TIMEOUT_MIN = > 30 ; # [s]
use constant DOWNLOAD_TIMEOUT_MAX = > 120 ; # [s]
2024-02-28 22:44:32 +00:00
use constant DOWNLOAD_TIMEOUT_DEFAULT = > DOWNLOAD_TIMEOUT_MIN ; # [s]
2024-03-03 17:20:14 +00:00
use constant PROCESSING_TIMEOUT = > DOWNLOAD_TIMEOUT_MAX + 60 ; # [s]
use constant SCHEDULING_RANGE = > 15 * 60 - PROCESSING_TIMEOUT - 60 ; # [s]
2024-02-28 22:44:32 +00:00
2024-02-25 21:05:29 +00:00
require Exporter ;
2024-03-03 17:20:14 +00:00
our $ VERSION = '1.017003' ;
2024-02-25 21:05:29 +00:00
our @ ISA = qw( Exporter ) ;
our @ EXPORT = qw( GetForecast GetAlerts UpdateAlerts UPDATE_DISTRICTS UPDATE_COMMUNEUNIONS UPDATE_ALL ) ;
our @ EXPORT_OK = qw( IsCommuneUnionWarncellId ) ;
my % forecastPropertyAliases = ( 'TX' = > 'Tx' , 'TN' = > 'Tn' , 'TG' = > 'Tg' , 'TM' = > 'Tm' ) ;
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 , 'SunAz' = > 1 , 'SunD1' = > 1 , 'SunD3' = > 1 , 'SunEl' = > 1 , 'SunUp' = > 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 , 'SunRise' = > 24 , 'SunSet' = > 24 , 'Tg' = > 24 , 'Tm' = > 24 , 'Tn' = > 24 , 'Tx' = > 24
) ;
my % forecastDefaultPropertiesS = (
'Tn' = > 1 , 'Tx' = > 1 , 'DD' = > 1 , 'FX1' = > 1 , 'Neff' = > 1 , 'RR1c' = > 1 , 'R602' = > 1 , 'RR3c' = > 1 , 'Rh00' = > 1 , 'TTT' = > 1 , 'ww' = > 1 , 'SunUp' = > 1
) ;
my % forecastDefaultPropertiesL = (
'Tg' = > 1 , 'Tn' = > 1 , 'Tx' = > 1 , 'DD' = > 1 , 'FX1' = > 1 , 'Neff' = > 1 , 'RR6c' = > 1 , 'R600' = > 1 , 'RRhc' = > 1 , 'Rh00' = > 1 , 'TTT' = > 1 , 'ww' = > 1 , 'SunUp' = > 1
) ;
2024-02-28 22:44:32 +00:00
2024-02-25 21:05:29 +00:00
# conversion of DWD value to: 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 @ alertsData = [ undef , undef ] ;
my @ alertsReceived = [ undef , undef ] ;
my @ alertsUpdating = [ undef , undef ] ;
my @ alertsErrorMessage = [ undef , undef ] ;
= head1 FHEM CALLBACK FUNCTIONS
= head2 Define ( $$ )
FHEM I <DefFn>
= 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 Define {
my ( $ hash , $ def ) = @ _ ;
my $ name = $ hash - > { NAME } ;
# module version
$ hash - > { VERSION } = $ VERSION ;
# test TZ environment variable
if ( ! defined ( $ ENV { "TZ" } ) ) {
$ hash - > { FHEM_TZ } = undef ;
} else {
$ hash - > { FHEM_TZ } = $ ENV { "TZ" } ;
}
# cache timezone attribute
$ hash - > { '.TZ' } = :: AttrVal ( $ hash , 'timezone' , $ hash - > { FHEM_TZ } ) ;
:: readingsSingleUpdate ( $ hash , 'state' , :: IsDisabled ( $ name ) ? 'disabled' : 'defined' , 1 ) ;
2024-03-03 17:20:14 +00:00
# randomize start of next update check by SCHEDULING_RANGE to distribute load cause by mulitple module instances
my $ nextUpdate = gettimeofday ( ) + int ( rand ( SCHEDULING_RANGE ) ) ;
2024-02-25 21:05:29 +00:00
:: readingsSingleUpdate ( $ hash , 'nextUpdate' , :: FmtTime ( $ nextUpdate ) , 1 ) ;
:: InternalTimer ( $ nextUpdate , 'DWD_OpenData::Timer' , $ hash ) ;
$ hash - > { '.firstRun' } = 1 ;
return undef ;
}
= head2 Undef ( $$ )
FHEM I <UndefFn>
= over
= item * param hash: hash of DWD_OpenData device
= item * param arg: module undefine arguments , will be ignored
= back
= cut
sub Undef {
my ( $ hash , $ arg ) = @ _ ;
Shutdown ( $ hash ) ;
return undef ;
}
= head2 Shutdown ( $ )
FHEM I <ShutdownFn>
= over
= item * param hash: hash of DWD_OpenData device
= back
= cut
sub Shutdown {
my ( $ hash ) = @ _ ;
my $ name = $ hash - > { NAME } ;
:: RemoveInternalTimer ( $ hash ) ;
if ( defined ( $ hash - > { ".alertsBlockingCall" } ) ) {
:: BlockingKill ( $ hash - > { ".alertsBlockingCall" } ) ;
}
my $ warncellId = $ hash - > { ".warncellId" } ;
if ( defined ( $ warncellId ) ) {
my $ communeUnion = IsCommuneUnionWarncellId ( $ warncellId ) ;
if ( defined ( $ hash - > { ".alertsFile" . $ communeUnion } ) ) {
close ( $ hash - > { ".alertsFileHandle" . $ communeUnion } ) ;
unlink ( $ hash - > { ".alertsFile" . $ communeUnion } ) ;
delete ( $ hash - > { ".alertsFile" . $ communeUnion } ) ;
}
}
if ( defined ( $ hash - > { ".forecastBlockingCall" } ) ) {
:: BlockingKill ( $ hash - > { ".forecastBlockingCall" } ) ;
}
if ( defined ( $ hash - > { ".forecastFile" } ) ) {
close ( $ hash - > { ".forecastFileHandle" } ) ;
unlink ( $ hash - > { ".forecastFile" } ) ;
delete ( $ hash - > { ".forecastFile" } ) ;
}
return undef ;
}
= head2 Attr ( @ )
FHEM I <AttrFn>
= over
= 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 <undef> on success or error message
= back
= cut
sub Attr {
my ( $ command , $ name , $ attribute , $ value ) = @ _ ;
my $ hash = $ ::defs { $ name } ;
for ( $ command ) {
when ( "set" ) {
for ( $ attribute ) {
when ( "disable" ) {
# enable/disable polling
if ( $ ::init_done ) {
if ( $ value ) {
:: RemoveInternalTimer ( $ hash ) ;
:: readingsSingleUpdate ( $ hash , 'state' , 'disabled' , 1 ) ;
} else {
:: readingsSingleUpdate ( $ hash , 'state' , 'defined' , 1 ) ;
:: InternalTimer ( gettimeofday ( ) + 3 , 'DWD_OpenData::Timer' , $ hash , 0 ) ;
}
}
}
2024-03-01 23:25:12 +00:00
when ( "forecastRefresh" ) {
if ( ! ( defined ( $ value ) && looks_like_number ( $ value ) && $ value >= 1 && $ value <= 6 ) ) {
my $ oldRefresh = :: AttrVal ( $ name , 'forecastRefresh' , 6 ) ;
if ( $ ::init_done && ( ( $ oldRefresh < 6 && $ value >= 6 ) || ( $ oldRefresh >= 6 && $ value < 6 ) ) ) {
# delete readings when switching between MOSMIX S and L
:: CommandDeleteReading ( undef , "$name ^fc.*" ) ;
}
}
}
2024-02-25 21:05:29 +00:00
when ( "forecastResolution" ) {
if ( defined ( $ value ) && looks_like_number ( $ value ) && $ value > 0 ) {
my $ oldForecastResolution = :: AttrVal ( $ name , 'forecastResolution' , 6 ) ;
if ( $ ::init_done && defined ( $ oldForecastResolution ) && $ oldForecastResolution != $ value ) {
:: CommandDeleteReading ( undef , "$name ^fc.*" ) ;
}
} else {
return "invalid value for forecastResolution (possible values are 1, 3 and 6)" ;
}
}
when ( "downloadTimeout" ) {
2024-02-28 22:44:32 +00:00
if ( ! ( defined ( $ value ) && looks_like_number ( $ value ) && $ value >= DOWNLOAD_TIMEOUT_MIN && $ value <= DOWNLOAD_TIMEOUT_MAX ) ) {
2024-03-01 23:25:12 +00:00
return "invalid value for downloadTimeout (" . DOWNLOAD_TIMEOUT_MIN . " .. " . DOWNLOAD_TIMEOUT_MAX . ")" ;
2024-02-25 21:05:29 +00:00
}
}
when ( "forecastStation" ) {
my $ oldForecastStation = :: AttrVal ( $ name , 'forecastStation' , undef ) ;
if ( $ ::init_done && defined ( $ oldForecastStation ) && $ oldForecastStation ne $ value ) {
:: CommandDeleteReading ( undef , "$name ^fc.*" ) ;
}
}
when ( "forecastWW2Text" ) {
if ( $ ::init_done && ! $ value ) {
:: CommandDeleteReading ( undef , "$name ^fc.*wwd\$" ) ;
}
}
when ( "timezone" ) {
if ( defined ( $ value ) && length ( $ value ) > 0 ) {
$ hash - > { '.TZ' } = $ value ;
} else {
return "timezone (e.g. Europe/Berlin) required" ;
}
}
}
}
when ( "del" ) {
for ( $ attribute ) {
when ( "disable" ) {
:: readingsSingleUpdate ( $ hash , 'state' , 'defined' , 1 ) ;
:: InternalTimer ( gettimeofday ( ) + 3 , 'DWD_OpenData::Timer' , $ hash , 0 ) ;
}
when ( "forecastResolution" ) {
my $ oldForecastResolution = :: AttrVal ( $ name , 'forecastResolution' , 6 ) ;
if ( $ oldForecastResolution != 6 ) {
:: CommandDeleteReading ( undef , "$name ^fc.*" ) ;
}
}
when ( "forecastStation" ) {
:: CommandDeleteReading ( undef , "$name ^fc.*" ) ;
}
2024-03-01 23:25:12 +00:00
when ( "forecastResolution" ) {
2024-02-25 21:05:29 +00:00
:: CommandDeleteReading ( undef , "$name ^fc.*" ) ;
}
when ( "forecastWW2Text" ) {
:: CommandDeleteReading ( undef , "$name ^fc.*wwd\$" ) ;
}
when ( "timezone" ) {
$ hash - > { '.TZ' } = $ hash - > { FHEM_TZ } ;
}
}
}
}
return undef ;
}
= head2 Get ( $@ )
FHEM I <GetFn>
= over
= item * param hash: hash of DWD_OpenData device
= 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 Get {
my ( $ hash , @ a ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my $ result = undef ;
my $ command = lc ( $ a [ 1 ] ) ;
for ( $ command ) {
when ( "alerts" ) {
my $ warncellId = $ a [ 2 ] ;
$ warncellId = :: AttrVal ( $ name , 'alertArea' , undef ) if ( ! defined ( $ warncellId ) ) ;
if ( defined ( $ warncellId ) ) {
my $ communeUnion = IsCommuneUnionWarncellId ( $ warncellId ) ;
2024-02-28 22:44:32 +00:00
if ( defined ( $ alertsUpdating [ $ communeUnion ] ) && ( time ( ) - $ alertsUpdating [ $ communeUnion ] < PROCESSING_TIMEOUT ) ) {
2024-02-25 21:05:29 +00:00
# abort if update is in progress
$ result = "alerts cache update in progress, please wait and try again" ;
} elsif ( defined ( $ alertsReceived [ $ communeUnion ] ) && ( time ( ) - $ alertsReceived [ $ communeUnion ] < 900 ) ) {
# use cache if not older than 15 minutes
$ result = UpdateAlerts ( $ hash , $ warncellId ) ;
} else {
# update cache if older than 15 minutes
$ result = GetAlerts ( $ hash , $ warncellId ) ;
}
} else {
$ result = "warncell id required for $name get $command" ;
}
}
when ( "forecast" ) {
my $ station = $ a [ 2 ] ;
$ station = :: AttrVal ( $ name , 'forecastStation' , undef ) if ( ! defined ( $ station ) ) ;
if ( defined ( $ station ) ) {
2024-02-28 22:44:32 +00:00
if ( defined ( $ hash - > { forecastUpdating } ) && ( time ( ) - $ hash - > { forecastUpdating } < PROCESSING_TIMEOUT ) ) {
2024-02-25 21:05:29 +00:00
# abort if update is in progress
$ result = "forecast update in progress, please wait and try again" ;
} else {
2024-02-28 22:44:32 +00:00
# force forecast update
:: readingsSingleUpdate ( $ hash , 'fc_dwdDocSize' , 0 , 0 ) ;
2024-02-25 21:05:29 +00:00
delete $ hash - > { ".fetchAlerts" } ;
$ result = GetForecast ( $ hash , $ station ) ;
}
} else {
$ result = "station code required for $name get $command" ;
}
}
when ( "updatealertscache" ) {
my $ updateMode = undef ;
my $ option = lc ( $ a [ 2 ] ) ;
for ( $ option ) {
when ( "communeunions" ) {
$ updateMode = UPDATE_COMMUNEUNIONS ;
}
when ( "districts" ) {
$ updateMode = UPDATE_DISTRICTS ;
}
when ( "all" ) {
$ updateMode = UPDATE_ALL ;
}
default {
return "update mode 'communeUnions', 'districts' or 'all' required for $name get $command" ;
}
}
my $ communeUnion = IsCommuneUnionWarncellId ( $ updateMode ) ;
2024-02-28 22:44:32 +00:00
if ( defined ( $ alertsUpdating [ $ communeUnion ] ) && ( time ( ) - $ alertsUpdating [ $ communeUnion ] < PROCESSING_TIMEOUT ) ) {
2024-02-25 21:05:29 +00:00
# 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 = GetAlerts ( $ hash , $ updateMode ) ;
}
}
default {
$ result = "unknown get command $command, choose one of alerts forecast updateAlertsCache:communeUnions,districts,all" ;
}
}
return $ result ;
}
= head2 Timer ( $ )
FHEM I <InternalTimer> function
= over
= item * param args: hash of DWD_OpenData device
= back
= cut
sub Timer {
my ( $ hash ) = @ _ ;
my $ name = $ hash - > { NAME } ;
2024-02-28 22:44:32 +00:00
2024-02-25 21:05:29 +00:00
:: Log3 $ name , 5 , "$name: Timer START" ;
my $ time = time ( ) ;
my ( $ tSec , $ tMin , $ tHour , $ tMday , $ tMon , $ tYear , $ tWday , $ tYday , $ tIsdst ) = gmtime ( $ time ) ;
2024-02-28 22:44:32 +00:00
my $ actQuarter = int ( $ tMin / 15 ) ; # 0 .. 3
2024-02-25 21:05:29 +00:00
# cancel periodic timer
:: RemoveInternalTimer ( $ hash ) ;
2024-02-28 22:44:32 +00:00
# perform updates every quarter of an hour: alerts=every, forecast=specific
2024-02-25 21:05:29 +00:00
my $ firstRun = delete $ hash - > { '.firstRun' } // 0 ;
2024-03-01 23:25:12 +00:00
my $ forecastQuarter = :: AttrVal ( $ name , 'forecastRefresh' , 6 ) >= 6 ? 0 : 2 ;
2024-02-28 22:44:32 +00:00
my $ fetchAlerts = defined ( $ hash - > { ".fetchAlerts" } ) && $ hash - > { ".fetchAlerts" } ; # fetch either alerts or forecast
2024-03-01 23:25:12 +00:00
:: Log3 $ name , 5 , "$name: Timer first:$firstRun forecastQuarter:$forecastQuarter fetchAlerts:$fetchAlerts" ;
2024-02-28 22:44:32 +00:00
# update forecast and alerts immediately at startup
$ forecastQuarter = $ actQuarter if ( $ firstRun ) ;
if ( $ actQuarter == $ forecastQuarter && ! $ fetchAlerts ) {
# preset: fetch alerts if not fetching forecast
2024-02-25 21:05:29 +00:00
$ hash - > { ".fetchAlerts" } = 1 ;
my $ forecastStation = :: AttrVal ( $ name , 'forecastStation' , undef ) ;
if ( defined ( $ forecastStation ) ) {
2024-02-28 22:44:32 +00:00
if ( ! defined ( $ hash - > { forecastUpdating } ) || ( $ time - $ hash - > { forecastUpdating } >= PROCESSING_TIMEOUT ) ) {
2024-02-25 21:05:29 +00:00
my $ result = GetForecast ( $ hash , $ forecastStation ) ;
if ( defined ( $ result ) ) {
:: Log3 $ name , 4 , "$name: error retrieving forecast: $result" ;
} else {
2024-02-28 22:44:32 +00:00
# fetching forecast started: wait for forecast fetch to complete before fetching alerts
2024-02-25 21:05:29 +00:00
$ hash - > { ".fetchAlerts" } = 0 ;
}
}
}
}
$ fetchAlerts = defined ( $ hash - > { ".fetchAlerts" } ) && $ hash - > { ".fetchAlerts" } ;
2024-02-28 22:44:32 +00:00
if ( $ actQuarter != $ forecastQuarter || $ fetchAlerts ) {
2024-02-25 21:05:29 +00:00
my $ warncellId = :: AttrVal ( $ name , 'alertArea' , undef ) ;
if ( defined ( $ warncellId ) ) {
# skip update if already in progress
my $ communeUnion = IsCommuneUnionWarncellId ( $ warncellId ) ;
2024-02-28 22:44:32 +00:00
if ( ! defined ( $ alertsUpdating [ $ communeUnion ] ) || ( $ time - $ alertsUpdating [ $ communeUnion ] >= PROCESSING_TIMEOUT ) ) {
2024-02-25 21:05:29 +00:00
my $ result = GetAlerts ( $ hash , $ warncellId ) ;
if ( defined ( $ result ) ) {
:: Log3 $ name , 4 , "$name: error retrieving alerts: $result" ;
}
}
}
2024-02-28 22:44:32 +00:00
# fetch forecast at next timer execution
$ hash - > { ".fetchAlerts" } = 0 ;
2024-02-25 21:05:29 +00:00
}
2024-03-03 17:20:14 +00:00
# reschedule next run to 5 .. SCHEDULING_RANGE seconds past next quarter
my $ nextUpdate = timegm ( 0 , $ actQuarter * 15 , $ tHour , $ tMday , $ tMon , $ tYear ) + 905 + int ( rand ( SCHEDULING_RANGE - 5 ) ) ;
2024-02-25 21:05:29 +00:00
:: readingsSingleUpdate ( $ hash , 'nextUpdate' , :: FmtTime ( $ nextUpdate ) , 1 ) ;
:: InternalTimer ( $ nextUpdate , 'DWD_OpenData::Timer' , $ hash ) ;
:: 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' } ;
}
my $ t = timelocal ( @ ta ) ;
if ( defined ( $ hash - > { FHEM_TZ } ) ) {
$ ENV { "TZ" } = $ hash - > { FHEM_TZ } ;
} else {
delete $ ENV { "TZ" } ;
}
return $ t ;
}
= head2 Localtime ( @ )
= over
= item * param hash: hash of DWD_OpenData device
= item * param t: epoch seconds
= item * return localtime array in device timezone
= back
= cut
sub Localtime {
my ( $ hash , $ t ) = @ _ ;
if ( defined ( $ hash - > { '.TZ' } ) ) {
$ ENV { "TZ" } = $ hash - > { '.TZ' } ;
}
my @ ta = localtime ( $ t ) ;
if ( defined ( $ hash - > { FHEM_TZ } ) ) {
$ ENV { "TZ" } = $ hash - > { FHEM_TZ } ;
} else {
delete $ ENV { "TZ" } ;
}
return @ ta ;
}
= head2 LocaltimeOffset ( @ )
= over
= item * param hash: hash of DWD_OpenData device
= item * param t: epoch seconds
= item * return time zone offset [ s ]
= back
= cut
sub LocaltimeOffset {
my ( $ hash , $ t ) = @ _ ;
if ( defined ( $ hash - > { '.TZ' } ) ) {
$ ENV { "TZ" } = $ hash - > { '.TZ' } ;
}
my $ z = strftime ( '%z' , localtime ( $ t ) ) ;
my $ tzo = 3600 * floor ( $ z / 100 ) + 60 * ( $ z % 100 ) ;
if ( defined ( $ hash - > { FHEM_TZ } ) ) {
$ ENV { "TZ" } = $ hash - > { FHEM_TZ } ;
} else {
delete $ ENV { "TZ" } ;
}
return $ tzo ;
}
2024-02-28 22:44:32 +00:00
= head2 FormatDateTimeUTC ( $ )
= over
= item * param t: epoch seconds
2024-03-01 23:25:12 +00:00
= item * return date time string with with format "YYYY-MM-DD HH:MM:SSZ" with UTC timezone
2024-02-28 22:44:32 +00:00
= back
= cut
sub FormatDateTimeUTC {
2024-03-01 23:25:12 +00:00
return :: strftime ( '%Y-%m-%d %H:%M:%SZ' , gmtime ( @ _ ) ) ;
2024-02-28 22:44:32 +00:00
}
= head2 ParseDateTimeUTC ( $$ )
= over
2024-03-01 23:25:12 +00:00
= item * param s: date string with format "YYYY-MM-DD HH:MM:SSZ" with UTC timezone
2024-02-28 22:44:32 +00:00
= item * return epoch seconds or C <undef> on error
= back
= cut
sub ParseDateTimeUTC {
my $ t ;
2024-03-01 23:25:12 +00:00
eval { $ t = :: timegm ( :: strptime ( @ _ , '%Y-%m-%d %H:%M:%S%z' ) ) } ;
2024-02-28 22:44:32 +00:00
return $ t ;
}
2024-02-25 21:05:29 +00:00
= head2 FormatDateTimeLocal ( $$ )
= over
= 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 FormatDateTimeLocal {
return strftime ( '%Y-%m-%d %H:%M:%S' , Localtime ( @ _ ) ) ;
}
= head2 FormatDateLocal ( $$ )
= over
= 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 FormatDateLocal {
return strftime ( '%Y-%m-%d' , Localtime ( @ _ ) ) ;
}
= head2 FormatTimeLocal ( $$ )
= over
= 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 FormatTimeLocal {
return strftime ( '%H:%M' , Localtime ( @ _ ) ) ;
}
= head2 FormatWeekdayLocal ( $$ )
= over
= item * param hash: hash of DWD_OpenData device
= item * param t: epoch seconds
= item * return abbreviated weekday name in device timezone
= back
= cut
sub FormatWeekdayLocal {
return strftime ( '%a' , Localtime ( @ _ ) ) ;
}
= head2 ParseDateTimeLocal ( $$ )
= over
= 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 <undef> on error
= back
= cut
sub ParseDateTimeLocal {
my ( $ hash , $ s ) = @ _ ;
my $ t ;
eval { $ t = Timelocal ( $ hash , :: strptime ( $ s , '%Y-%m-%d %H:%M:%S' ) ) } ;
return $ t ;
}
= head2 ParseDateLocal ( $$ )
= over
= 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 <undef> on error
= back
= cut
sub ParseDateLocal {
my ( $ hash , $ s ) = @ _ ;
my $ t ;
eval { $ t = Timelocal ( $ hash , :: strptime ( $ s , '%Y-%m-%d' ) ) } ;
return $ t ;
}
= head2 ParseCAPTime ( $ )
= over
= item * param s: time string with format "YYYY-MM-DDThh:mm:ssZZZ:ZZ"
= item * return epoch seconds
= back
= cut
sub ParseCAPTime {
my ( $ s ) = @ _ ;
$ s =~ s | ( . + ) : | $ 1 | ; # remove colon from time zone offset
#Log 1, "ParseCAPTime: " . $s;
return Time::Piece - > strptime ( $ s , '%Y-%m-%dT%H:%M:%S%z' ) - > epoch ;
}
= head2 ParseKMLTime ( $ )
= over
= item * param s: time string with format "YYYY-MM-DDThh:mm:ss.000Z"
= item * return epoch seconds
= back
= cut
sub ParseKMLTime {
my ( $ s ) = @ _ ;
$ s =~ s | ( . + ) \ .000 Z | $ 1 | ; # remove milliseconds and timezone
return Time::Piece - > strptime ( $ s , '%Y-%m-%dT%H:%M:%S' ) - > epoch ;
}
= head2 IsCommuneUnionWarncellId ( $ )
= over
= item * param warncellId: numeric wanrcell id
= item * return true if warncell id belongs to commune union group
= back
= cut
sub IsCommuneUnionWarncellId {
my ( $ warncellId ) = @ _ ;
return int ( $ warncellId /100000000) == 5 || int($warncellId/ 100000000 ) == 7 || 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 ) ) ) {
$ daysAvailable + + ;
}
:: Log3 $ name , 5 , "$name: RotateForecast: START $daysAvailable day(s) exist" ;
my $ oT = :: ReadingsVal ( $ name , 'fc0_date' , undef ) ;
my $ oldToday = defined ( $ oT ) ? ParseDateLocal ( $ hash , $ oT ) : undef ;
my $ stationChanged = :: ReadingsVal ( $ name , 'fc_station' , '' ) ne $ station ;
if ( $ stationChanged ) {
# different station, delete all existing readings
:: Log3 $ name , 3 , "$name: RotateForecast: station has changed, deleting exisiting 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 ) = Localtime ( $ hash , $ time ) ;
$ today = Timelocal ( $ hash , 0 , 0 , 0 , $ tMday , $ tMon , $ tYear ) ;
}
my $ daysForward = sprintf ( "%0.0f" , ( $ today - $ oldToday ) / 86400.0 ) ; # round() [s] -> [d]
:: Log3 $ name , $ daysForward > 0 ? 4 : 5 , "$name: RotateForecast: shifting forward by $daysForward day(s) ($oldToday -> $today)" ;
if ( $ daysForward > 0 ) {
# different day
if ( $ daysForward < $ daysAvailable ) {
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 < 24 / $ forecastResolution ; $ s + + ) {
push ( @ shiftProperties , $ s . '_time' ) ;
push ( @ shiftProperties , $ s . '_wwd' ) ;
}
# shift readings forward by days keeping reading timestamps
for ( my $ d = 0 ; $ d < ( $ daysAvailable - $ daysForward ) ; $ d + + ) {
my $ sourcePrefix = 'fc' . ( $ daysForward + $ d ) . '_' ;
my $ destinationPrefix = 'fc' . $ d . '_' ;
foreach my $ property ( @ shiftProperties ) {
my $ sourceReading = $ sourcePrefix . $ property ;
my $ destinationReading = $ destinationPrefix . $ property ;
my $ sourceValue = :: ReadingsVal ( $ name , $ sourceReading , undef ) ;
if ( defined ( $ sourceValue ) ) {
my $ timestamp = $ hash - > { READINGS } { $ sourceReading } { TIME } ;
:: readingsBulkUpdate ( $ hash , $ destinationReading , $ sourceValue ) ;
$ hash - > { READINGS } { $ destinationReading } { TIME } = $ timestamp ;
} else {
my $ destinationValue = :: ReadingsVal ( $ name , $ destinationReading , undef ) ;
if ( defined ( $ destinationValue ) ) {
:: Log3 $ name , 3 , "$name: RotateForecast WARNING: deleting reading $destinationReading because the source value $sourceReading is undefined" ;
:: CommandDeleteReading ( undef , "$name $destinationReading" ) ;
}
}
}
}
# delete existing readings of all days that have not been written
for ( my $ d = ( $ daysAvailable - $ daysForward ) ; $ d < $ daysAvailable ; $ d + + ) {
:: CommandDeleteReading ( undef , "$name ^fc" . $ d . "_.*" ) ;
}
$ daysAvailable -= $ daysForward ;
} else {
# nothing remains after shifting, delete existing day readings
:: Log3 $ name , 3 , "$name: RotateForecast WARNING: deleting all readings because no forecast data remains for rotation" ;
:: CommandDeleteReading ( undef , "$name ^fc\\d+.*" ) ;
$ daysAvailable = 0 ;
}
}
}
:: Log3 $ name , 5 , "$name: RotateForecast: END $daysAvailable day(s) remain" ;
return $ daysAvailable ;
}
= head2 PruneForecast ( $ )
find youngest reading of each day and delete all readings that are older than 1 day
excluding the readings "day" , "time" and "weekday"
= over
= item * param hash: hash of DWD_OpenData device
= back
= cut
sub PruneForecast {
my ( $ hash ) = @ _ ;
my $ name = $ hash - > { NAME } ;
if ( :: AttrVal ( $ name , 'forecastPruning' , 0 ) != 1 ) {
return ;
}
:: Log3 $ name , 5 , "$name: PruneForecast: START" ;
my @ readingNames = ( grep { /^fc/ } keys % { $ hash - > { READINGS } } ) ;
# find youngest timestamp per day
my % youngestTimestamps ;
foreach my $ readingName ( @ readingNames ) {
if ( ! ( $ readingName =~ m/^fc\d*_(day|time|weekday)$/ ) ) {
my @ parts = $ readingName =~ /^fc(\d+)_.*/ ;
if ( scalar ( @ parts ) == 1 ) {
my $ relativeDay = $ parts [ 0 ] ;
my $ timestamp = :: time_str2num ( $ hash - > { READINGS } { TIME } ) ;
my $ youngestTimestamp = $ youngestTimestamps { $ relativeDay } ;
if ( ! defined ( $ youngestTimestamp ) || $ timestamp > $ youngestTimestamp ) {
$ youngestTimestamps { $ relativeDay } = $ timestamp ;
}
}
}
}
# delete readings that are too old
foreach my $ readingName ( @ readingNames ) {
if ( ! ( $ readingName =~ m/^fc\d*_(day|time|weekday)$/ ) ) {
my @ parts = $ readingName =~ /^fc(\d+)_.*/ ;
if ( scalar ( @ parts ) == 1 ) {
my $ relativeDay = $ parts [ 0 ] ;
my $ timestamp = :: time_str2num ( $ hash - > { READINGS } { TIME } ) ;
my $ youngestTimestamp = $ youngestTimestamps { $ relativeDay } ;
if ( defined ( $ youngestTimestamp ) && $ timestamp < ( $ youngestTimestamp - 86400 ) ) {
:: Log3 $ name , 3 , "$name: PruneForecast WARNING: deleting reading $readingName because it is more than 1 day older than all other readings of the same day" ;
:: CommandDeleteReading ( undef , "$name $readingName" ) ;
}
}
}
}
:: Log3 $ name , 5 , "$name: PruneForecast: END" ;
}
sub ProcessForecast ;
= head2 GetForecast ( $$ )
= over
= item * param hash: hash of DWD_OpenData device
= item * param station: station name , string
= back
= cut
sub GetForecast {
my ( $ hash , $ station ) = @ _ ;
my $ name = $ hash - > { NAME } ;
if ( ! :: IsDisabled ( $ name ) ) {
:: Log3 $ name , 5 , "$name: GetForecast START (PID $$)" ;
# test if XML module is available
eval {
require XML::LibXML ;
} ;
if ( $@ ) {
return "$name: Perl module XML::LibXML not found, see commandref for details how to fix" ;
}
# download, unzip and parse using BlockingCall
if ( defined ( $ hash - > { ".forecastFile" } ) ) {
# delete old temp file
close ( $ hash - > { ".forecastFileHandle" } ) ;
unlink ( $ hash - > { ".forecastFile" } ) ;
}
( $ hash - > { ".forecastFileHandle" } , $ hash - > { ".forecastFile" } ) = tempfile ( UNLINK = > 1 ) ;
$ hash - > { ".station" } = $ station ;
if ( defined ( $ hash - > { ".forecastBlockingCall" } ) ) {
# kill old blocking call
:: BlockingKill ( $ hash - > { ".forecastBlockingCall" } ) ;
}
2024-02-28 22:44:32 +00:00
my $ timeout = :: AttrVal ( $ name , 'downloadTimeout' , DOWNLOAD_TIMEOUT_DEFAULT ) ;
2024-02-25 21:05:29 +00:00
$ hash - > { ".forecastBlockingCall" } = :: BlockingCall ( "DWD_OpenData::GetForecastStart" , $ hash , "DWD_OpenData::GetForecastFinish" , $ timeout , "DWD_OpenData::GetForecastAbort" , $ hash ) ;
$ hash - > { forecastUpdating } = time ( ) ;
:: readingsSingleUpdate ( $ hash , 'state' , 'updating forecasts' , 1 ) ;
:: Log3 $ name , 5 , "$name: GetForecast END" ;
return undef ;
} else {
return "disabled" ;
}
}
2024-02-28 22:44:32 +00:00
= head2 GetHeaders ( $ )
= over
2024-03-03 17:20:14 +00:00
= item * param name: name of DWD_OpenData device
2024-02-28 22:44:32 +00:00
= item * param url: URL for wich the HTTP headers should be retrieved .
2024-03-03 17:20:14 +00:00
= item * return hash of header entries: content_length ( bytes ) , last_modified ( epoch time ) or C <undef> on error
2024-02-28 22:44:32 +00:00
= back
= cut
2024-03-03 17:20:14 +00:00
#sub GetHeaders {
# my ($name, $url) = @_;
# my $ua = new LWP::UserAgent(env_proxy => 1, timeout => 5, agent => 'fhem');
# my $request = new HTTP::Request('HEAD' => $url);
# $request->header('Accept' => 'text/html');
# my $response = $ua->request($request);
# if ($response->is_success()) {
# return $response;
# }
# return undef;
#}
2024-02-28 22:44:32 +00:00
sub GetHeaders {
2024-03-03 17:20:14 +00:00
my ( $ name , $ url ) = @ _ ;
my $ param = {
url = > $ url ,
method = > "HEAD" ,
timeout = > :: AttrVal ( $ name , 'downloadTimeout' , DOWNLOAD_TIMEOUT_DEFAULT )
} ;
my ( $ httpError , $ content ) = :: HttpUtils_BlockingGet ( $ param ) ;
if ( ! $ httpError ) {
my @ entries = split /\n/ , $ param - > { httpheader } ;
my % headers ;
for my $ entry ( @ entries ) {
if ( $ entry =~ /Content-Length/xs ) {
( $ headers { content_length } ) = $ entry =~ /Content-Length:\s([0-9]*)/xs ;
} elsif ( $ entry =~ /Last-Modified/xs ) {
my ( $ lastModified ) = $ entry =~ /Last-Modified:\s(.*)/xs ;
eval {
$ headers { last_modified } = :: timegm ( :: strptime ( $ lastModified , '%a, %d %b %Y %H:%M:%S %Z' ) )
} ;
}
}
return % headers ;
2024-02-28 22:44:32 +00:00
}
return undef ;
}
2024-03-01 23:25:12 +00:00
= head2 IsDocumentUpdated ( $$ $ )
Check if a web document was updated by comparing the webserver header info with reading values .
= over
= item * param hash: hash of DWD_OpenData device
= item * param url: URL for wich the HTTP headers should be retrieved .
= item * param prefix: reading name prefix ( 'fc' or 'a' ) for document size and timestamp
= item * param docSize: output , size [ bytes ] of the web document
= item * param docTime: output , timestamp [ UTC ] of the web document
2024-03-03 17:20:14 +00:00
= item * return true if new and old document properties differ
2024-03-01 23:25:12 +00:00
= back
= cut
sub IsDocumentUpdated {
my ( $ hash , $ url , $ prefix ) = @ _ ;
my $ name = $ hash - > { NAME } ;
# check if file on webserver was modified
:: Log3 $ name , 5 , "$name: IsDocumentUpdated BEFORE" ;
2024-03-03 17:20:14 +00:00
my % headers = GetHeaders ( $ name , $ url ) ;
2024-03-01 23:25:12 +00:00
my $ update = 1 ;
2024-03-03 17:20:14 +00:00
if ( % headers ) {
$ _ [ 3 ] = $ headers { content_length } ; # docSize
$ _ [ 4 ] = FormatDateTimeUTC ( $ headers { last_modified } ) ; # docTime
2024-03-01 23:25:12 +00:00
my $ lastURL = :: ReadingsVal ( $ name , $ prefix . '_url' , '' ) ;
my $ lastSize = :: ReadingsVal ( $ name , $ prefix . '_dwdDocSize' , 0 ) ;
my $ lastTime = :: ReadingsVal ( $ name , $ prefix . '_dwdDocTime' , '' ) ;
my $ emptyAlertsZipSize = 22 ; # bytes of empty zip file
:: Log3 $ name , 5 , "$name: IsDocumentUpdated docSize:$_[3]/$lastSize docTime:$_[4]/$lastTime URL:$url/$lastURL" ;
if ( $ url eq $ lastURL && ( $ _ [ 3 ] == $ lastSize && $ _ [ 4 ] eq $ lastTime ) || ( $ prefix eq 'a' && $ _ [ 3 ] == $ emptyAlertsZipSize && $ lastSize == $ emptyAlertsZipSize ) ) {
# not modified
$ update = 0 ;
}
}
else
{
# headers not available
$ _ [ 3 ] = 0 ; # docSize
$ _ [ 4 ] = '' ; # docTime
}
:: Log3 $ name , 5 , "$name: IsDocumentUpdated AFTER" ;
return $ update ;
}
2024-03-03 17:20:14 +00:00
= over
extract parts of exception stacktrace and log
= cut
sub ConvertToErrorMessage {
my ( $ exception , $ name , $ sub ) = @ _ ;
# exception?
my $ errorMessage = '' ;
if ( $ exception ) {
my @ parts = split ( / at |\n/ , $ exception ) ; # discard anything after " at " or newline
if ( @ parts ) {
$ errorMessage = $ parts [ 0 ] ;
:: Log3 $ name , 4 , "$name: $sub ERROR: $parts[0]" ;
} else {
$ errorMessage = $@ ;
:: Log3 $ name , 4 , "$name: $sub ERROR: $exception" ;
}
}
# get rid of newlines and commas because of Blocking InformFn parameter restrictions
$ errorMessage =~ s/\n/; /g ;
$ errorMessage =~ s/,/;/g ;
return $ errorMessage ;
}
= over
download forecast kmz file from URL into a string variable and unzip string content into a string array with one entry per file in zip
= over
= item * param name: name of DWD_OpenData device
= item * param param: parameter hash from call to HttpUtils_NonblockingGet
= item * return array of file contents ( one per file , typically one )
= back
= cut
sub GetForecastDataDiskless {
my ( $ name , $ param ) = @ _ ;
# download forecast document into variable
my @ fileContent ;
my ( $ httpError , $ zipFileContent ) = :: HttpUtils_BlockingGet ( $ param ) ;
eval {
my $ url = $ param - > { url } ;
my $ code = $ param - > { code } ;
if ( defined ( $ httpError ) && length ( $ httpError ) > 0 ) {
die "error retrieving URL '$url': $httpError" ;
}
if ( defined ( $ code ) && $ code != 200 ) {
die "HTTP error $code retrieving URL '$url'" ;
}
if ( ! defined ( $ zipFileContent ) || length ( $ zipFileContent ) == 0 ) {
die "no data retrieved from URL '$url'" ;
}
:: Log3 $ name , 5 , "$name: GetForecastDataDiskless: data received, unzipping ..." ;
# create memory mapped file from received data and unzip into string array
open my $ zipFileHandle , '<' , \ $ zipFileContent ;
unzip ( $ zipFileHandle = > \ @ fileContent , MultiStream = > 1 , AutoClose = > 1 ) or die "unzip failed: $UnzipError\n" ;
} ;
return ( ConvertToErrorMessage ( $@ , $ name , 'GetForecastDataDiskless' ) , \ @ fileContent ) ;
}
= over
download forecast kmz file from URL into a string variable , unzip into temp file and filter forecast data for station into a string
= over
= item * param name: name of DWD_OpenData device
= item * param param: parameter hash from call to HttpUtils_NonblockingGet
= item * return array of file contents ( one per file , typically one )
= back
= cut
sub GetForecastDataUsingFile {
my ( $ name , $ param ) = @ _ ;
# download forecast document into variable
my @ fileContent ;
my ( $ httpError , $ zipFileContent ) = :: HttpUtils_BlockingGet ( $ param ) ;
eval {
my $ url = $ param - > { url } ;
my $ code = $ param - > { code } ;
if ( defined ( $ httpError ) && length ( $ httpError ) > 0 ) {
die "error retrieving URL '$url': $httpError" ;
}
if ( defined ( $ code ) && $ code != 200 ) {
die "HTTP error $code retrieving URL '$url'" ;
}
if ( ! defined ( $ zipFileContent ) || length ( $ zipFileContent ) == 0 ) {
die "no data retrieved from URL '$url'" ;
}
:: Log3 $ name , 5 , "$name: GetForecastDataDiskless: data received, unzipping ..." ;
# unzip to temp file
open ( my $ zipFileHandle , '<' , \ $ zipFileContent ) or die "unable to open file $!" ;
my $ hash = $ param - > { hash } ;
my $ station = $ param - > { station } ;
my $ kmlFileName = dirname ( $ hash - > { ".forecastFile" } ) . "/" . "forecast-$station.kml" ;
unzip ( $ zipFileHandle = > $ kmlFileName , MultiStream = > 1 , AutoClose = > 1 ) or die "unzip failed: $UnzipError\n" ;
my $ kmlFileSize = - s $ kmlFileName ;
:: Log3 $ name , 5 , "$name: GetForecastDataDiskless: unzipped " . $ kmlFileSize . " bytes, filtering ..." ;
# read temp file content into string
open ( my $ kmlFileHandle , '<' , $ kmlFileName ) or die "unable to open file $!" ;
#read($kmlFileHandle, my $fileData, -s $kmlFileHandle);
my $ fileData = '' ;
my $ phase = 0 ; # copy header
my $ station = $ param - > { station } ;
while ( my $ line = <$kmlFileHandle> ) {
if ( $ line =~ /<kml:name/ ) {
if ( $ line =~ /$station/ ) {
$ phase = 2 ; # copy station data
} else {
$ phase = 1 ; # skip station data
}
} elsif ( $ phase == 2 && $ line =~ /<\/kml:Placemark/ ) {
$ phase = 3 ; # done
}
if ( $ phase == 0 || $ phase == 2 ) {
# copy line
$ fileData . = $ line ;
} elsif ( $ phase == 3 ) {
# finalize document
$ fileData . = $ line . " </kml:Document>\n" . "</kml:kml>" ;
$ phase = 4 ;
last ;
}
}
close ( $ kmlFileHandle ) ;
unlink ( $ kmlFileName ) ;
:: Log3 $ name , 5 , "$name: GetForecastDataDiskless: filtered " . length ( $ fileData ) . " bytes" ;
push ( @ fileContent , \ $ fileData ) ;
} ;
return ( ConvertToErrorMessage ( $@ , $ name , 'GetForecastDataDiskless' ) , \ @ fileContent ) ;
}
2024-02-25 21:05:29 +00:00
= head2 GetForecastStart ( $ )
BlockingCall I <BlockingFn> callback
= over
= item * param hash: hash of DWD_OpenData device
= item * return result required by function L </GetForecastFinish(@)>
= 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 GetForecastStart {
my ( $ hash ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my $ station = $ hash - > { ".station" } ;
# give main process time to execute
usleep ( 100 ) ;
# get forecast for station from DWD server
2024-02-28 22:44:32 +00:00
my $ url ;
2024-03-01 23:25:12 +00:00
my $ mosmixType = :: AttrVal ( $ name , 'forecastRefresh' , 6 ) < 6 ? 'S' : 'L' ;
if ( $ mosmixType eq 'S' ) {
2024-02-25 21:05:29 +00:00
$ url = "https://opendata.dwd.de/weather/local_forecasts/mos/MOSMIX_S/all_stations/kml/MOSMIX_S_LATEST_240.kmz" ;
} else {
2024-02-28 22:44:32 +00:00
$ url = 'https://opendata.dwd.de/weather/local_forecasts/mos/MOSMIX_L/single_stations/' . $ station . '/kml/MOSMIX_L_LATEST_' . $ station . '.kmz' ;
2024-02-25 21:05:29 +00:00
}
2024-03-01 23:25:12 +00:00
# determine if a new forecast report should be downloaded
my ( $ dwdDocSize , $ dwdDocTime ) ;
my $ update = IsDocumentUpdated ( $ hash , $ url , 'fc' , $ dwdDocSize , $ dwdDocTime ) ;
my $ lastDocSize = :: ReadingsVal ( $ name , 'fc_dwdDocSize' , 0 ) ;
my $ lastDocTimestamp = ParseDateTimeUTC ( :: ReadingsVal ( $ name , 'fc_dwdDocTime' , '1970-01-01 00:00:00Z' ) ) ;
my $ dwdDocTimestamp = length ( $ dwdDocTime ) ? ParseDateTimeUTC ( $ dwdDocTime ) : time ( ) ;
my $ maxDocAge = ( :: AttrVal ( $ name , 'forecastRefresh' , 6 ) - 0.5 ) * 60 * 60 ; # [s]
$ update = $ update && ( $ lastDocSize == 0 || ( $ dwdDocTimestamp - $ lastDocTimestamp ) >= $ maxDocAge ) ;
2024-02-28 22:44:32 +00:00
2024-03-03 17:20:14 +00:00
:: Log3 $ name , 5 , "$name: GetForecastStart $dwdDocTime $dwdDocTimestamp $lastDocTimestamp $maxDocAge $lastDocSize : $update" ;
2024-02-28 22:44:32 +00:00
my $ result ;
if ( $ update ) {
2024-03-03 17:20:14 +00:00
# define download and processing properties
2024-02-28 22:44:32 +00:00
my $ param = {
url = > $ url ,
method = > "GET" ,
timeout = > :: AttrVal ( $ name , 'downloadTimeout' , DOWNLOAD_TIMEOUT_DEFAULT ) ,
hash = > $ hash ,
station = > $ station ,
2024-03-01 23:25:12 +00:00
mosmixType = > $ mosmixType ,
dwdDocSize = > $ dwdDocSize ,
dwdDocTime = > $ dwdDocTime
2024-02-28 22:44:32 +00:00
} ;
2024-03-03 17:20:14 +00:00
# download and unpack forecast report
2024-02-28 22:44:32 +00:00
:: Log3 $ name , 5 , "$name: GetForecastStart START (PID $$): $url" ;
2024-03-03 17:20:14 +00:00
my ( $ errorMessage , $ fileContent ) ;
if ( $ dwdDocSize == 0 || $ dwdDocSize > 1000000 ) {
( $ errorMessage , $ fileContent ) = GetForecastDataUsingFile ( $ name , $ param ) ;
} else {
( $ errorMessage , $ fileContent ) = GetForecastDataDiskless ( $ name , $ param ) ;
}
2024-02-28 22:44:32 +00:00
2024-03-03 17:20:14 +00:00
# process forecast data
if ( length ( $ errorMessage ) ) {
$ result = [ $ name , $ errorMessage ] ;
} else {
$ result = ProcessForecast ( $ param , $ fileContent ) ;
}
2024-02-28 22:44:32 +00:00
:: Log3 $ name , 5 , "$name: GetForecastStart END" ;
} else {
# already up to date
$ result = [ $ name , 'up-to-date' ] ;
2024-02-25 21:05:29 +00:00
2024-02-28 22:44:32 +00:00
:: Log3 $ name , 5 , "$name: GetForecastStart UP-TO-DATE" ;
}
2024-02-25 21:05:29 +00:00
return $ result ;
}
= head2 getStationPos ( $$ $ )
= over
= item * param name: name of DWD_OpenData device
= item * param station: name of station to search for
= item * param placemarkNodeList: XML node to search
= item * index in list ( 1 .. ) or 0 if not found
= back
find XML node of station
= cut
sub getStationPos {
my $ name = shift ;
my $ station = shift ;
my $ placemarkNodeList = shift ;
2024-02-28 22:44:32 +00:00
my $ pos = 0 ;
2024-02-25 21:05:29 +00:00
my $ listSize = $ placemarkNodeList - > size ( ) ;
2024-02-28 22:44:32 +00:00
LOOP:
2024-02-25 21:05:29 +00:00
for my $ n ( 1 .. $ listSize ) {
my $ pn = $ placemarkNodeList - > get_node ( $ n ) ;
2024-02-28 22:44:32 +00:00
for my $ placemarkChildNode ( $ pn - > nonBlankChildNodes ( ) ) {
2024-02-25 21:05:29 +00:00
if ( $ placemarkChildNode - > nodeName ( ) eq 'kml:name' ) {
my $ stname = $ placemarkChildNode - > textContent ( ) ;
if ( $ stname eq $ station ) {
$ pos = $ n ;
2024-02-25 21:58:44 +00:00
last LOOP ;
2024-02-25 21:05:29 +00:00
}
}
}
}
2024-02-28 22:44:32 +00:00
2024-02-25 21:05:29 +00:00
return $ pos ;
}
= head2 ProcessForecast ( $$ $ )
= over
= 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 <undef> on success or error message
= 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 ProcessForecast {
2024-03-03 17:20:14 +00:00
my ( $ param , $ xmlStrings ) = @ _ ;
2024-02-25 21:05:29 +00:00
my $ hash = $ param - > { hash } ;
my $ name = $ hash - > { NAME } ;
my $ url = $ param - > { url } ;
my $ station = $ param - > { station } ;
2024-03-01 23:25:12 +00:00
my $ mosmixType = $ param - > { mosmixType } ;
2024-02-28 22:44:32 +00:00
my $ dwdDocSize = $ param - > { dwdDocSize } ;
my $ dwdDocTime = $ param - > { dwdDocTime } ;
2024-02-25 21:05:29 +00:00
:: Log3 $ name , 5 , "$name: ProcessForecast START" ;
my % forecast ;
my $ relativeDay = 0 ;
my @ coordinates ;
2024-03-03 17:20:14 +00:00
{
:: Log3 $ name , 5 , "$name: ProcessForecast: data unpacked, decoding ..." ;
2024-02-25 21:05:29 +00:00
# prepare processing
my $ forecastProperties = :: AttrVal ( $ name , 'forecastProperties' , undef ) ;
my @ properties = split ( ',' , $ forecastProperties ) if ( defined ( $ forecastProperties ) ) ;
my % selectedProperties ;
if ( ! @ properties ) {
# no selection: use defaults
2024-03-01 23:25:12 +00:00
if ( $ mosmixType eq 'S' ) {
2024-02-25 21:05:29 +00:00
% selectedProperties = % forecastDefaultPropertiesS ;
} else {
% selectedProperties = % forecastDefaultPropertiesL ;
2024-02-28 22:44:32 +00:00
}
2024-02-25 21:05:29 +00:00
} else {
# use selected properties
for my $ property ( @ properties ) { # use selected properties
$ property =~ s/^\s+|\s+$//g ; # trim
$ selectedProperties { $ property } = 1 ;
}
}
2024-03-03 17:20:14 +00:00
# collect forecast header data
2024-02-25 21:05:29 +00:00
my % header ;
$ header { station } = $ station ;
2024-02-28 22:44:32 +00:00
$ header { url } = $ url ;
$ header { dwdDocSize } = $ dwdDocSize ;
$ header { dwdDocTime } = $ dwdDocTime ;
2024-02-25 21:05:29 +00:00
# parse XML strings (files from zip)
2024-03-03 17:20:14 +00:00
for my $ xmlString ( @$ xmlStrings ) {
2024-02-25 21:05:29 +00:00
if ( substr ( $ { $ xmlString } , 0 , 2 ) eq 'PK' ) { # empty string, skip
# empty string, skip
next ;
}
# 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 data
my @ timestamps ;
my $ issuer = undef ;
my $ defaultUndefSign = '-' ;
my $ productDefinitionNodeList = $ dom - > getElementsByLocalName ( 'ProductDefinition' ) ;
2024-02-28 22:44:32 +00:00
if ( $ productDefinitionNodeList - > size ( ) ) {
2024-02-25 21:05:29 +00:00
my $ productDefinitionNode = $ productDefinitionNodeList - > get_node ( 1 ) ;
for my $ productDefinitionChildNode ( $ productDefinitionNode - > nonBlankChildNodes ( ) ) {
if ( $ productDefinitionChildNode - > nodeName ( ) eq 'dwd:Issuer' ) {
$ issuer = $ productDefinitionChildNode - > textContent ( ) ;
$ header { copyright } = "Datenbasis: $issuer" ;
} elsif ( $ productDefinitionChildNode - > nodeName ( ) eq 'dwd:IssueTime' ) {
my $ issueTime = $ productDefinitionChildNode - > textContent ( ) ;
$ header { time } = FormatDateTimeLocal ( $ hash , ParseKMLTime ( $ issueTime ) ) ;
} elsif ( $ productDefinitionChildNode - > nodeName ( ) eq 'dwd:ForecastTimeSteps' ) {
for my $ forecastTimeStepsChildNode ( $ productDefinitionChildNode - > nonBlankChildNodes ( ) ) {
if ( $ forecastTimeStepsChildNode - > nodeName ( ) eq 'dwd:TimeStep' ) {
my $ forecastTimeSteps = $ forecastTimeStepsChildNode - > textContent ( ) ;
push ( @ timestamps , ParseKMLTime ( $ forecastTimeSteps ) ) ;
}
}
} elsif ( $ productDefinitionChildNode - > nodeName ( ) eq 'dwd:FormatCfg' ) {
for my $ formatCfgChildNode ( $ productDefinitionChildNode - > nonBlankChildNodes ( ) ) {
if ( $ formatCfgChildNode - > nodeName ( ) eq 'dwd:DefaultUndefSign' ) {
$ defaultUndefSign = $ formatCfgChildNode - > textContent ( ) ;
}
}
}
}
}
$ forecast { timestamps } = \ @ timestamps ;
$ header { defaultUndefSign } = $ defaultUndefSign ;
if ( ! defined ( $ issuer ) ) {
die "error in XML data, forecast issuer not found" ;
}
# extract time data
my % timeProperties ;
my ( $ longitude , $ latitude , $ altitude ) ;
my $ placemarkNodeList = $ dom - > getElementsByLocalName ( 'Placemark' ) ;
2024-02-28 22:44:32 +00:00
if ( $ placemarkNodeList - > size ( ) ) {
2024-02-25 21:05:29 +00:00
my $ placemarkNodePos ;
2024-03-01 23:25:12 +00:00
if ( $ mosmixType eq 'S' ) {
2024-02-25 21:05:29 +00:00
$ placemarkNodePos = getStationPos ( $ name , $ station , $ placemarkNodeList ) ;
if ( $ placemarkNodePos < 1 ) {
die "station '" . $ station . "' not found in XML data" ;
}
} else {
$ placemarkNodePos = 1 ;
}
my $ placemarkNode = $ placemarkNodeList - > get_node ( $ placemarkNodePos ) ;
for my $ placemarkChildNode ( $ placemarkNode - > nonBlankChildNodes ( ) ) {
if ( $ placemarkChildNode - > nodeName ( ) eq 'kml:description' ) {
my $ description = $ placemarkChildNode - > textContent ( ) ;
$ header { description } = encode ( 'UTF-8' , $ description ) ;
} elsif ( $ placemarkChildNode - > nodeName ( ) eq 'kml:ExtendedData' ) {
for 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 } ;
2024-02-28 22:44:32 +00:00
if ( defined ( $ alias ) ) { $ elementName = $ alias } ;
2024-02-25 21:05:29 +00:00
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 ) ;
$ timeProperties { $ elementName } = \ @ values ;
}
}
}
} elsif ( $ placemarkChildNode - > nodeName ( ) eq 'kml:Point' ) {
my $ coordinates = $ placemarkChildNode - > nonBlankChildNodes ( ) - > get_node ( 1 ) - > textContent ( ) ;
$ header { coordinates } = $ coordinates ;
( $ longitude , $ latitude , $ altitude ) = split ( ',' , $ coordinates ) ;
}
}
}
# calculate sun position dependent properties for each timestamp
if ( defined ( $ longitude ) && defined ( $ latitude ) && defined ( $ altitude ) ) {
my @ azimuths ;
my @ elevations ;
my @ sunups ;
my @ sunrises ;
my @ sunsets ;
my $ lastDate = '' ;
my $ sunElevationCorrection = AstroSun:: ElevationCorrection ( $ altitude ) ;
for my $ timestamp ( @ timestamps ) {
my ( $ azimuth , $ elevation ) = AstroSun:: AzimuthElevation ( $ timestamp , $ longitude , $ latitude ) ;
push ( @ azimuths , $ azimuth ) ; # [deg]
push ( @ elevations , $ elevation ) ; # [deg]
push ( @ sunups , $ elevation >= $ sunElevationCorrection ? 1 : 0 ) ;
my $ date = FormatDateLocal ( $ hash , $ timestamp ) ;
if ( $ date ne $ lastDate ) {
# one calculation per day
my ( $ rise , $ transit , $ set ) = AstroSun:: RiseSet ( $ timestamp + LocaltimeOffset ( $ hash , $ timestamp ) , $ longitude , $ latitude , $ altitude ) ;
push ( @ sunrises , FormatTimeLocal ( $ hash , $ rise ) ) ; # round down to current minute
push ( @ sunsets , FormatTimeLocal ( $ hash , $ set + 30 ) ) ; # round up to next minute
$ lastDate = $ date ;
#::Log3 $name, 3, "$name: ProcessForecast " . FormatDateTimeLocal($hash, $timestamp) . " $rise " . FormatDateTimeLocal($hash, $rise) . " $transit " . FormatDateTimeLocal($hash, $transit). " $set " . FormatDateTimeLocal($hash, $set + 30);
} else {
push ( @ sunrises , $ defaultUndefSign ) ; # round down to current minute
push ( @ sunsets , $ defaultUndefSign ) ; # round up to next minute
}
}
if ( defined ( $ selectedProperties { SunAz } ) ) {
$ timeProperties { SunAz } = \ @ azimuths ;
}
if ( defined ( $ selectedProperties { SunEl } ) ) {
$ timeProperties { SunEl } = \ @ elevations ;
}
if ( defined ( $ selectedProperties { SunUp } ) ) {
$ timeProperties { SunUp } = \ @ sunups ;
}
if ( defined ( $ selectedProperties { SunRise } ) ) {
$ timeProperties { SunRise } = \ @ sunrises ;
}
if ( defined ( $ selectedProperties { SunSet } ) ) {
$ timeProperties { SunSet } = \ @ sunsets ;
}
}
$ forecast { timeProperties } = \ % timeProperties ;
}
$ forecast { header } = \ % header ;
} ;
my $ errorMessage = '' ;
if ( $@ ) {
# exception
my @ parts = split ( / at |\n/ , $@ ) ; # discard anything after " at " or newline
if ( @ parts ) {
$ errorMessage = $ parts [ 0 ] ;
:: Log3 $ name , 4 , "$name: ProcessForecast ERROR: $parts[0]" ;
} else {
$ errorMessage = $@ ;
:: Log3 $ name , 4 , "$name: ProcessForecast ERROR: $@" ;
}
} else {
# forecast parsed successfully
if ( defined ( $ hash - > { ".forecastFile" } ) ) {
if ( open ( my $ file , ">" , $ hash - > { ".forecastFile" } ) ) {
# write forecast to temp file
binmode ( $ file ) ;
my $ frozenForecast = freeze ( \ % forecast ) ;
:: Log3 $ name , 5 , "$name: ProcessForecast temp file " . $ hash - > { ".forecastFile" } . " forecast " . keys ( % forecast ) . " size " . length ( $ frozenForecast ) ;
print ( $ file $ frozenForecast ) ;
close ( $ file ) ;
} else {
$ errorMessage = $! ;
:: Log3 $ name , 3 , "$name: ProcessForecast error opening temp file: $errorMessage" ;
}
} else {
$ errorMessage = 'result file name not defined' ;
:: Log3 $ name , 3 , "$name: ProcessForecast ERROR: temp file name not defined" ;
}
}
# get rid of newlines and commas because of Blocking InformFn parameter restrictions
$ errorMessage =~ s/\n/; /g ;
$ errorMessage =~ s/,/;/g ;
:: Log3 $ name , 5 , "$name: ProcessForecast END" ;
return [ $ name , $ errorMessage ] ;
}
= head2 GetForecastFinish ( @ )
BlockingCall I <FinishFn> callback , expects array returned by function L </GetForecastStart($)> as single parameter
= over
= item * param name: name of DWD_OpenData device
= item * param errorMessage: empty string or processing error message
= back
= cut
sub GetForecastFinish {
my ( $ name , $ errorMessage ) = @ _ ;
if ( defined ( $ name ) ) {
:: Log3 $ name , 5 , "$name: GetForecastFinish START (PID $$)" ;
my $ hash = $ ::defs { $ name } ;
delete $ hash - > { ".forecastBlockingCall" } ;
delete $ hash - > { forecastUpdating } ;
if ( defined ( $ errorMessage ) && length ( $ errorMessage ) > 0 ) {
# error, skip further processing
} elsif ( ! defined ( $ hash - > { ".forecastFile" } ) ) {
$ errorMessage = "internal temp file name missing" ;
:: Log3 $ name , 3 , "$name: GetForecastFinish ERROR: $errorMessage" ;
} else {
# deserialize forecast
my $ fh = $ hash - > { ".forecastFileHandle" } ;
my $ terminator = $/ ;
$/ = undef ; # enable slurp file read mode
my $ frozenForecast = <$fh> ;
$/ = $ terminator ; # restore default file read mode
close ( $ hash - > { ".forecastFileHandle" } ) ;
unlink ( $ hash - > { ".forecastFile" } ) ;
my % newForecast = % { thaw ( $ frozenForecast ) } ;
:: Log3 $ name , 5 , "$name: GetForecastFinish temp file " . $ hash - > { ".forecastFile" } . " forecast " . keys ( % newForecast ) . " size " . length ( $ frozenForecast ) ;
delete ( $ hash - > { ".forecastFile" } ) ;
UpdateForecast ( $ hash , \ % newForecast ) ;
}
if ( defined ( $ errorMessage ) && length ( $ errorMessage ) > 0 ) {
:: readingsBeginUpdate ( $ hash ) ;
2024-02-28 22:44:32 +00:00
if ( $ errorMessage eq 'up-to-date' ) {
:: readingsBulkUpdate ( $ hash , 'state' , "forecast unchanged" ) ;
:: readingsBulkUpdate ( $ hash , 'fc_state' , 'updated' ) ;
} else {
:: readingsBulkUpdate ( $ hash , 'state' , "forecast error: $errorMessage" ) ;
:: readingsBulkUpdate ( $ hash , 'fc_state' , "error: $errorMessage" ) ;
}
2024-02-25 21:05:29 +00:00
# rotate forecast anyway
my $ station = $ hash - > { ".station" } ;
RotateForecast ( $ hash , $ station ) ;
:: readingsEndUpdate ( $ hash , 1 ) ;
} else {
:: readingsSingleUpdate ( $ hash , 'fc_state' , 'updated' , 1 ) ;
}
if ( defined ( $ hash - > { ".fetchAlerts" } ) && ! $ hash - > { ".fetchAlerts" } ) {
# get forecast was initiated by timer, reschedule to fetch alerts
$ hash - > { ".fetchAlerts" } = 1 ;
2024-02-28 22:44:32 +00:00
:: InternalTimer ( gettimeofday ( ) + 1 , 'DWD_OpenData::Timer' , $ hash ) ;
2024-02-25 21:05:29 +00:00
}
:: Log3 $ name , 5 , "$name: GetForecastFinish END" ;
} else {
:: Log 3 , "GetForecastFinish ERROR: device name missing" ;
}
}
= head2 GetForecastAbort ( $ )
BlockingCall I <AbortFn> callback
= over
= item * param hash: hash of DWD_OpenData device
= back
= cut
sub GetForecastAbort {
my ( $ hash , $ errorMessage ) = @ _ ;
my $ name = $ hash - > { NAME } ;
delete $ hash - > { ".forecastBlockingCall" } ;
delete $ hash - > { forecastUpdating } ;
$ errorMessage = "downloading and processing weather forecast data failed ($errorMessage)" ;
:: Log3 $ name , 3 , "$name: GetForecastAbort ERROR: $errorMessage" ;
:: readingsBeginUpdate ( $ hash ) ;
:: readingsBulkUpdate ( $ hash , 'state' , "forecast error: $errorMessage" ) ;
:: readingsBulkUpdate ( $ hash , 'fc_state' , "error: $errorMessage" ) ;
# rotate forecast anyway
my $ station = $ hash - > { ".station" } ;
RotateForecast ( $ hash , $ station ) ;
:: readingsEndUpdate ( $ hash , 1 ) ;
if ( defined ( $ hash - > { ".fetchAlerts" } ) && ! $ hash - > { ".fetchAlerts" } ) {
# get forecast was initiated by timer, reschedule to fetch alerts
$ hash - > { ".fetchAlerts" } = 1 ;
2024-02-28 22:44:32 +00:00
:: InternalTimer ( gettimeofday ( ) + 1 , 'DWD_OpenData::Timer' , $ hash ) ;
2024-02-25 21:05:29 +00:00
}
}
= head2 UpdateForecast ( $$ )
update forecast readings
= over
= item * param hash: hash of DWD_OpenData device
= item * param forecast: hash ref to forecast data
= item * return C <undef> or error message
= back
= cut
sub UpdateForecast {
my ( $ hash , $ forecast ) = @ _ ;
my $ name = $ hash - > { NAME } ;
:: Log3 $ name , 5 , "$name: UpdateForecast: START" ;
:: readingsBeginUpdate ( $ hash ) ;
# preprocess existing readings
my $ time = time ( ) ;
my ( $ tSec , $ tMin , $ tHour , $ tMday , $ tMon , $ tYear , $ tWday , $ tYday , $ tIsdst ) = Localtime ( $ hash , $ time ) ;
my $ today = Timelocal ( $ hash , 0 , 0 , 0 , $ tMday , $ tMon , $ tYear ) ;
my $ station = $ forecast - > { header } { station } ;
my $ daysAvailable = RotateForecast ( $ hash , $ station , $ today ) ;
# create header readings
my $ defaultUndefSign = $ forecast - > { header } { defaultUndefSign } ;
delete $ forecast - > { header } { defaultUndefSign } ;
while ( my ( $ property , $ value ) = each % { $ forecast - > { header } } )
{
:: readingsBulkUpdate ( $ hash , 'fc_' . $ property , $ value ) ;
}
# prepare time processing
my $ forecastWW2Text = :: AttrVal ( $ name , 'forecastWW2Text' , 0 ) ;
my $ forecastDays = :: AttrVal ( $ name , 'forecastDays' , 6 ) ;
my $ forecastResolution = :: AttrVal ( $ name , 'forecastResolution' , 6 ) ;
# create time readings
my $ lastDayPrefix = '' ;
my $ relativeDay = - 1 ;
my $ timestamps = $ forecast - > { timestamps } ;
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 ) ) ; # 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 % { $ forecast - > { timeProperties } } ) {
#::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(2) 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 ) ;
}
}
}
}
}
# delete readings of all days that have not been updated
if ( $ relativeDay >= 0 && $ 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 . "_.*" ) ;
}
}
# delete readings with inconsistent timestamps
PruneForecast ( $ hash ) ;
:: readingsBulkUpdate ( $ hash , 'state' , 'forecast updated' ) ;
:: readingsEndUpdate ( $ hash , 1 ) ;
:: Log3 $ name , 5 , "$name: UpdateForecast: END" ;
return undef ;
}
= head2 GetAlerts ( $$ )
= over
= item * param hash: hash of DWD_OpenData device
= item * param warncellId: numeric id of warncell , may also be C <UPDATE_DISTRICTS> , C <UPDATE_COMMUNEUNIONS> or C <UPDATE_ALL>
= back
= cut
sub GetAlerts {
my ( $ hash , $ warncellId ) = @ _ ;
my $ name = $ hash - > { NAME } ;
if ( ! :: IsDisabled ( $ name ) ) {
:: Log3 $ name , 5 , "$name: GetAlerts START (PID $$)" ;
# test if XML module is available
eval {
require XML::LibXML ;
} ;
if ( $@ ) {
return "$name: Perl module XML::LibXML not found, see commandref for details how to fix" ;
}
# @todo delete expired alerts?
# download, unzip and parse using BlockingCall
my $ communeUnion = IsCommuneUnionWarncellId ( $ warncellId ) ;
if ( defined ( $ hash - > { ".alertsFile" . $ communeUnion } ) ) {
# delete old temp file
close ( $ hash - > { ".alertsFileHandle" . $ communeUnion } ) ;
unlink ( $ hash - > { ".alertsFile" . $ communeUnion } ) ;
}
( $ hash - > { ".alertsFileHandle" . $ communeUnion } , $ hash - > { ".alertsFile" . $ communeUnion } ) = tempfile ( UNLINK = > 1 ) ;
$ hash - > { ".warncellId" } = $ warncellId ;
if ( defined ( $ hash - > { ".alertsBlockingCall" . $ communeUnion } ) ) {
# kill old blocking call
:: BlockingKill ( $ hash - > { ".alertsBlockingCall" . $ communeUnion } ) ;
}
2024-02-28 22:44:32 +00:00
my $ timeout = :: AttrVal ( $ name , 'downloadTimeout' , DOWNLOAD_TIMEOUT_DEFAULT ) ;
2024-02-25 21:05:29 +00:00
$ hash - > { ".alertsBlockingCall" . $ communeUnion } = :: BlockingCall ( "DWD_OpenData::GetAlertsStart" , $ hash , "DWD_OpenData::GetAlertsFinish" , $ timeout , "DWD_OpenData::GetAlertsAbort" , $ hash ) ;
$ alertsUpdating [ $ communeUnion ] = time ( ) ;
:: readingsSingleUpdate ( $ hash , 'state' , 'updating alerts cache' , 1 ) ;
:: Log3 $ name , 5 , "$name: GetAlerts END" ;
return undef ;
} else {
return "disabled" ;
}
}
sub ProcessAlerts ;
= head2 GetAlertsStart ( $ )
BlockingCall I <BlockingFn> callback
= over
= item * param hash: hash of DWD_OpenData device
= item * return result required by function L </GetAlertsFinish(@)>
= 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 GetAlertsStart {
my ( $ hash ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my $ warncellId = $ hash - > { ".warncellId" } ;
# give main process time to execute
usleep ( 100 ) ;
# get communion (5, 7, 8) or district (1, 9) alerts for Germany from DWD server
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' ;
2024-03-01 23:25:12 +00:00
my ( $ dwdDocSize , $ dwdDocTime ) ;
my $ update = IsDocumentUpdated ( $ hash , $ url , 'a' , $ dwdDocSize , $ dwdDocTime ) ;
my $ result ;
if ( $ update ) {
my $ param = {
url = > $ url ,
method = > "GET" ,
timeout = > :: AttrVal ( $ name , 'downloadTimeout' , DOWNLOAD_TIMEOUT_DEFAULT ) ,
hash = > $ hash ,
warncellId = > $ warncellId ,
dwdDocSize = > $ dwdDocSize ,
dwdDocTime = > $ dwdDocTime ,
} ;
:: Log3 $ name , 5 , "$name: GetAlertsStart START (PID $$): $url $dwdDocSize $dwdDocTime" ;
my ( $ httpError , $ fileContent ) = :: HttpUtils_BlockingGet ( $ param ) ;
2024-02-25 21:05:29 +00:00
2024-03-01 23:25:12 +00:00
# process retrieved data
$ result = ProcessAlerts ( $ param , $ httpError , $ fileContent ) ;
:: Log3 $ name , 5 , "$name: GetAlertsStart END" ;
} else {
# already up to date
2024-03-03 17:20:14 +00:00
$ result = [ $ name , 'up-to-date' , $ warncellId , time ( ) ] ;
2024-03-01 23:25:12 +00:00
:: Log3 $ name , 5 , "$name: GetAlertsStart UP-TO-DATE" ;
}
2024-02-25 21:05:29 +00:00
return $ result ;
}
= head2 ProcessAlerts ( $$ $ )
= over
2024-03-01 23:25:12 +00:00
= 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
2024-02-25 21:05:29 +00:00
= item * return result required by function L </GetAlertsFinish(@)>
= 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 ProcessAlerts {
my ( $ param , $ httpError , $ fileContent ) = @ _ ;
my $ hash = $ param - > { hash } ;
my $ name = $ hash - > { NAME } ;
my $ url = $ param - > { url } ;
my $ code = $ param - > { code } ;
my $ warncellId = $ param - > { warncellId } ;
2024-03-01 23:25:12 +00:00
$ param - > { receivedTime } = time ( ) ;
:: Log3 $ name , 5 , "$name: ProcessAlerts START (PID $$) $warncellId" ;
2024-02-25 21:05:29 +00:00
my % alerts ;
eval {
if ( defined ( $ httpError ) && length ( $ httpError ) > 0 ) {
die "error retrieving URL '$url': $httpError" ;
}
if ( defined ( $ code ) && $ code != 200 ) {
die "HTTP error $code retrieving URL '$url'" ;
}
if ( ! defined ( $ fileContent ) || length ( $ fileContent ) == 0 ) {
die "no data retrieved from URL '$url'" ;
}
:: Log3 $ name , 5 , "$name: ProcessAlerts: data received" ;
# create memory mapped file from received data and unzip
open my $ zipFileHandle , '<' , \ $ fileContent ;
my @ xmlStrings ;
unzip ( $ zipFileHandle = > \ @ xmlStrings , MultiStream = > 1 ) or die "error unzipping data: $UnzipError\n" ;
# parse XML strings
foreach my $ xmlString ( @ xmlStrings ) {
if ( substr ( $ { $ xmlString } , 0 , 2 ) eq 'PK' ) {
# empty string, skip
next ;
}
# parse XML string
:: Log3 $ name , 5 , "$name: ProcessAlerts: parsing XML document" ;
my $ dom = XML::LibXML - > load_xml ( string = > $ xmlString ) ;
if ( ! $ dom ) {
die "error parsing XML data" ;
}
my $ xpc = XML::LibXML::XPathContext - > new ( $ dom ) ;
$ xpc - > registerNs ( 'cap' , 'urn:oasis:names:tc:emergency:cap:1.2' ) ;
my $ alert = { } ;
my $ alertNode = $ dom - > documentElement ( ) ;
foreach my $ alertChildNode ( $ alertNode - > nonBlankChildNodes ( ) ) {
#::Log3 $name, 5, "$name: ProcessAlerts child node: " . $alertChildNode->nodeName();
if ( $ alertChildNode - > nodeName ( ) eq 'identifier' ) {
$ alert - > { identifier } = $ alertChildNode - > textContent ( ) ;
#::Log3 $name, 5, "$name: ProcessAlerts identifier: " . $alert->{identifier};
} elsif ( $ alertChildNode - > nodeName ( ) eq 'status' ) {
$ alert - > { status } = $ alertChildNode - > textContent ( ) ;
} elsif ( $ alertChildNode - > nodeName ( ) eq 'msgType' ) {
$ alert - > { msgType } = $ alertChildNode - > textContent ( ) ;
} elsif ( $ alertChildNode - > nodeName ( ) eq 'references' ) {
# get list of references, separated by whitespace, each reference consisting of 3 parts: sender, identifier, sent
$ alert - > { references } = [] ;
my @ references = split ( ' ' , $ alertChildNode - > textContent ( ) ) ;
foreach my $ reference ( @ references ) {
my @ parts = split ( ',' , $ reference ) ;
if ( scalar ( @ parts ) == 3 ) {
push ( @ { $ alert - > { references } } , $ parts [ 2 ] ) ;
}
}
} elsif ( $ alertChildNode - > nodeName ( ) eq 'info' ) {
foreach my $ infoChildNode ( $ alertChildNode - > nonBlankChildNodes ( ) ) {
#::Log3 $name, 5, "$name: ProcessAlerts child node: '" . $infoChildNode->nodeName() . "'";
if ( $ infoChildNode - > nodeName ( ) eq 'category' ) {
$ alert - > { category } = $ infoChildNode - > textContent ( ) ;
} elsif ( $ infoChildNode - > nodeName ( ) eq 'event' ) {
$ alert - > { event } = $ infoChildNode - > textContent ( ) ;
} elsif ( $ infoChildNode - > nodeName ( ) eq 'responseType' ) {
$ alert - > { responseType } = $ infoChildNode - > textContent ( ) ;
} elsif ( $ infoChildNode - > nodeName ( ) eq 'urgency' ) {
$ alert - > { urgency } = $ infoChildNode - > textContent ( ) ;
} elsif ( $ infoChildNode - > nodeName ( ) eq 'severity' ) {
$ alert - > { severity } = $ infoChildNode - > textContent ( ) ;
} elsif ( $ infoChildNode - > nodeName ( ) eq 'eventCode' ) {
$ xpc - > setContextNode ( $ infoChildNode ) ;
my $ valueName = $ xpc - > findvalue ( "./cap:valueName" ) ;
if ( $ valueName eq 'LICENSE' ) {
$ alert - > { license } = $ xpc - > findvalue ( "./cap:value" ) ;
} elsif ( $ valueName eq 'II' ) {
$ alert - > { eventCode } = $ xpc - > findvalue ( "./cap:value" ) ;
} elsif ( $ valueName eq 'GROUP' ) {
$ alert - > { eventGroup } = $ xpc - > findvalue ( "./cap:value" ) ;
} elsif ( $ valueName eq 'AREA_COLOR' ) {
$ alert - > { areaColor } = $ xpc - > findvalue ( "./cap:value" ) ;
$ alert - > { areaColor } =~ s/ /, /g ;
}
} elsif ( $ infoChildNode - > nodeName ( ) eq 'onset' ) {
$ alert - > { onset } = ParseCAPTime ( $ infoChildNode - > textContent ( ) ) ;
} elsif ( $ infoChildNode - > nodeName ( ) eq 'expires' ) {
$ alert - > { expires } = ParseCAPTime ( $ infoChildNode - > textContent ( ) ) ;
} elsif ( $ infoChildNode - > nodeName ( ) eq 'headline' ) {
$ alert - > { headline } = $ infoChildNode - > textContent ( ) ;
} elsif ( $ infoChildNode - > nodeName ( ) eq 'description' ) {
$ alert - > { description } = $ infoChildNode - > textContent ( ) ;
} elsif ( $ infoChildNode - > nodeName ( ) eq 'instruction' ) {
$ alert - > { instruction } = $ infoChildNode - > textContent ( ) ;
} elsif ( $ infoChildNode - > nodeName ( ) eq 'area' ) {
$ xpc - > setContextNode ( $ infoChildNode ) ;
my $ valueName = $ xpc - > findvalue ( "./cap:geocode/cap:valueName" ) ;
if ( $ valueName eq 'WARNCELLID' ) {
if ( ! defined ( $ alert - > { warncellid } ) ) {
$ alert - > { warncellid } = [] ;
$ alert - > { areaDesc } = [] ;
$ alert - > { altitude } = [] ;
$ alert - > { ceiling } = [] ;
}
#::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" ) ) ;
push ( @ { $ alert - > { ceiling } } , $ xpc - > findvalue ( "./cap:ceiling" ) ) ;
}
}
}
}
}
#::Log3 $name, 5, "$name: ProcessAlerts header: $alert->{identifier}, $alert->{status}, $alert->{msgType}: $alert->{headline}, $alert->{warncellids}[0]";
if ( ! defined ( $ alert - > { identifier } ) ) {
die "error in XML data, no alert identifier found" ;
}
if ( $ alert - > { status } ne 'Test' && $ alert - > { responseType } ne 'Monitor' ) {
$ alerts { $ alert - > { identifier } } = $ alert ;
}
}
} ;
my $ errorMessage = '' ;
if ( $@ ) {
# exception
my @ parts = split ( / at |\n/ , $@ ) ; # discard anything after " at " or newline
if ( @ parts ) {
$ errorMessage = $ parts [ 0 ] ;
:: Log3 $ name , 4 , "$name: ProcessAlerts ERROR: $parts[0]" ;
} else {
$ errorMessage = $@ ;
:: Log3 $ name , 4 , "$name: ProcessAlerts ERROR: $@" ;
}
} else {
# alerts parsed successfully
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: ProcessAlerts temp file " . $ hash - > { ".alertsFile" . $ communeUnion } . " alerts " . keys ( % alerts ) . " size " . length ( $ frozenAlerts ) ;
print ( $ file $ frozenAlerts ) ;
close ( $ file ) ;
} else {
$ errorMessage = $! ;
:: Log3 $ name , 3 , "$name: ProcessAlerts error opening temp file: $errorMessage" ;
}
} else {
$ errorMessage = 'result file name not defined' ;
:: Log3 $ name , 3 , "$name: ProcessAlerts ERROR: temp file name not defined" ;
}
}
# get rid of newlines and commas because of Blocking InformFn parameter restrictions
$ errorMessage =~ s/\n/; /g ;
$ errorMessage =~ s/,/;/g ;
:: Log3 $ name , 5 , "$name: ProcessAlerts END" ;
2024-03-01 23:25:12 +00:00
return [ $ name , $ errorMessage , $ param - > { warncellId } , $ param - > { receivedTime } , $ param - > { url } , $ param - > { dwdDocSize } , $ param - > { dwdDocTime } ] ;
2024-02-25 21:05:29 +00:00
}
= head2 GetAlertsFinish ( @ )
BlockingCall I <FinishFn> callback , expects array returned by function L </GetAlertsStart($)> as single parameter
= 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 <UPDATE_DISTRICTS> , C <UPDATE_COMMUNEUNIONS> or C <UPDATE_ALL>
= item * param time : epoch time when alerts where received
= back
= cut
sub GetAlertsFinish {
2024-03-01 23:25:12 +00:00
my ( $ name , $ errorMessage , $ warncellId , $ receivedTime , $ url , $ dwdDocSize , $ dwdDocTime ) = @ _ ;
my $ paramCount = @ _ ;
my % docHeader ;
if ( $ paramCount > 3 ) {
$ docHeader { warncellId } = $ warncellId ;
$ docHeader { receivedTime } = $ receivedTime ;
$ docHeader { url } = $ url ;
$ docHeader { dwdDocSize } = $ dwdDocSize ;
$ docHeader { dwdDocTime } = $ dwdDocTime ;
}
2024-02-25 21:05:29 +00:00
if ( defined ( $ name ) ) {
2024-03-01 23:25:12 +00:00
:: Log3 $ name , 5 , "$name: GetAlertsFinish START (PID $$) $warncellId" ;
2024-02-25 21:05:29 +00:00
my $ hash = $ ::defs { $ name } ;
my $ communeUnion = IsCommuneUnionWarncellId ( $ warncellId ) ;
delete $ hash - > { ".alertsBlockingCall" . $ communeUnion } ;
if ( defined ( $ errorMessage ) && length ( $ errorMessage ) > 0 ) {
# error, skip further processing
} elsif ( ! defined ( $ hash - > { ".alertsFile" . $ communeUnion } ) ) {
$ errorMessage = "internal temp file name missing" ;
:: Log3 $ name , 3 , "$name: GetAlertsFinish ERROR: $errorMessage" ;
} else {
# deserialize alerts
my $ fh = $ hash - > { ".alertsFileHandle" . $ communeUnion } ;
my $ terminator = $/ ;
$/ = undef ; # enable slurp file read mode
my $ frozenAlerts = <$fh> ;
$/ = $ terminator ; # restore default file read mode
close ( $ hash - > { ".alertsFileHandle" . $ communeUnion } ) ;
unlink ( $ hash - > { ".alertsFile" . $ communeUnion } ) ;
my % newAlerts = % { thaw ( $ 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?
my $ alerts = { } ;
# update global alert list
foreach my $ alert ( values ( % newAlerts ) ) {
my $ indentifierExists = defined ( $ alerts - > { $ alert - > { identifier } } ) ;
if ( $ indentifierExists ) {
:: 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 ;
} elsif ( $ alert - > { msgType } eq 'Update' ) {
# delete old alerts
foreach my $ reference ( @ { $ alert - > { references } } ) {
delete $ alerts - > { $ reference } ;
}
# add new alert
$ alerts - > { $ alert - > { identifier } } = $ alert ;
} elsif ( $ alert - > { msgType } eq 'Cancel' ) {
# delete old alerts
foreach my $ reference ( @ { $ alert - > { references } } ) {
delete $ alerts - > { $ reference } ;
}
}
}
$ alertsData [ $ communeUnion ] = $ alerts ;
if ( $ warncellId == UPDATE_ALL ) {
2024-02-28 22:44:32 +00:00
if ( ! defined ( $ alertsUpdating [ 0 ] ) || ( time ( ) - $ alertsUpdating [ 0 ] >= PROCESSING_TIMEOUT ) ) {
2024-02-25 21:05:29 +00:00
# communeunions cache updated, start district cache update;
GetAlerts ( $ hash , UPDATE_DISTRICTS ) ;
}
} elsif ( $ warncellId < 0 ) {
:: readingsSingleUpdate ( $ hash , 'state' , 'alerts cache updated' , 1 ) ;
}
}
2024-03-01 23:25:12 +00:00
$ alertsReceived [ $ communeUnion ] = $ receivedTime ;
2024-02-25 21:05:29 +00:00
if ( defined ( $ errorMessage ) && length ( $ errorMessage ) > 0 ) {
2024-03-01 23:25:12 +00:00
if ( $ errorMessage eq 'up-to-date' ) {
:: readingsBeginUpdate ( $ hash ) ;
2024-03-03 17:20:14 +00:00
:: readingsBulkUpdate ( $ hash , 'a_time' , FormatDateTimeLocal ( $ hash , $ receivedTime ) ) ;
2024-03-01 23:25:12 +00:00
:: readingsBulkUpdate ( $ hash , 'a_state' , 'updated' ) ;
2024-03-03 17:20:14 +00:00
:: readingsBulkUpdate ( $ hash , 'state' , "alerts unchanged" ) ;
2024-03-01 23:25:12 +00:00
:: readingsEndUpdate ( $ hash , 1 ) ;
} else {
:: readingsSingleUpdate ( $ hash , 'state' , "alerts error: $errorMessage" , 1 ) ;
}
2024-02-25 21:05:29 +00:00
} else {
$ alertsErrorMessage [ $ communeUnion ] = undef ;
}
2024-03-01 23:25:12 +00:00
if ( $ paramCount > 3 && $ errorMessage ne 'up-to-date' ) {
2024-02-25 21:05:29 +00:00
# update alert readings for warncell id
2024-03-01 23:25:12 +00:00
UpdateAlerts ( $ hash , $ warncellId , \ % docHeader ) ;
2024-02-25 21:05:29 +00:00
}
$ alertsUpdating [ $ communeUnion ] = undef ;
$ hash - > { ALERTS_IN_CACHE } = ( ref ( $ alertsData [ 0 ] ) eq 'HASH' ? scalar ( keys ( % { $ alertsData [ 0 ] } ) ) : 0 ) + ( ref ( $ alertsData [ 1 ] ) eq 'HASH' ? scalar ( keys ( % { $ alertsData [ 1 ] } ) ) : 0 ) ;
:: Log3 $ name , 5 , "$name: GetAlertsFinish END" ;
} else {
:: Log 3 , "GetAlertsFinish ERROR: device name missing" ;
}
}
= head2 GetAlertsAbort ( $ )
BlockingCall I <AbortFn> callback
= over
= item * param hash: hash of DWD_OpenData device
= back
= cut
sub GetAlertsAbort {
my ( $ hash , $ errorMessage ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my $ warncellId = $ hash - > { ".warncellId" } ;
my $ communeUnion = IsCommuneUnionWarncellId ( $ warncellId ) ;
delete $ hash - > { ".alertsBlockingCall" . $ communeUnion } ;
$ alertsUpdating [ $ communeUnion ] = undef ;
$ errorMessage = "downloading and processing weather alerts data failed ($errorMessage)" ;
:: Log3 $ name , 3 , "$name: GetAlertsAbort ERROR: $errorMessage" ;
$ alertsErrorMessage [ $ communeUnion ] = $ errorMessage ;
if ( $ warncellId >= 0 ) {
# update alert readings for warncell id
UpdateAlerts ( $ hash , $ warncellId ) ;
} else {
:: readingsSingleUpdate ( $ hash , 'state' , "alerts error: $errorMessage" , 1 ) ;
}
}
= head2 UpdateAlerts ( $$ )
update alert readings for given warncell id from global alerts list
= over
= item * param hash: hash of DWD_OpenData device
= item * param warncellId: numeric warncell id greater zero
= item * return C <undef> or error message
= back
= cut
sub UpdateAlerts {
2024-03-01 23:25:12 +00:00
my ( $ hash , $ warncellId , $ docHeader ) = @ _ ;
2024-02-25 21:05:29 +00:00
my $ name = $ hash - > { NAME } ;
# delete existing alert readings
2024-03-01 23:25:12 +00:00
:: CommandDeleteReading ( undef , "$name ^(?!a_count|a_state|a_time|a_url|a_dwdDocSize|a_dwdDocTime)a_.*" ) ;
2024-02-25 21:05:29 +00:00
:: readingsBeginUpdate ( $ hash ) ;
# create alert for next 24 hours, if retrieval failed
my $ index = 0 ;
my $ communeUnion = IsCommuneUnionWarncellId ( $ warncellId ) ;
if ( defined ( $ alertsErrorMessage [ $ communeUnion ] ) && length ( $ alertsErrorMessage [ $ communeUnion ] ) > 0 ) {
my $ prefix = 'a_' . $ index . '_' ;
my $ time = time ( ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'category' , 'Met' ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'event' , 0 ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'eventDesc' , 'STÖRUNG' ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'eventGroup' , 'FHEM' ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'responseType' , 'Prepare' ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'urgency' , 'Immediate' ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'severity' , 'Severe' ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'areaColor' , '255, 0, 0' ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'onset' , FormatDateTimeLocal ( $ hash , $ time ) ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'expires' , FormatDateTimeLocal ( $ hash , $ time + 24 * 60 * 60 ) ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'headline' , 'FHEM: Aktualisierung der Wetterwarnungen fehlgeschlagen' ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'description' , "Fehler: $alertsErrorMessage[$communeUnion]" ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'instruction' , 'ACHTUNG! Aktuell stehen aufgrund einer Störung keine aktuellen Wetterwarnungen zur Verfügung.' ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'area' , 0 ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'areaDesc' , 'DWD Open Data Server' ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'altitude' , 0 ) ;
:: readingsBulkUpdate ( $ hash , $ prefix . 'ceiling' , 0 ) ;
$ index + + ;
:: readingsBulkUpdate ( $ hash , 'a_state' , "error: $alertsErrorMessage[$communeUnion]" ) ;
} else {
:: readingsBulkUpdate ( $ hash , 'a_state' , 'updated' ) ;
}
# prepare processing
my $ alertExcludeEvents = :: AttrVal ( $ name , 'alertExcludeEvents' , undef ) ;
my @ excludeEventsList = split ( ',' , $ alertExcludeEvents ) if ( defined ( $ alertExcludeEvents ) ) ;
foreach my $ excludeEvent ( @ excludeEventsList ) {
$ excludeEvent =~ s/^\s+|\s+$//g ; # trim
}
my % excludeEvents = map { $ _ = > 1 } @ excludeEventsList ;
# order alerts by onset
if ( ref ( $ alertsData [ $ communeUnion ] ) eq 'HASH' ) {
my $ alerts = $ alertsData [ $ communeUnion ] ;
my @ identifiers = sort { $ alerts - > { $ a } - > { onset } <=> $ alerts - > { $ b } - > { onset } } keys ( % { $ alerts } ) ;
foreach my $ identifier ( @ identifiers ) {
my $ alert = $ alerts - > { $ identifier } ;
# find alert for selected warncell
my $ areaIndex = 0 ;
foreach my $ wcId ( @ { $ alert - > { warncellid } } ) {
if ( $ wcId == $ warncellId && ! ( lc ( $ alert - > { severity } ) eq 'minor' && defined ( $ excludeEvents { $ alert - > { eventCode } } ) ) ) {
# alert found that is not on the exclude list, 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' , 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 ( ) ;
}
$ areaIndex + + ;
}
# license
if ( $ index == 1 && defined ( $ alert - > { license } ) ) {
:: readingsBulkUpdate ( $ hash , 'a_copyright' , encode ( 'UTF-8' , $ alert - > { license } ) ) ;
}
}
}
2024-03-01 23:25:12 +00:00
# alert count, receive time and DWD document properties
2024-02-25 21:05:29 +00:00
:: readingsBulkUpdate ( $ hash , 'a_count' , $ index ) ;
2024-03-01 23:25:12 +00:00
if ( defined ( $ docHeader ) ) {
:: readingsBulkUpdate ( $ hash , "a_time" , FormatDateTimeLocal ( $ hash , $ docHeader - > { receivedTime } ) ) ;
:: readingsBulkUpdate ( $ hash , "a_url" , $ docHeader - > { url } ) ;
:: readingsBulkUpdate ( $ hash , "a_dwdDocSize" , $ docHeader - > { dwdDocSize } ) ;
:: readingsBulkUpdate ( $ hash , "a_dwdDocTime" , $ docHeader - > { dwdDocTime } ) ;
}
2024-02-25 21:05:29 +00:00
:: readingsBulkUpdate ( $ hash , 'state' , 'alerts updated' ) ;
:: readingsEndUpdate ( $ hash , 1 ) ;
return undef ;
}
# -----------------------------------------------------------------------------
package main ;
= head1 FHEM INIT FUNCTION
= head2 DWD_OpenData_Initialize ( $ )
FHEM I <Initialize> function
= over
= item * param hash: hash of DWD_OpenData device
= 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 '
2024-03-03 17:20:14 +00:00
. 'forecastStation forecastDays forecastProperties forecastResolution:1,3,6 forecastWW2Text:0,1 forecastPruning:0,1 forecastRefresh:slider,6,-1,1 '
2024-02-25 21:05:29 +00:00
. 'alertArea alertLanguage:DE,EN alertExcludeEvents '
. 'timezone '
. 'downloadTimeout '
. $ readingFnAttributes ;
}
# -----------------------------------------------------------------------------
1 ;
# -----------------------------------------------------------------------------
#
# CHANGES
#
2024-03-03 17:20:14 +00:00
# 01.03.2024 (version 1.17.3) jensb + DS_Starter
# feature: unzip large forecast files to disk and filter out selected station before processing
# change: increased max value for attribute downloadTimeout to 120 s
# change: LWP request HEAD replaced with HttpUtils_BlockingGet
#
2024-03-01 23:25:12 +00:00
# 01.03.2024 (version 1.17.2) jensb
# feature: skip download of alert data if DWD document is unchanged
# change: attribute forecastDataPresision replaced with attribute forecastRefresh
#
2024-02-28 22:44:32 +00:00
# 28.02.2024 (version 1.17.1) jensb
# feature: skip download of forecast data if DWD document is unchanged
# feature: show context description for get commands and attributes in FHEMWEB
#
2024-02-25 21:05:29 +00:00
# 25.02.2024 (version 1.17.0) DS_Starter + jensb
# feature: support MOSMIX S
#
# 16.02.2021 (version 1.16.3) jensb
# bugfix: fix version for experimental::smartmatch
#
# 03.12.2020 (version 1.16.2) jensb
# change: increased log level in sub RotateForecast
#
# 03.12.2020 (version 1.16.1) jensb
# bugfix: delete destination reading if source reading is undefined when rotating forecast at daybreak
# feature: new attribute forecastPruning to delete forecast readings that are more than 1 day older than the other readings of the same day
#
# 22.11.2020 (version 1.15.0) jensb
# feature: keep reading timestamp when rotating forecast values at daybreak
#
# 17.06.2020 (version 1.14.6) jensb
# bugfix: $warncellId uninitialized when shutdown before first forecast fetch
# coding: prototypes removed
#
# 05.04.2020 (version 1.14.5) jensb
# bugfix: perform forecast rotation if download fails without timeout
# bugfix: skip alert update if initial download fails
#
# 17.04.2019 (version 1.14.4) jensb
# bugfix: fix reading SunUp (azimuth/elevation calculation)
#
# 17.03.2019 (version 1.14.3) jensb
# coding: moved sun related code into separate module AstroSun
#
# 11.03.2019 (version 1.14.1) jensb
# feature: support warncells that begin with 7
#
# 04.03.2019 (version 1.14.0) jensb
# coding: replaced Julian date calculation
# change: SunUp based on upper solar limb instead of nautical twilight
# feature: new daily sun position readings SunRise, SunSet
#
# 23.02.2019 (version 1.13.0) jensb
# feature: new hourly sun position readings SunAz, SunEl and SunUp
#
# 10.02.2019 (version 1.12.3) jensb
# feature: do not delete readings a_count, a_state, a_time when updating alerts
#
# 28.12.2018 (version 1.12.2) jensb
# bugfix: modified regexp to delete forecast readings on attribute change
#
# 23.12.2018 (version 1.12.1) jensb
# feature: new attribute alertExcludeEvents
# feature: delete forecast readings if attribute forecastResolution or forecastStation are changed
#
# 20.12.2018 (version 1.12.0) jensb
# feature: enable 1h forecast resolution
#
# 02.12.2018 (version 1.11.0) jensb
# feature: async processing of forecast enhanced (HttpUtils_NonblockingGet replaced by BlockingCall) to further unload FHEM process
# feature: staggered update of forecast and alert to spread load
# feature: improved cleanup of file descriptors on undef and shutdown
# feature: alerts and forecast retrieval error detection improved
# feature: new readings a_state and fc_state
# feature: create internal alert on retrieval error
# bugfix: forecast retrieval timeout handling
# bugfix: forecast rotation days calculation
# bugfix: update scheduling when summertime 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
#
2024-03-03 17:20:14 +00:00
# 18.02.2018 JoWiemann + jensb
# feature: LWP::Simple replaced by HttpUtils_NonblockingGet
2024-02-25 21:05:29 +00:00
#
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
#
# @todo forecast: wwd in English
# @todo forecast: if a property is not available for a given hour the value of the previous or next hour might be used/interpolated?
# @todo alerts: queue get commands while cache is updating?
#
# -----------------------------------------------------------------------------
= head1 FHEM COMMANDREF METADATA
= over
= item device
= 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
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData" > </a>
2024-02-25 21:05:29 +00:00
<h3> DWD_OpenData </h3>
<ul>
The Deutsche Wetterdienst ( DWD ) provides public weather related data via its < a href = "https://www.dwd.de/DE/leistungen/opendata/opendata.html" > Open Data Server </a> . Any usage of the service and the data provided by the DWD is subject to the usage conditions on the Open Data Server webpage . An overview of the available content can be found at < a href = "https://www.dwd.de/DE/leistungen/opendata/help/inhalt_allgemein/opendata_content_de_en_xls.xls" > OpenData_weather_content . xls </a> . <br> <br>
This module provides two elements of the available data:
<ul> <br>
<li> weather forecasts:
< a href = "https://opendata.dwd.de/weather/local_forecasts/mos/MOSMIX_L/single_stations/" > Total lists of local forecasts of WMO , national and interpolated stations , all variables , 3 , 9 , 15 , 21 UTC </a> . 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 . <br> <br>
You can request forecasts for different stations in sequence using the command <code> get forecast & lt ; station code & gt ; </code> or for one station continuously using the attribute <code> forecastStation </code> . To get continuous mode for more than one station you need to create separate DWD_OpenData devices . <br> <br>
In continuous mode the forecast data will be shifted by one day at midnight without requiring new data from the DWD . <br> <br>
</li> <br>
<li> weather alerts:
< a href = "https://opendata.dwd.de/weather/alerts/cap" > Warning status for Germany as union of referenced community /district warnings</ a > . This data is updated by the DWD as required . <br> <br>
After updating the alerts cache using the command <code> get updateAlertsCache & lt ; mode & gt ; </code> you can request alerts for different warncells in sequence using the command <code> get alerts & lt ; warncell id & gt ; </code> . Setting the attribute <code> alertArea </code> will enable continuous mode . To get continuous mode for more than one station you need to create separate DWD_OpenData devices . <br> <br>
Notes: This function is not suitable to rely on to ensure your safety ! It will cause significant download traffic if used in continuous mode ( more than 1 GB per day are possible ) . The device needs to keep all alerts for Germany in memory at all times to comply with the requirements of the common alerting protocol ( CAP ) , even if only one warn cell is monitored . Depending on the weather activity this requires noticeable amounts of memory and CPU .
</li>
</ul> <br>
Installation notes: <br> <br>
<ul>
<li> This module requires the additional Perl module <code> XML:: LibXML </code> for weather alerts . It can be installed depending on your OS and your preferences ( e . g . <code> sudo apt - get install libxml - libxml - perl </code> or using CPAN ) . </li> <br>
<li> 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 <code> proxy </code> to a suitable value in the format <code> myProxyHost:myProxyPort </code> . </li> <br>
<li> Verify that your FHEM time is correct by entering <code> { localtime ( ) } </code> into the FHEM command line . If not , check the system time and timezone of your FHEM server and adjust appropriately . It may be necessary to add <code> export TZ = `cat /etc/timezone` </code> or something similar to your FHEM start script <code> /etc/i nit . d /fhem</co de > or your system configuration file <code> /etc/ profile </code> . If <code> /etc/ timezone </code> does not exists or is undefined execute <code> tzselect </code> to find your timezone and write the result into this file . After making changes restart FHEM and enter <code> { $ ENV { TZ } } </code> into the FHEM command line to verify . To fix the timezone temporarily without restarting FHEM enter <code> { $ ENV { TZ } = 'Europe/Berlin' } </code> or something similar into the FHEM command line . Again use <code> tzselect </code> to fine a valid timezone name . </li> <br>
<li> The weekday of the forecast will be in the language of your FHEM system . Enter <code> { $ ENV { LANG } } </code> into the FHEM command line to verify . If nothing is displayed or you see an unexpected language setting , add <code> export LANG = de_DE . UTF - 8 </code> or something similar to your FHEM start script , restart FHEM and check again . If you get a locale warning when starting FHEM the required language pack might be missing . It can be installed depending on your OS and your preferences ( e . g . <code> dpkg - reconfigure locales </code> , <code> apt - get install language - pack - de </code> or something similar ) . </li> <br>
<li> The digits in a warncell id of a communeunion or a district are mostly identical to an <i> Amtliche Gemeindekennziffer </i> if you strip of the 1 st digit from the warncell id . You can lookup an Amtliche Gemeindekennziffer using the name of a communeunion or district e . g . at < a href = "https://www.statistik-bw.de/Statistik-Portal/gemeindeverz.asp" > Statistische & Auml ; mter des Bundes und der L & auml ; nder </a> . Then add 8 for a communeunion or 1 or 9 for a district at the beginning and try to find an exact or near match in the < a href = "https://www.dwd.de/DE/leistungen/opendata/help/warnungen/cap_warncellids_csv.csv" > Warncell - IDs for CAP alerts catalogue </a> . This approach is an alternative to <i> guessing </i> the right warncell id by the name of a communeunion or district . </li> <br>
<li> 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 . </li> <br>
<li> 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 . </li> <br>
<li> Starting on 17.09 .2018 the forecast data from the DWD 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 . </li> <br>
<li> This module provides sun position related information that is not available from the DWD . The properties for sunrise , sunset and sun up are calculated for the upper solar limb at given altitude and typical atmospheric refraction . </li> <br>
</ul> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-define" > </a>
2024-02-25 21:05:29 +00:00
<b> Define </b> <br> <br>
<code> define & lt ; name & gt ; DWD_OpenData </code> <br> <br> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-get" > </a>
2024-02-25 21:05:29 +00:00
<b> Get </b>
<ul> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-get-forecast" > </a>
2024-02-25 21:05:29 +00:00
<li>
<code> get forecast [ & lt ; station code & gt ; ] </code> <br>
Fetch forecast for a station from DWD and update readings . The station code is either a 5 digit WMO station code or an alphanumeric DWD station code from the < a href = "https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_stationskatalog.pdf" > MOSMIX station catalogue </a> . If the attribute <code> forecastStation </code> is set , no <i> station code </i> must be provided . <br>
The operation is performed non - blocking .
</li> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-get-alerts" > </a>
2024-02-25 21:05:29 +00:00
<li>
<code> get alerts [ & lt ; warncell id & gt ; ] </code> <br>
Set alert readings for given warncell id . A warncell id is a 9 digit numeric value from the < a href = "https://www.dwd.de/DE/leistungen/opendata/help/warnungen/cap_warncellids_csv.csv" > Warncell - IDs for CAP alerts catalogue </a> . Supported ids start with 8 ( communeunion ) , 1 and 9 ( district ) or 5 ( coast ) . If the attribute <code> alertArea </code> is set , no <i> warncell id </i> must be provided . <br>
If the alerts cache is empty or older than 15 minutes the cache is updated first and the operation is non - blocking . If the cache is valid the operation is blocking . If a cache update is already in progress the operation fails . <br>
To verify that alerts are provided for the warncell id you selected you should consult another source , wait for an alert situation and compare .
</li> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-get-updateAlertsCache" > </a>
2024-02-25 21:05:29 +00:00
<li>
<code> get updateAlertsCache { communeUnions | districts | all } </code> <br>
Fetch alerts to update the alerts cache . Note that 'coast' alerts are part of the 'communeUnion' cache data . <br>
The operation is performed non - blocking because it typically requires several seconds . If a cache update is already in progress the operation fails . <br>
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 .
</li>
</ul> <br> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-attr" > </a>
2024-02-25 21:05:29 +00:00
<b> Attributes </b> <br>
<ul> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-attr-disable" > </a>
2024-02-25 21:05:29 +00:00
<li> disable { 0 | 1 } , default: 0 <br>
Disable fetching data .
</li> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-attr-downloadTimeout" > </a>
<li> downloadTimeout & lt ; n & gt ; , 30 .. 60 s , default: 30 s<br>
Timeout for downloading data ( alerts , forecasts ) from DWD server .
2024-02-25 21:05:29 +00:00
</li> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-attr-timezone" > </a>
2024-02-25 21:05:29 +00:00
<li> timezone & lt ; tz & gt ; , default: OS dependent <br>
< a href = "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones" > IANA TZ string </a> 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 <code> tzselect </code> on the Linux command line to find a valid timezone string .
</li> <br>
</ul>
<b> forecast </b> related:
<ul> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-attr-forecastStation" > </a>
2024-02-25 21:05:29 +00:00
<li> forecastStation & lt ; station code & gt ; , default: none <br>
Setting forecastStation enables automatic updates every hour .
2024-02-28 22:44:32 +00:00
The station code is either a 5 digit WMO station code or an alphanumeric DWD station code from the <code> id </code> column of the
< a href = "https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_stationskatalog.pdf" > MOSMIX station catalogue </a> . <br>
2024-02-25 21:05:29 +00:00
Note: When value is changed all existing forecast readings will be deleted .
</li> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-attr-forecastDays" > </a>
<li> forecastDays & lt ; n & gt ; , default: 6 d <br>
2024-02-25 21:05:29 +00:00
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 ) .
</li> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-attr-forecastResolution" > </a>
2024-02-25 21:05:29 +00:00
<li> forecastResolution { 1 | 3 | 6 } , default: 6 h <br>
Time resolution ( number of hours between 2 samples ) . <br>
Note: When value is changed all existing forecast readings will be deleted .
</li> <br>
2024-03-01 23:25:12 +00:00
< a id = "DWD_OpenData-attr-forecastRefresh" > </a>
<li> forecastRefresh & lt ; n & gt ; , 1 .. 6 h , default: 6 h <br>
2024-02-28 22:44:32 +00:00
The DWD distinguishes between MOSMIX S and L reports , which differ in terms of update frequency and available data elements: <br>
2024-03-03 17:20:14 +00:00
- 1 .. 5 h: MOSMIX S , 40 data elements , updated every 1 h at ~ 25 min past every hour , download volume ~ 40 MB / h <br>
2024-03-01 23:25:12 +00:00
- 6 h: MOSMIX L , ~ 115 data elements , updated every 6 h at ~ 55 min past 21 /3/ 9 /15 UTC, download volume ~3 kB/ h <br>
2024-02-28 22:44:32 +00:00
See the
< a href = "https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_verfahrenbeschreibung_gesamt.pdf" > MOSMIX processes description </a>
and the
< a href = "https://www.dwd.de/DE/leistungen/opendata/help/schluessel_datenformate/kml/mosmix_elemente_xls.html" > MOSMIX data element discription </a>
for more details . <br> <br>
Notes for using MOSMIX S: <br>
- MOSMIX S is an EXPERIMENTAL feature and may cause system instability . <br>
2024-03-03 17:20:14 +00:00
- MOSMIX S requires more than 10000 times the recources of MOSMIX L . <br>
- Consider dynamically adapting forecastRefresh to your requirements to save recources , e . g . slower refresh at night if you focus is sun related . <br>
- Minimum hardware recommendations: CPU with 2 cores , 2 GB RAM , 1 GB tmpfs for / tmp or magnetic disk . <br>
- When using an SD card for /tmp its lifetime will be reduced significantly due to the write rate of ~700 MB/ h . <br>
- Processing time dependes on download bandwidth and hardware performance and may take half a minute or more . <br>
- Depending on the available download bandwidth the attribute downloadTimeout must be adjusted . <br>
2024-03-01 23:25:12 +00:00
- When switching between MOSMIX S and L all existing forecast readings will be deleted .
2024-02-25 21:05:29 +00:00
</li> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-attr-forecastProperties" > </a>
2024-02-25 21:05:29 +00:00
<li> forecastProperties [ & lt ; p1 & gt ; [ , & lt ; p2 & gt ; ] ... ] , default: Tx , Tn , Tg , TTT , DD , FX1 , Neff , RR6c , RRhc , Rh00 , ww <br>
See the < a href = "https://opendata.dwd.de/weather/lib/MetElementDefinition.xml" > DWD forecast property defintions </a> for more details . <br>
Notes: <br>
- Not all properties are available for all stations and for all hours . <br>
- If you remove a property from the list then already existing readings must be deleted manually in continuous mode . <br>
</li> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-attr-forecastWW2Text" > </a>
2024-02-25 21:05:29 +00:00
<li> forecastWW2Text { 0 | 1 } , default: 0 <br>
Create additional wwd readings containing the weather code as a descriptive text in German language .
</li> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-attr-forecastPruning" > </a>
2024-02-25 21:05:29 +00:00
<li> forecastPruning { 0 | 1 } , default: 0 <br>
Search for and delete forecast readings that are more then one day older then other forecast readings of the same day . Pruning will be performed after a successful forecast update . <br>
Notes: <br>
- Intended to maintain data consistency e . g . when a forecast station changes the reporting hour of a forecast property . <br>
- Requires noticable extra computing resources and may cause side effects if your FHEM configuration depends on a reading that is deleted . <br>
</li> <br>
</ul>
<b> alert </b> related:
<ul> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-attr-alertArea" > </a>
2024-02-25 21:05:29 +00:00
<li> alertArea & lt ; warncell id & gt ; , default: none <br>
Setting alertArea enables automatic updates of the alerts cache every 15 minutes . <br>
A warncell id is a 9 digit numeric value from the < a href = "https://www.dwd.de/DE/leistungen/opendata/help/warnungen/cap_warncellids_csv.csv" > Warncell - IDs for CAP alerts catalogue </a> . Supported ids start with 7 and 8 ( communeunion ) , 1 and 9 ( district ) or 5 ( coast ) . To verify that alerts are provided for the warncell id you selected you should consult another source , wait for an alert situation and compare .
</li>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-attr-alertLanguage" > </a>
2024-02-25 21:05:29 +00:00
<li> alertLanguage [ DE | EN ] , default: DE <br>
Language of descriptive alert properties .
</li>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-attr-alertExcludeEvents" > </a>
2024-02-25 21:05:29 +00:00
<li> alertExcludeEvents & lt ; event code & gt ; , default: none <br>
Comma separated list of numeric events codes for which no alerts should be created . <br>
Only minor alerts may be suppressed . Use at your own risk !
</li>
</ul> <br> <br>
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData-readings" > </a>
2024-02-25 21:05:29 +00:00
<b> Readings </b> <br> <br>
The <b> forecast </b> readings are build like this: <br> <br>
<code> fc & lt ; day & gt ; _ [ & lt ; sample & gt ; _ ] & lt ; property & gt ; </code> <br> <br>
A description of the more than 70 properties available and their units of measurement can be found < a href = "https://opendata.dwd.de/weather/lib/MetElementDefinition.xml" > here </a> . 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: <br> <br>
<ul>
<li> day - relative day ( 0 .. 9 ) based on the timezone attribute where 0 is today </li> <br>
<li> sample - relative time ( 0 .. 3 , 7 or 23 ) equivalent to multiples of 6 , 3 or 1 hours UTC depending on the <code> forecastResolution </code> attribute </li> <br>
<li> day properties ( typically for 06 : 00 station time , see raw data of station for actual time relation )
<ul>
<li> date - date based on the timezone attribute </li>
<li> weekday - abbreviated weekday based on the timezone attribute in the language of your FHEM system </li>
<li> Tn [ ° C ] - minimum temperature of previous 12 hours </li>
<li> Tx [ ° C ] - maximum temperature of previous 12 hours ( typically at 18 : 00 station time ) </li>
<li> Tm [ ° C ] - average temperature of previous 24 hours </li>
<li> Tg [ ° C ] - minimum temperature 5 cm above ground of previous 12 hours </li>
<li> PEvap [ kg /m2] - evapotranspiration of previous 24 hours</ li >
<li> SunD [ s ] - total sunshine duration of previous day </li>
</ul>
</li> <br>
<li> hour properties
<ul>
<li> time - time based on the timezone attribute </li>
<li> TTT [ ° C ] - dry bulb temperature at 2 meter above ground </li>
<li> Td [ ° C ] - dew point temperature at 2 meter above ground </li>
<li> DD [ ° ] - average wind direction 10 m above ground </li>
<li> FF [ km /h] - average wind speed 10 m above ground</ li >
<li> FX1 [ km /h] - maximum wind speed in the last hour</ li >
<li> SunD1 [ s ] - sunshine duration in the last hour </li>
<li> SunD3 [ s ] - sunshine duration in the last 3 hours </li>
<li> RR1c [ kg /m2] - precipitation amount in the last hour</ li >
<li> RR3c [ kg /m2] - precipitation amount in the last 3 hours</ li >
<li> RR6c [ kg /m2] - precipitation amount in the last 6 hours</ li >
<li> R600 [ % ] - probability of rain in the last 6 hours </li>
<li> RRhc [ kg /m2] - precipitation amount in the last 12 hours</ li >
<li> Rh00 [ % ] - probability of rain in the last 12 hours </li>
<li> RRdc [ kg /m2] - precipitation amount in the last 24 hours</ li >
<li> Rd00 [ % ] - probability of rain in the last 24 hours </li>
<li> ww - weather code ( see WMO 4680 /4677, SYNOP)</ li >
<li> wwd - German weather code description </li>
<li> VV [ m ] - horizontal visibility </li>
<li> Neff [ % ] - effective cloud cover </li>
<li> Nl [ % ] - lower level cloud cover below 2000 m </li>
<li> Nm [ % ] - medium level cloud cover below 7000 m </li>
<li> Nh [ % ] - high level cloud cover above 7000 m </li>
<li> PPPP [ hPa ] - pressure equivalent at sea level </li>
</ul>
</li>
<li> extra day properties , not provided by the DWD
<ul>
<li> SunRise - time of sunrise based on the timezone attribute </li>
<li> SunSet - time of sunset based on the timezone attribute </li>
</ul>
</li>
<li> extra hour properties , not provided by the DWD
<ul>
<li> SunAz [ ° ] - sun azimuth </li>
<li> SunEl [ ° ] - sun elevation </li>
<li> SunUp - sun up ( 0 : night , 1 : day ) </li>
</ul>
</li>
</ul> <br>
Additionally there are global forecast readings:
<ul>
<ul>
<li> fc_state - state of the last forecast update , possible values are 'updated' and 'error: ...' </li>
<li> fc_station - forecast station code ( WMO or DWD ) </li>
<li> fc_description - station description </li>
<li> fc_coordinates - world coordinate and height of station </li>
<li> fc_time - time the forecast was issued based on the timezone attribute </li>
2024-03-01 23:25:12 +00:00
<li> fc_url - URL of the forecast report document on the DWD webserver </li>
<li> fc_dwdDocTime - time of the forecast report document on the DWD webserver ( UTC ) </li>
<li> fc_dwdDocSize - size of the forecast report document on the DWD webserver ( bytes ) </li>
2024-02-25 21:05:29 +00:00
<li> fc_copyright - legal information , must be displayed with forecast data , see DWD usage conditions </li>
</ul>
</ul> <br>
The <b> alert </b> readings are ordered by onset and are build like this: <br> <br>
<code> a_ & lt ; index & gt ; _ & lt ; property & gt ; </code> <br> <br>
<ul>
<li> index - alert index , starting with 0 , total a_count , ordered by onset </li> <br>
<li> alert properties
<ul>
<li> category - 'Met' or 'Health' </li>
<li> event - numeric event code , see DWD documentation for details </li>
<li> eventDesc - short event description in selected language </li>
<li> eventGroup - event group , see DWD documentation for details </li>
<li> responseType - 'None' = no instructions , 'Prepare' = instructions , 'AllClear' = alert cleared </li>
<li> urgency - 'Immediate' = warning or 'Future' = information </li>
<li> severity - 'Minor' , 'Moderate' , 'Severe' or 'Extreme' </li>
<li> areaColor - RGB colour depending on urgency and severity , comma separated decimal triple </li>
<li> onset - start time of alert based on the timezone attribute </li>
<li> expires - end time of alert based on the timezone attribute </li>
<li> headline - headline in selected language , typically a combination of the properties urgency and event </li>
<li> description - description of the alert in selected language </li>
<li> instruction - safety instructions in selected language </li>
<li> area - numeric warncell id </li>
<li> areaDesc - description of area , e . g . 'Stadt Berlin' </li>
<li> altitude - min . altitude [ m ] </li>
<li> ceiling - max . altitude [ m ] </li>
</ul>
</li> <br>
</ul>
Additionally there are some global alert readings: <br> <br>
<ul>
<ul>
2024-03-01 23:25:12 +00:00
<li> a_state - state of the last alerts update , possible values are 'updated' and 'error: ...' </li>
<li> a_time - time the last alerts update was downloaded , based on the timezone attribute </li>
<li> a_count - number of alerts available for selected warncell id </li>
<li> a_url - URL of the alerts report document on the DWD webserver </li>
<li> a_dwdDocTime - time of the alerts report document on the DWD webserver ( UTC ) </li>
<li> a_dwdDocSize - size of the alerts report document on the DWD webserver ( bytes ) </li>
<li> a_copyright - legal information , must be displayed with forecast data , see DWD usage conditions , not available if count is zero </li>
2024-02-25 21:05:29 +00:00
</ul>
</ul> <br>
Alerts should be considered active for onset <= now < expires and responseType != 'AllClear' independent of urgency . <br>
Inactive alerts with responseType = 'AllClear' may provide relevant instructions . <br> <br>
Note that all alert readings are completely replaced and reindexed with each update ! <br> <br>
Further information regarding the alert properties can be found in the documentation of the < a href = "https://www.dwd.de/DE/leistungen/opendata/help/warnungen/cap_dwd_profile_de_pdf.pdf" > CAP DWS Profile </a> . <br> <br>
<b> Performance </b> <br> <br>
Note that depending on your device configuration each forecast consists of quite a lot of readings and each reading update will cause a FHEM event that needs to be processed . Depending on your hardware and your FHEM configuration this will take several hundred milliseconds . If you need to improve overall performance you can limit the number of readings created by setting a ) the attribute <code> forecastProperties </code> to the ones you actually use , b ) the attribute <code> forecastResolution </code> to the highest value suitable for your purposes and c ) the attribute <code> forecastDays </code> to the lowest number suitable for your purposes . To further reduce the event processing overhead you can set the attribute <code> event - on - update - reading </code> to a small list of important reading that really need events ( e . g . <code> state , fc_state , a_state </code> ) . For almost the same reason be selective when creating a log device . If you use wildcards for all readings without filtering either at the source device with < a href = "#readingFnAttributes" > readingFnAttributes </a> or at the destination device with a regexp you will get significant extra file IO when the readings are updated and quite a lot of data . <br>
</ul> <br>
= end html
= begin html_DE
2024-02-28 22:44:32 +00:00
< a id = "DWD_OpenData" > </a>
2024-02-25 21:05:29 +00:00
<h3> DWD_OpenData </h3>
<ul>
Der Deutsche Wetterdienst ( DWD ) stellt Wetterdaten & uuml ; ber den < a href = "https://www.dwd.de/DE/leistungen/opendata/opendata.html" > Open Data Server </a> zur Verf & uuml ; gung . Die Verwendung dieses Dienstes und der vom DWD zur Verf & uuml ; gung gestellten Daten unterliegt den auf der OpenData Webseite beschriebenen Bedingungen . Einen & Uuml ; berblick & uuml ; ber die verf & uuml ; gbaren Daten findet man in der Tabelle < a href = "https://www.dwd.de/DE/leistungen/opendata/help/inhalt_allgemein/opendata_content_de_en_xls.xls" > OpenData_weather_content . xls </a> . <br> <br>
Eine Installationsbeschreibung findet sich in der < a href = "https://wiki.fhem.de/wiki/DWD_OpenData" > FHEMWiki </a> . <br> <br>
Eine detaillierte Modulbeschreibung gibt es auf Englisch - siehe die englische Modulhilfe von < a href = "commandref.html#DWD_OpenData" > DWD_OpenData </a> . <br>
</ul> <br>
= end html_DE
= cut