From 4b8aad2047d2847d7861d8228ada052803b2ce77 Mon Sep 17 00:00:00 2001 From: Marko Oldenburg Date: Sat, 24 Dec 2022 04:39:45 +0100 Subject: [PATCH] add directory structure move API files remove YahooWeatherAPI --- 59_Weather.pm => FHEM/59_Weather.pm | 0 OpenWeatherMapAPI.pm | 962 --------------- YahooWeatherAPI.pm | 374 ------ .../FHEM/APIs/Weather/DarkSkyAPI.pm | 947 +++++++-------- lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm | 1029 +++++++++++++++++ .../FHEM/APIs/Weather/wundergroundAPI.pm | 210 ++-- 6 files changed, 1601 insertions(+), 1921 deletions(-) rename 59_Weather.pm => FHEM/59_Weather.pm (100%) delete mode 100644 OpenWeatherMapAPI.pm delete mode 100644 YahooWeatherAPI.pm rename DarkSkyAPI.pm => lib/FHEM/APIs/Weather/DarkSkyAPI.pm (59%) create mode 100644 lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm rename wundergroundAPI.pm => lib/FHEM/APIs/Weather/wundergroundAPI.pm (90%) diff --git a/59_Weather.pm b/FHEM/59_Weather.pm similarity index 100% rename from 59_Weather.pm rename to FHEM/59_Weather.pm diff --git a/OpenWeatherMapAPI.pm b/OpenWeatherMapAPI.pm deleted file mode 100644 index 9c6cc3d..0000000 --- a/OpenWeatherMapAPI.pm +++ /dev/null @@ -1,962 +0,0 @@ -# $Id: $ -############################################################################### -# -# Developed with VSCodium and richterger perl plugin -# -# (c) 2019-2023 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net) -# All rights reserved -# -# Special thanks goes to: -# - Harry (harryman) for many tests and patch that implements onecall API -# -# -# 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 -# any later version. -# -# The GNU General Public License can be found at -# http://www.gnu.org/copyleft/gpl.html. -# A copy is found in the textfile GPL.txt and important notices to the license -# from the author is found in LICENSE.txt distributed with these scripts. -# -# This script is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# -############################################################################### - -### Beispielaufruf -# https://api.openweathermap.org/data/2.5/weather?lat=[lat]&lon=[long]&APPID=[API] Current -# https://api.openweathermap.org/data/2.5/forecast?lat=[lat]&lon=[long]&APPID=[API] Forecast -# https://api.openweathermap.org/data/3.0/onecall?lat=[lat]&lon=[long]&APPID=[API] Current,Forecast -# https://openweathermap.org/weather-conditions Icons und Conditions ID's - -package FHEM::APIs::Weather::OpenWeatherMapAPI; -use strict; -use warnings; -use FHEM::Meta; - -use POSIX; -use HttpUtils; -use experimental qw /switch/; - -my %META; -my $ret = FHEM::Meta::getMetadata( __FILE__, \%META ); -return "$@" if ($@); -return $ret if ($ret); -$::packages{OpenWeatherMapAPI}{META} = \%META; -use version 0.77; our $VERSION = $::packages{OpenWeatherMapAPI}{META}{version}; - -# use Data::Dumper; - -# try to use JSON::MaybeXS wrapper -# for chance of better performance + open code -eval { - require JSON::MaybeXS; - import JSON::MaybeXS qw( decode_json encode_json ); - 1; -} or do { - - # try to use JSON wrapper - # for chance of better performance - eval { - # JSON preference order - local $ENV{PERL_JSON_BACKEND} = - 'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP' - unless ( defined( $ENV{PERL_JSON_BACKEND} ) ); - - require JSON; - import JSON qw( decode_json encode_json ); - 1; - } or do { - - # In rare cases, Cpanel::JSON::XS may - # be installed but JSON|JSON::MaybeXS not ... - eval { - require Cpanel::JSON::XS; - import Cpanel::JSON::XS qw(decode_json encode_json); - 1; - } or do { - - # In rare cases, JSON::XS may - # be installed but JSON not ... - eval { - require JSON::XS; - import JSON::XS qw(decode_json encode_json); - 1; - } or do { - - # Fallback to built-in JSON which SHOULD - # be available since 5.014 ... - eval { - require JSON::PP; - import JSON::PP qw(decode_json encode_json); - 1; - } or do { - - # Fallback to JSON::backportPP in really rare cases - require JSON::backportPP; - import JSON::backportPP qw(decode_json encode_json); - 1; - }; - }; - }; - }; -}; - -my $missingModul = ''; -## no critic (Conditional "use" statement. Use "require" to conditionally include a module (Modules::ProhibitConditionalUseStatements)) -eval { use Encode qw /encode_utf8/; 1 } or $missingModul .= 'Encode '; - -# use Data::Dumper; # for Debug only -## API URL -eval { use Readonly; 1 } - or $missingModul .= 'Readonly '; # apt install libreadonly-perl -## use critic - -# Readonly my $URL => 'https://api.openweathermap.org/data/2.5/'; -Readonly my $URL => 'https://api.openweathermap.org/data/'; -## URL . 'weather?' for current data -## URL . 'onecall?' for current,forecast data - -my %codes = ( - 200 => 45, - 201 => 45, - 202 => 45, - 210 => 4, - 211 => 4, - 212 => 3, - 221 => 4, - 230 => 45, - 231 => 45, - 232 => 45, - 300 => 9, - 301 => 9, - 302 => 9, - 310 => 9, - 311 => 9, - 312 => 9, - 313 => 9, - 314 => 9, - 321 => 9, - 500 => 35, - 501 => 35, - 502 => 35, - 503 => 35, - 504 => 35, - 511 => 35, - 520 => 35, - 521 => 35, - 522 => 35, - 531 => 35, - 600 => 14, - 601 => 16, - 602 => 13, - 611 => 46, - 612 => 46, - 613 => 46, - 615 => 5, - 616 => 5, - 620 => 14, - 621 => 46, - 622 => 42, - 701 => 19, - 711 => 22, - 721 => 19, - 731 => 23, - 741 => 20, - 751 => 23, - 761 => 19, - 762 => 3200, - 771 => 1, - 781 => 0, - 800 => 32, - 801 => 30, - 802 => 26, - 803 => 26, - 804 => 28, -); - -sub new { - ### geliefert wird ein Hash - my $class = shift; - my $argsRef = shift; - - my $apioptions = _parseApiOptions( $argsRef->{apioptions} ); - - my $self = { - devName => $argsRef->{devName}, - key => ( - ( defined( $argsRef->{apikey} ) && $argsRef->{apikey} ) - ? $argsRef->{apikey} - : 'none' - ), - lang => $argsRef->{language}, - lat => ( split( ',', $argsRef->{location} ) )[0], - long => ( split( ',', $argsRef->{location} ) )[1], - fetchTime => 0, - endpoint => 'none', - forecast => '', - alerts => 0, - }; - - $self->{cachemaxage} = ( - defined( $apioptions->{cachemaxage} ) - ? $apioptions->{cachemaxage} - : 900 - ); - - $self->{apiversion} = - ( $apioptions->{version} ? $apioptions->{version} : '2.5' ); - - $self->{cached} = _CreateForecastRef($self); - - bless $self, $class; - return $self; -} - -sub _parseApiOptions { - my $apioptions = shift; - - my @params; - my %h; - - @params = split( ',', $apioptions ); - while (@params) { - my $param = shift(@params); - next if ( $param eq '' ); - my ( $key, $value ) = split( ':', $param, 2 ); - $h{$key} = $value; - } - - return \%h; -} - -sub setAlerts { - my $self = shift; - my $alerts = shift // 0; - - $self->{alerts} = $alerts; - return; -} - -sub setForecast { - my $self = shift; - my $forecast = shift // ''; - - $self->{forecast} = $forecast; - return; -} - -sub setFetchTime { - my $self = shift; - - $self->{fetchTime} = time(); - return; -} - -sub setRetrieveData { - my $self = shift; - - _RetrieveDataFromOpenWeatherMap($self); - return; -} - -sub setLocation { - my $self = shift; - my $lat = shift; - my $long = shift; - - $self->{lat} = $lat; - $self->{long} = $long; - - return; -} - -sub getFetchTime { - my $self = shift; - - return $self->{fetchTime}; -} - -sub getWeather { - my $self = shift; - - return $self->{cached}; -} - -sub _RetrieveDataFromOpenWeatherMap { - my $self = shift; - - # retrieve data from cache - if ( ( time() - $self->{fetchTime} ) < $self->{cachemaxage} - && $self->{cached}->{lat} == $self->{lat} - && $self->{cached}->{long} == $self->{long} - && $self->{endpoint} eq 'none' ) - { - return _CallWeatherCallbackFn($self); - } - - $self->{cached}->{lat} = $self->{lat} - unless ( $self->{cached}->{lat} == $self->{lat} ); - $self->{cached}->{long} = $self->{long} - unless ( $self->{cached}->{long} == $self->{long} ); - - my $paramRef = { - timeout => 15, - self => $self, - endpoint => $self->{endpoint} eq 'none' ? 'onecall' : 'none', - callback => \&_RetrieveDataFinished, - }; - - $self->{endpoint} = $paramRef->{endpoint}; - - if ( $self->{lat} eq 'error' - || $self->{long} eq 'error' - || $self->{key} eq 'none' - || $missingModul ) - { - _RetrieveDataFinished( - $paramRef, -'The given location is invalid. (wrong latitude or longitude?) put both as an attribute in the global device or set define option location=[LAT],[LONG]', - undef - ) if ( $self->{lat} eq 'error' || $self->{long} eq 'error' ); - - _RetrieveDataFinished( $paramRef, - 'No given api key. (define myWeather Weather apikey=[KEY])', - undef ) - if ( $self->{key} eq 'none' ); - - _RetrieveDataFinished( $paramRef, - 'Perl modul ' . $missingModul . ' is missing.', undef ) - if ($missingModul); - } - else { - $paramRef->{url} = - $URL - . $self->{apiversion} . '/' - . $paramRef->{endpoint} . '?' . 'lat=' - . $self->{lat} . '&' . 'lon=' - . $self->{long} . '&' - . 'APPID=' - . $self->{key} . '&' - . 'units=' - . 'metric' . '&' . 'lang=' - . $self->{lang} . '&' - . 'exclude=' - . _CreateExcludeString( $self->{forecast}, $self->{alerts} ); - - ::HttpUtils_NonblockingGet($paramRef); - } - - return; -} - -sub _CreateExcludeString { - my $forecast = shift; - my $alerts = shift; - - my @exclude = qw/alerts minutely hourly daily/; - my @forecast = split( ',', $forecast ); - my @alerts = ( $alerts ? 'alerts' : '' ); - - my %in_forecast = map { $_ => 1 } @forecast, @alerts; - my @diff = grep { not $in_forecast{$_} } @exclude; - - return join( ',', @diff ); -} - -sub _RetrieveDataFinished { - my $paramRef = shift; - my $err = shift; - my $response = shift; - my $self = $paramRef->{self}; - - if ( !$err ) { - $self->{cached}->{status} = 'ok'; - $self->{cached}->{validity} = 'up-to-date'; - $self->{fetchTime} = time(); - _ProcessingRetrieveData( $self, $response ); - } - else { - $self->{fetchTime} = time() if ( not defined( $self->{fetchTime} ) ); - _ErrorHandling( $self, $err ); - _ProcessingRetrieveData( $self, $response ); - } - - return; -} - -sub _ProcessingRetrieveData { - my $self = shift; - my $response = shift; - - if ( $self->{cached}->{status} eq 'ok' - && defined($response) - && $response ) - { - if ( $response =~ m/^{.*}$/x ) { - my $data = eval { decode_json($response) }; - - if ($@) { - _ErrorHandling( $self, - 'OpenWeatherMap Weather decode JSON err ' . $@ ); - } - elsif (defined( $data->{cod} ) - && $data->{cod} - && $data->{cod} != 200 - && defined( $data->{message} ) - && $data->{message} ) - { - _ErrorHandling( $self, $data->{cod} . ': ' . $data->{message} ); - } - else { - ### Debug - # print '!!! DEBUG !!! - Endpoint: ' . $self->{endpoint} . "\n"; - # print '!!! DEBUG !!! - Response: ' . Dumper $data; - ###### Ab hier wird die ResponseHash Referenze für die Rückgabe zusammen gestellt - $self->{cached}->{current_date_time} = - strftimeWrapper( "%a, %e %b %Y %H:%M", - localtime( $self->{fetchTime} ) ); - - given ( $self->{endpoint} ) { - when ('onecall') { - ## löschen des alten current Datensatzes - delete $self->{cached}->{current}; - - ## löschen des alten forecast Datensatzes - delete $self->{cached}->{forecast}; - - ## löschen des alten Alerts Datensatzes - delete $self->{cached}->{alerts}; - - $self->{cached}->{current} = { - 'temperature' => int( - sprintf( "%.0f", $data->{current}->{temp} ) - ), - 'temp_c' => int( - sprintf( "%.0f", $data->{current}->{temp} ) - ), - 'tempFeelsLike_c' => int( - sprintf( "%.0f", - $data->{current}->{feels_like} ) - ), - 'dew_point' => int( - sprintf( - "%.0f", $data->{current}->{dew_point} - ) - ), - 'humidity' => $data->{current}->{humidity}, - 'condition' => encode_utf8( - $data->{current}->{weather}->[0]->{description} - ), - 'pressure' => int( - sprintf( "%.1f", $data->{current}->{pressure} ) - + 0.5 - ), - 'wind' => int( - sprintf( "%.1f", - ( $data->{current}->{wind_speed} * 3.6 ) ) - + 0.5 - ), - 'wind_speed' => int( - sprintf( "%.1f", - ( $data->{current}->{wind_speed} * 3.6 ) ) - + 0.5 - ), - 'wind_gust' => int( - sprintf( "%.1f", - ( $data->{current}->{wind_gust} * 3.6 ) ) + - 0.5 - ), - 'wind_direction' => $data->{current}->{wind_deg}, - 'rain_1h' => $data->{rain}->{'1h'}, - 'cloudCover' => $data->{current}->{clouds}, - 'code' => - $codes{ $data->{current}->{weather}->[0]->{id} }, - 'iconAPI' => - $data->{current}->{weather}->[0]->{icon}, - 'condition' => encode_utf8( - $data->{current}->{weather}->[0]->{description} - ), - 'sunsetTime' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( $data->{current}->{sunset} ) - ), - 'sunriseTime' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( $data->{current}->{sunrise} ) - ), - 'pubDate' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( $data->{current}->{dt} ) - ), - 'visibility' => int( - sprintf( "%.1f", - $data->{current}->{visibility} ) + 0.5 - ), - 'uvi' => $data->{current}->{uvi}, - 'timezone' => $data->{timezone}, - 'timezone_offset' => $data->{timezone_offset}, - }; - - if ( ref( $data->{hourly} ) eq "ARRAY" - && scalar( @{ $data->{hourly} } ) > 0 ) - { - my $i = 0; - for ( @{ $data->{hourly} } ) { - push( - @{ $self->{cached}->{forecast}->{hourly} }, - { - 'pubDate' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - $data->{hourly}->[$i]->{dt} - ) - ), - 'day_of_week' => strftime( - "%a, %H:%M", - localtime( - $data->{hourly}->[$i]->{dt} - ) - ), - 'temperature' => int( - sprintf( "%.0f", - $data->{hourly}->[$i]->{temp} ) - ), - 'temp_c' => int( - sprintf( "%.0f", - $data->{hourly}->[$i]->{temp} ) - ), - 'tempFeelsLike' => int( - sprintf( "%.0f", - $data->{hourly}->[$i] - ->{feels_like} ) - ), - 'dew_point' => int( - sprintf( "%.0f", - $data->{hourly}->[$i] - ->{dew_point} ) - ), - 'humidity' => - $data->{hourly}->[$i]->{humidity}, - 'condition' => encode_utf8( - $data->{hourly}->[$i]->{weather} - ->[0]->{description} - ), - 'pressure' => int( - sprintf( "%.1f", - $data->{hourly}->[$i] - ->{pressure} ) + 0.5 - ), - 'wind' => int( - sprintf( - "%.1f", - ( - $data->{hourly}->[$i] - ->{wind_speed} * 3.6 - ) - ) + 0.5 - ), - 'wind_speed' => int( - sprintf( - "%.1f", - ( - $data->{hourly}->[$i] - ->{wind_speed} * 3.6 - ) - ) + 0.5 - ), - 'wind_gust' => int( - sprintf( - "%.1f", - ( - $data->{hourly}->[$i] - ->{wind_gust} * 3.6 - ) - ) + 0.5 - ), - 'wind_direction' => - $data->{hourly}->[$i]->{wind_deg}, - 'cloudCover' => - $data->{hourly}->[$i]->{clouds}, - 'code' => $codes{ - $data->{hourly}->[$i]->{weather} - ->[0]->{id} - }, - 'iconAPI' => - $data->{hourly}->[$i]->{weather}->[0] - ->{icon}, - 'rain1h' => - $data->{hourly}->[$i]->{rain}->{'1h'}, - 'snow1h' => - $data->{hourly}->[$i]->{snow}->{'1h'}, - 'uvi' => $data->{hourly}->[$i]->{uvi}, - 'visibility' => int( - sprintf( "%.1f", - $data->{hourly}->[$i] - ->{visibility} ) + 0.5 - ), - }, - ); - - $i++; - } - } - - if ( ref( $data->{daily} ) eq "ARRAY" - && scalar( @{ $data->{daily} } ) > 0 ) - { - my $i = 0; - for ( @{ $data->{daily} } ) { - push( - @{ $self->{cached}->{forecast}->{daily} }, - { - 'pubDate' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - $data->{daily}->[$i]->{dt} - ) - ), - 'day_of_week' => strftime( - "%a, %H:%M", - localtime( - $data->{daily}->[$i]->{dt} - ) - ), - 'sunrise' => strftime( - "%H:%M", - localtime( - $data->{daily}->[$i]->{sunrise} - ) - ), - 'sunset' => strftime( - "%a, %H:%M", - localtime( - $data->{daily}->[$i]->{sunset} - ) - ), - 'moonrise' => strftime( - "%a, %H:%M", - localtime( - $data->{daily}->[$i]->{moonrise} - ) - ), - 'moon_phase' => - $data->{daily}->[$i]->{moon_phase}, - 'moonset' => strftime( - "%a, %H:%M", - localtime( - $data->{daily}->[$i]->{moonset} - ) - ), - 'temperature' => int( - sprintf( "%.0f", - $data->{daily}->[$i]->{temp} - ->{day} ) - ), - 'temperature_morn' => int( - sprintf( "%.0f", - $data->{daily}->[$i]->{temp} - ->{morn} ) - ), - 'temperature_eve' => int( - sprintf( "%.0f", - $data->{daily}->[$i]->{temp} - ->{eve} ) - ), - 'temperature_night' => int( - sprintf( "%.0f", - $data->{daily}->[$i]->{temp} - ->{night} ) - ), - 'tempFeelsLike_morn' => int( - sprintf( "%.0f", - $data->{daily}->[$i] - ->{feels_like}->{morn} ) - ), - 'tempFeelsLike_eve' => int( - sprintf( "%.0f", - $data->{daily}->[$i] - ->{feels_like}->{eve} ) - ), - 'tempFeelsLike_night' => int( - sprintf( "%.0f", - $data->{daily}->[$i] - ->{feels_like}->{night} ) - ), - 'tempFeelsLike_day' => int( - sprintf( "%.0f", - $data->{daily}->[$i] - ->{feels_like}->{day} ) - ), - 'temp_c' => int( - sprintf( "%.0f", - $data->{daily}->[$i]->{temp} - ->{day} ) - ), - 'low_c' => int( - sprintf( "%.0f", - $data->{daily}->[$i]->{temp} - ->{min} ) - ), - 'high_c' => int( - sprintf( "%.0f", - $data->{daily}->[$i]->{temp} - ->{max} ) - ), - 'tempLow' => int( - sprintf( "%.0f", - $data->{daily}->[$i]->{temp} - ->{min} ) - ), - 'tempHigh' => int( - sprintf( "%.0f", - $data->{daily}->[$i]->{temp} - ->{max} ) - ), - 'dew_point' => int( - sprintf( "%.0f", - $data->{daily}->[$i] - ->{dew_point} ) - ), - 'humidity' => - $data->{daily}->[$i]->{humidity}, - 'condition' => encode_utf8( - $data->{daily}->[$i]->{weather} - ->[0]->{description} - ), - 'code' => $codes{ - $data->{daily}->[$i]->{weather} - ->[0]->{id} - }, - 'iconAPI' => - $data->{daily}->[$i]->{weather}->[0] - ->{icon}, - 'pressure' => int( - sprintf( "%.1f", - $data->{daily}->[$i]->{pressure} - ) + 0.5 - ), - 'wind' => int( - sprintf( - "%.1f", - ( - $data->{daily}->[$i] - ->{wind_speed} * 3.6 - ) - ) + 0.5 - ), - 'wind_speed' => int( - sprintf( - "%.1f", - ( - $data->{daily}->[$i] - ->{wind_speed} * 3.6 - ) - ) + 0.5 - ), - 'wind_gust' => int( - sprintf( - "%.1f", - ( - $data->{daily}->[$i] - ->{wind_gust} * 3.6 - ) - ) + 0.5 - ), - 'wind_direction' => int( - sprintf( - "%.1f", - ( - $data->{daily}->[$i] - ->{wind_deg} - ) - ) - ), - 'cloudCover' => - $data->{daily}->[$i]->{clouds}, - 'code' => $codes{ - $data->{daily}->[$i]->{weather} - ->[0]->{id} - }, - 'rain' => $data->{daily}->[$i]->{rain}, - 'snow' => $data->{daily}->[$i]->{snow}, - 'uvi' => $data->{daily}->[$i]->{uvi}, - }, - ); - - $i++; - } - } - - if ( ref( $data->{alerts} ) eq "ARRAY" - && scalar( @{ $data->{alerts} } ) > 0 ) - { - my $i = 0; - for ( @{ $data->{alerts} } ) { - push( - @{ $self->{cached}->{alerts} }, - { - 'End' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - ( - $data->{alerts}->[$i]->{end} - ) - ) - ), - 'Start' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - ( - $data->{alerts}->[$i] - ->{start} - ) - ) - ), - 'Description' => encode_utf8( - $data->{alerts}->[$i]->{description} - ), - 'SenderName' => encode_utf8( - $data->{alerts}->[$i]->{sender_name} - ), - 'Event' => encode_utf8( - $data->{alerts}->[$i]->{event} - ), - }, - ); - - $i++; - } - } - } - } - } - } - else { _ErrorHandling( $self, 'OpenWeatherMap ' . $response ); } - } - - $self->{endpoint} = 'none' if ( $self->{endpoint} eq 'onecall' ); - - _RetrieveDataFromOpenWeatherMap($self) - if ( $self->{endpoint} eq 'weather' ); - - _CallWeatherCallbackFn($self) if ( $self->{endpoint} eq 'none' ); - - return; -} - -sub _CallWeatherCallbackFn { - my $self = shift; - - # print 'Dumperausgabe: ' . Dumper $self; - ### Aufruf der callbackFn - ::Weather_RetrieveCallbackFn( $self->{devName} ); - - return; -} - -sub _ErrorHandling { - my $self = shift; - my $err = shift; - - $self->{cached}->{current_date_time} = - strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( $self->{fetchTime} ) ); - $self->{cached}->{status} = $err; - $self->{cached}->{validity} = 'stale'; - - return; -} - -sub _CreateForecastRef { - my $self = shift; - - my $forecastRef = ( - { - lat => $self->{lat}, - long => $self->{long}, - apiMaintainer => -'Marko Oldenburg (CoolTux)', - apiVersion => version->parse( __PACKAGE__->VERSION() )->normal, - } - ); - - return $forecastRef; -} - -sub strftimeWrapper { - my @data = @_; - my $string = POSIX::strftime(@data); - - $string =~ s/\xe4/ä/xg; - $string =~ s/\xc4/Ä/xg; - $string =~ s/\xf6/ö/xg; - $string =~ s/\xd6/Ö/xg; - $string =~ s/\xfc/ü/xg; - $string =~ s/\xdc/Ü/xg; - $string =~ s/\xdf/ß/xg; - $string =~ s/\xdf/ß/xg; - $string =~ s/\xe1/á/xg; - $string =~ s/\xe9/é/xg; - $string =~ s/\xc1/Á/xg; - $string =~ s/\xc9/É/xg; - - return $string; -} - -############################################################################## - -1; - -=pod - -=encoding utf8 - -=for :application/json;q=META.json OpenWeatherMapAPI.pm -{ - "abstract": "Weather API for Weather OpenWeatherMap", - "x_lang": { - "de": { - "abstract": "Wetter API für OpenWeatherMap" - } - }, - "version": "v3.0.2", - "author": [ - "Marko Oldenburg " - ], - "x_fhem_maintainer": [ - "CoolTux" - ], - "x_fhem_maintainer_github": [ - "CoolTuxNet" - ], - "prereqs": { - "runtime": { - "requires": { - "FHEM::Meta": 0, - "HttpUtils": 0, - "strict": 0, - "warnings": 0, - "constant": 0, - "POSIX": 0, - "JSON::PP": 0 - }, - "recommends": { - "JSON": 0 - }, - "suggests": { - "JSON::XS": 0, - "Cpanel::JSON::XS": 0 - } - } - } -} -=end :application/json;q=META.json - -=cut - -__END__ diff --git a/YahooWeatherAPI.pm b/YahooWeatherAPI.pm deleted file mode 100644 index a7be94c..0000000 --- a/YahooWeatherAPI.pm +++ /dev/null @@ -1,374 +0,0 @@ -# $Id: YahooWeatherAPI.pm 16641 2018-04-21 12:28:38Z neubert $ - -############################################################################## -# -# YahooWeatherAPI.pm -# Copyright by Dr. Boris Neubert -# e-mail: omega at online dot de -# -# This file is part of fhem. -# -# Fhem is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# Fhem is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with fhem. If not, see . -# -############################################################################## - -package main; - -use strict; -use warnings; -use HttpUtils; -use JSON; # apt-get install libperl-JSON on Debian and derivatives -#use Data::Dumper; # for Debug only - -# Yahoo! Weather API: http://developer.yahoo.com/weather/ - -use constant URL => "https://query.yahooapis.com/v1/public/yql?q=select%%20*%%20from%%20weather.forecast%%20where%%20woeid=%s%%20and%%20u=%%27c%%27&format=%s&env=store%%3A%%2F%%2Fdatatables.org%%2Falltableswithkeys"; - - -# Mapping / translation of current weather codes 0-47 -my @YahooCodes_en = ( - 'tornado', 'tropical storm', 'hurricane', 'severe thunderstorms', 'thunderstorms', 'mixed rain and snow', - 'mixed rain and sleet', 'mixed snow and sleet', 'freezing drizzle', 'drizzle', 'freezing rain' ,'showers', - 'showers', 'snow flurries', 'light snow showers', 'blowing snow', 'snow', 'hail', - 'sleet', 'dust', 'foggy', 'haze', 'smoky', 'blustery', - 'windy', 'cold', 'cloudy', - 'mostly cloudy', # night - 'mostly cloudy', # day - 'partly cloudy', # night - 'partly cloudy', # day - 'clear', - 'sunny', - 'fair', #night - 'fair', #day - 'mixed rain and hail', - 'hot', 'isolated thunderstorms', 'scattered thunderstorms', 'scattered thunderstorms', 'scattered showers', 'heavy snow', - 'scattered snow showers', 'heavy snow', 'partly cloudy', 'thundershowers', 'snow showers', 'isolated thundershowers'); - -my @YahooCodes_de = ( - 'Tornado', 'schwerer Sturm', 'Orkan', 'schwere Gewitter', 'Gewitter', 'Regen und Schnee', - 'Regen und Graupel', 'Schnee und Graupel', 'Eisregen', 'Nieselregen', 'gefrierender Regen' ,'Schauer', - 'Schauer', 'Schneetreiben', 'leichte Schneeschauer', 'Schneeverwehungen', 'Schnee', 'Hagel', - 'Graupel', 'Staub', 'Nebel', 'Dunst', 'Smog', 'Sturm', - 'windig', 'kalt', 'wolkig', - 'überwiegend wolkig', # night - 'überwiegend wolkig', # day - 'teilweise wolkig', # night - 'teilweise wolkig', # day - 'klar', # night - 'sonnig', - 'heiter', # night - 'heiter', # day - 'Regen und Hagel', - 'heiß', 'einzelne Gewitter', 'vereinzelt Gewitter', 'vereinzelt Gewitter', 'vereinzelt Schauer', 'starker Schneefall', - 'vereinzelt Schneeschauer', 'starker Schneefall', 'teilweise wolkig', 'Gewitterregen', 'Schneeschauer', 'vereinzelt Gewitter'); - -my @YahooCodes_nl = ( - 'tornado', 'zware storm', 'orkaan', 'hevig onweer', 'onweer', - 'regen en sneeuw', - 'regen en ijzel', 'sneeuw en ijzel', 'aanvriezende motregen', - 'motregen', 'aanvriezende regen' ,'buien', - 'buien', 'sneeuw windstoten', 'lichte sneeuwbuien', - 'stuifsneeuw', 'sneeuw', 'hagel', - 'ijzel', 'stof', 'mist', 'waas', 'smog', 'onstuimig', - 'winderig', 'koud', 'bewolkt', - 'overwegend bewolkt', # night - 'overwegend bewolkt', # day - 'gedeeltelijk bewolkt', # night - 'gedeeltelijk bewolkt', # day - 'helder', #night - 'zonnig', - 'mooi', #night - 'mooi', #day - 'regen en hagel', - 'heet', 'plaatselijk onweer', 'af en toe onweer', 'af en toe onweer', 'af en toe regenbuien', 'hevige sneeuwval', - 'af en toe sneeuwbuien', 'hevige sneeuwval', 'deels bewolkt', - 'onweersbuien', 'sneeuwbuien', 'af en toe onweersbuien'); - -my @YahooCodes_fr = ( - 'tornade', 'tempête tropicale', 'ouragan', 'tempête sévère', 'orage', 'pluie et neige', - 'pluie et grésil', 'neige et grésil', 'bruine verglassante', 'bruine', 'pluie verglassante' ,'averse', - 'averses', 'tourbillon de neige', 'légères averses de neige', 'rafale de neige', 'neige', 'grêle', - 'giboulées', 'poussières', 'brouillard', 'brume', 'enfumé', 'orageux', - 'venteux', 'froid', 'nuageux', - 'couverte', # night - 'couvert', # day - 'partiellement couverte', # night - 'partiellement couvert', # day - 'clair', - 'ensoleillé', - 'douce', #night - 'agréable', #day - 'pluie et grêle', - 'chaud', 'orages isolés', 'tempêtes éparses', 'orages épars', 'averses éparses', 'tempête de neige', - 'chûtes de neiges éparses', 'tempêtes de neige', 'partielement nuageux', 'averses orageuses', 'chûte de neige', 'chûtes de neige isolées'); - -my @YahooCodes_pl = ( - 'tornado', 'burza tropikalna', 'huragan', 'porywiste burze', 'burze', 'deszcz ze śniegiem', - 'deszcz i deszcz ze śniegiem', 'śnieg i deszcz ze śniegiem', 'marznąca mżawka', 'mżawka', 'marznący deszcz' ,'deszcz', - 'deszcz', 'przelotne opady śniegu', 'lekkie opady śniegu', 'zamieć śnieżna', 'śnieg', 'grad', - 'deszcz ze śniegiem', 'pył', 'mgła', 'mgła', 'smog', 'przenikliwie', - 'wietrznie', 'zimno', 'pochmurno', - 'pochmurno', # night - 'pochmurno', # day - 'częściowe zachmurzenie', # night - 'częściowe zachmurzenie', # day - 'czyste niebo', - 'słonecznie', - 'ładna noc', #night - 'ładny dzień', #day - 'deszcz z gradem', - 'gorąco', 'gdzieniegdzie burze', 'burze', 'burze', 'przelotne opady śniegu', 'duże opady śniegu', - 'ciężkie opady śniegu', 'dużo śniegu', 'częściowe zachmurzenie', 'burze z deszczem', 'opady śniegu', 'przejściowo burze'); - -my @YahooCodes_it = ( - 'tromba d\'aria', 'tempesta tropicale', 'uragano', 'temporali di grande intensità', 'temporali', 'pioggia mista e neve', - 'pioggia mista e nevischio', 'neve mista e nevischio', 'pioggia gelata', 'pioggia leggera', 'grandine' ,'rovesci', - 'piogge', 'raffiche di neve', 'deboli nevicate', 'bufera di neve', 'neve', 'grandine', - 'nevischio', 'pulviscolo', 'nebbia', 'foschia', 'smog', 'ventoso', - 'ventoso', 'freddo', 'nuvoloso', - 'parzialmente nuvoloso', # night - 'parzialmente nuvoloso', # day - 'parzialmente nuvoloso', # night - 'parzialmente nuvoloso', # day - 'sereno', - 'soleggiato', - 'bel tempo', #night - 'bel tempo', #day - 'pioggia mista a grandine', - 'caldo', 'temporali isolati', 'temporali sparsi', 'temporali sparsi', 'piogge sparse', 'forti nevicate', - 'nevicate sparse', 'forti nevicate', 'parzialmente nuvoloso', 'rovesci temporaleschi', 'rovesci di neve', 'temporali isolati'); - - -################################### - -# Cache -my %YahooWeatherAPI_CachedData= (); -my %YahooWeatherAPI_CachedDataTs= (); - -################################### - -# -# there is a bug in the Yahoo Weather API that gets all units wrong -# these routines fix that - - -sub value_to_C($) { - my ($F)= @_; - return(int(($F-32)*5/9+0.5)); -} - -sub value_to_hPa($) { - my ($inHg)= @_; - return int($inHg/33.86390+0.5); -} - -sub value_to_km($) { - my ($value)= @_; - return int($value/1.609347219+0.5); -} - -################################### - -# call: YahooWeatherAPI_RetrieveData(%%args) -# -# the args hash reference must contain at least -# woeid => WOEID [WHERE-ON-EARTH-ID], go to http://weather.yahoo.com to find out -# format => xml or json -# blocking => 0 or 1 -# callbackFnRef => reference to callback function with arguments ($argsRef, $err, $result) -# the args hash reference is returned as first argument of the callbackFn -# - -sub YahooWeatherAPI_RetrieveData($) { - my ($argsRef)= @_; - YahooWeatherAPI_RetrieveDataWithCache(0, $argsRef); -} - -sub YahooWeatherAPI_RetrieveDataWithCache($$) { - - my ($maxage, $argsRef)= @_; - my $woeid= $argsRef->{woeid}; - - Log3 undef, 5, "YahooWeatherAPI: retrieve weather for $woeid."; - - # retrieve data from cache - my $ts= $YahooWeatherAPI_CachedDataTs{$woeid}; - if(defined($ts)) { - my $now= time(); - my $age= $now- $ts; - if($age< $maxage) { - Log3 undef, 5, "YahooWeatherAPI: data is cached, age $age seconds < $maxage seconds."; - $argsRef->{callbackFnRef}($argsRef, "", $YahooWeatherAPI_CachedData{$woeid}); - return; - } else { - Log3 undef, 5, "YahooWeatherAPI: cache is expired, age $age seconds > $maxage seconds."; - } - } else { - Log3 undef, 5, "YahooWeatherAPI: no data in cache."; - } - - my $format= $argsRef->{format}; - my $blocking= $argsRef->{blocking}; - my $callbackFnRef= $argsRef->{callbackFnRef}; - - my $url = sprintf(URL, $woeid, $format); - - #Debug "Retrieve Yahoo Weather data for " . $argsRef->{hash}->{NAME}; - - if ($blocking) { - # do not use noshutdown => 0 in parameters - my $response = HttpUtils_BlockingGet({ url => $url, timeout => 15 }); - my %param= (argsRef => $argsRef); - YahooWeatherAPI_RetrieveDataFinished(\%param, undef, $response); - } else { - # do not use noshutdown => 0 in parameters - HttpUtils_NonblockingGet({ - url => $url, - timeout => 15, - argsRef => $argsRef, - callback => \&YahooWeatherAPI_RetrieveDataFinished, - }); - } -} - -sub YahooWeatherAPI_RetrieveDataFinished($$$) { - my ($paramRef, $err, $response) = @_; - my $argsRef= $paramRef->{argsRef}; - #Debug "Finished retrieving Yahoo Weather data for " . $argsRef->{hash}->{NAME}; - if(!$err) { - my $woeid= $argsRef->{woeid}; - $YahooWeatherAPI_CachedDataTs{$woeid}= time(); - $YahooWeatherAPI_CachedData{$woeid}= $response; - Log3 undef, 5, "YahooWeatherAPI: caching data."; - } - $argsRef->{callbackFnRef}($argsRef, $err, $response); -} - - -# this decodes a JSON result and returns the Weather Channel hash reference -sub YahooWeatherAPI_JSONReturnChannelData($) { - my ($response)= @_; - return("empty response", undef) unless($response); - #Debug "Decoding response: $response"; - #Debug "response: " . Dumper($response); - my $data; - eval { $data= decode_json($response) }; - return($@, undef) if($@); - my $query= $data->{query}; - #Debug Dumper($query); - my $count= $query->{count}; - #Debug "$count result(s)."; - return("$count result(s) retrieved", undef) unless($count == 1); - my $channel= $query->{results}{channel}; - return(undef, $channel); -} - -sub YahooWeatherAPI_ConvertChannelData($) { - my ($data)= @_; # hash reference - - $data->{wind}{chill}= value_to_C($data->{wind}{chill}); # # API delivers wrong value - $data->{atmosphere}{pressure}= value_to_hPa($data->{atmosphere}{pressure}); # API delivers wrong value - - - my $units= YahooWeatherAPI_units($data); # units hash reference - - $data->{wind}{speed}= value_to_km($data->{wind}{speed}); # API delivers km - $data->{atmosphere}{visibility}= value_to_km($data->{atmosphere}{visibility}); # API delivers km - - return 0 if($units->{temperature} eq "C"); - - my $item= $data->{item}; - $item->{condition}{temp}= value_to_C($item->{condition}{temp}); - - my $forecast= $item->{forecast}; - foreach my $fc (@{$forecast}) { - $fc->{low}= value_to_C($fc->{low}); - $fc->{high}= value_to_C($fc->{high}); - } - return 1; - -} - - -sub YahooWeatherAPI_ParseDateTime($) { - - my ($value)= @_; ### "Fri, 13 Nov 2015 8:00 am CET" - - my @months= qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/; - my %monthindex; - @monthindex{@months} = (0..$#months); - - if($value =~ '^(\w{3}), (\d{1,2}) (\w{3}) (\d{4}) (\d{1,2}):(\d{2}) (\w{2}) (\w{3,4})$') { - my ($wd, $d, $mon, $y, $h, $n, $p, $tz)= ($1,$2,$3,$4,$5,$6,$7,$8); - # 12 AM= 0, 12 PM= 12 - $h+=12 if($h==12); if($p eq "PM") { $h= ($h+12) % 24 } else { $h%= 12 }; - my $m= $monthindex{$mon}; - return undef unless defined($m); - #main::Debug "###### $value -> $wd $d $m $y $h:$n $tz"; - # $mday= 1.. - # $month= 0..11 - # $year is year-1900 - # we ignore the time zone as it probably never changes for a weather device an assume - # local time zone - return fhemTimeLocal(0, $n, $h, $d, $m, $y-1900); - } else { - return undef; - } -} - -sub YahooWeatherAPI_pubDate($) { - - my ($channel)= @_; - - ### pubDate Fri, 13 Nov 2015 8:00 am CET - if(!defined($channel->{item}{pubDate})) { - return("no pubDate received", "", undef); - }; - my $pubDate= $channel->{item}{pubDate}; - my $ts= YahooWeatherAPI_ParseDateTime($pubDate); - if(defined($ts)) { - return("okay", $pubDate, $ts); - } else { - return("could not parse pubDate $pubDate", $pubDate, undef); - } -} - -sub YahooWeatherAPI_units($) { - - my ($channel)= @_; - return $channel->{units}; -} - -sub YahooWeatherAPI_getYahooCodes($) { - - my ($lang)= @_; - - if($lang eq "de") { - return @YahooCodes_de; - } elsif($lang eq "nl") { - return @YahooCodes_nl; - } elsif($lang eq "fr") { - return @YahooCodes_fr; - } elsif($lang eq "pl") { - return @YahooCodes_pl; - } elsif($lang eq "it") { - return @YahooCodes_it; - } else { - return @YahooCodes_en; - } -} - -############################################################################## - -1; diff --git a/DarkSkyAPI.pm b/lib/FHEM/APIs/Weather/DarkSkyAPI.pm similarity index 59% rename from DarkSkyAPI.pm rename to lib/FHEM/APIs/Weather/DarkSkyAPI.pm index 4902524..077b05c 100644 --- a/DarkSkyAPI.pm +++ b/lib/FHEM/APIs/Weather/DarkSkyAPI.pm @@ -27,22 +27,24 @@ # # ############################################################################### - -package DarkSkyAPI; +package FHEM::APIs::Weather::DarkSkyAPI; use strict; use warnings; use FHEM::Meta; -use Data::Dumper; - -FHEM::Meta::Load(__PACKAGE__); -use version 0.50; our $VERSION = $main::packages{DarkSkyAPI}{META}{version}; - -package DarkSkyAPI::Weather; -use strict; -use warnings; use POSIX; use HttpUtils; +use experimental qw /switch/; + +my $META = {}; +my $ret = FHEM::Meta::getMetadata( __FILE__, $META ); +return "$@" if ($@); +return $ret if ($ret); +$::packages{DarkSkyAPI}{META} = $META; + +use version 0.77; our $VERSION = $META->{version}; + +# use Data::Dumper; # try to use JSON::MaybeXS wrapper # for chance of better performance + open code @@ -50,15 +52,11 @@ eval { require JSON::MaybeXS; import JSON::MaybeXS qw( decode_json encode_json ); 1; -}; - -if ($@) { - $@ = undef; +} or do { # try to use JSON wrapper # for chance of better performance eval { - # JSON preference order local $ENV{PERL_JSON_BACKEND} = 'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP' @@ -67,10 +65,7 @@ if ($@) { require JSON; import JSON qw( decode_json encode_json ); 1; - }; - - if ($@) { - $@ = undef; + } or do { # In rare cases, Cpanel::JSON::XS may # be installed but JSON|JSON::MaybeXS not ... @@ -78,10 +73,7 @@ if ($@) { require Cpanel::JSON::XS; import Cpanel::JSON::XS qw(decode_json encode_json); 1; - }; - - if ($@) { - $@ = undef; + } or do { # In rare cases, JSON::XS may # be installed but JSON not ... @@ -89,10 +81,7 @@ if ($@) { require JSON::XS; import JSON::XS qw(decode_json encode_json); 1; - }; - - if ($@) { - $@ = undef; + } or do { # Fallback to built-in JSON which SHOULD # be available since 5.014 ... @@ -100,30 +89,32 @@ if ($@) { require JSON::PP; import JSON::PP qw(decode_json encode_json); 1; - }; - - if ($@) { - $@ = undef; + } or do { # Fallback to JSON::backportPP in really rare cases require JSON::backportPP; import JSON::backportPP qw(decode_json encode_json); 1; - } - } - } - } -} + }; + }; + }; + }; +}; my $missingModul = ''; -eval "use Encode qw(encode_utf8);1" or $missingModul .= "Encode "; +## no critic (Conditional "use" statement. Use "require" to conditionally include a module (Modules::ProhibitConditionalUseStatements)) +eval { use Encode qw /encode_utf8/; 1 } or $missingModul .= 'Encode '; + +eval { use Readonly; 1 } + or $missingModul .= 'Readonly '; # apt install libreadonly-perl +## use critic # use Data::Dumper; # for Debug only ## API URL -use constant DEMODATA => +Readonly my $DEMODATA => '{"latitude":50.112,"longitude":8.686,"timezone":"Europe/Berlin","currently":{"time":1551214558,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":9.65,"apparentTemperature":9.65,"dewPoint":1.39,"humidity":0.56,"pressure":1032.69,"windSpeed":0.41,"windGust":1.35,"windBearing":84,"cloudCover":0,"uvIndex":0,"visibility":10.01,"ozone":276.41},"hourly":{"summary":"Leicht bewölkt am heute Nacht.","icon":"partly-cloudy-night","data":[{"time":1551211200,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":10.59,"apparentTemperature":10.59,"dewPoint":1.84,"humidity":0.55,"pressure":1032.7,"windSpeed":0.28,"windGust":1.15,"windBearing":89,"cloudCover":0,"uvIndex":0,"visibility":10.01,"ozone":277},{"time":1551214800,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":9.58,"apparentTemperature":9.58,"dewPoint":1.36,"humidity":0.56,"pressure":1032.69,"windSpeed":0.42,"windGust":1.37,"windBearing":83,"cloudCover":0,"uvIndex":0,"visibility":10.01,"ozone":276.37},{"time":1551218400,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":8.61,"apparentTemperature":8.61,"dewPoint":0.73,"humidity":0.58,"pressure":1032.63,"windSpeed":0.5,"windGust":1.47,"windBearing":72,"cloudCover":0,"uvIndex":0,"visibility":11.47,"ozone":275.56},{"time":1551222000,"summary":"Leicht bewölkt","icon":"partly-cloudy-night","precipIntensity":0,"precipProbability":0,"temperature":8.06,"apparentTemperature":8.06,"dewPoint":-0.45,"humidity":0.55,"pressure":1032.55,"windSpeed":0.86,"windGust":1.5,"windBearing":40,"cloudCover":0.26,"uvIndex":0,"visibility":16.09,"ozone":274.76},{"time":1551225600,"summary":"Leicht bewölkt","icon":"partly-cloudy-night","precipIntensity":0,"precipProbability":0,"temperature":7.48,"apparentTemperature":7.48,"dewPoint":-1.38,"humidity":0.53,"pressure":1032.4,"windSpeed":1.14,"windGust":1.49,"windBearing":33,"cloudCover":0.42,"uvIndex":0,"visibility":16.09,"ozone":274.13},{"time":1551229200,"summary":"Leicht bewölkt","icon":"partly-cloudy-night","precipIntensity":0,"precipProbability":0,"temperature":6.62,"apparentTemperature":6.62,"dewPoint":-1.89,"humidity":0.54,"pressure":1032.12,"windSpeed":1.11,"windGust":1.43,"windBearing":38,"cloudCover":0.36,"uvIndex":0,"visibility":16.09,"ozone":273.77},{"time":1551232800,"summary":"Leicht bewölkt","icon":"partly-cloudy-night","precipIntensity":0,"precipProbability":0,"temperature":5.73,"apparentTemperature":5.73,"dewPoint":-2.39,"humidity":0.56,"pressure":1031.83,"windSpeed":1.07,"windGust":1.34,"windBearing":46,"cloudCover":0.29,"uvIndex":0,"visibility":16.09,"ozone":273.55},{"time":1551236400,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":4.91,"apparentTemperature":4.91,"dewPoint":-2.81,"humidity":0.57,"pressure":1031.49,"windSpeed":1.03,"windGust":1.23,"windBearing":54,"cloudCover":0.23,"uvIndex":0,"visibility":16.09,"ozone":273.44},{"time":1551240000,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":4.02,"apparentTemperature":4.02,"dewPoint":-3.26,"humidity":0.59,"pressure":1031.18,"windSpeed":0.99,"windGust":1.15,"windBearing":63,"cloudCover":0.21,"uvIndex":0,"visibility":16.09,"ozone":273.43},{"time":1551243600,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":3.26,"apparentTemperature":3.26,"dewPoint":-3.61,"humidity":0.61,"pressure":1030.85,"windSpeed":0.96,"windGust":1.08,"windBearing":73,"cloudCover":0.22,"uvIndex":0,"visibility":16.09,"ozone":273.5},{"time":1551247200,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":3.19,"apparentTemperature":3.19,"dewPoint":-3.51,"humidity":0.61,"pressure":1030.54,"windSpeed":0.92,"windGust":1.01,"windBearing":83,"cloudCover":0.22,"uvIndex":0,"visibility":16.09,"ozone":273.65},{"time":1551250800,"summary":"Heiter","icon":"clear-day","precipIntensity":0,"precipProbability":0,"temperature":4.28,"apparentTemperature":4.28,"dewPoint":-2.62,"humidity":0.61,"pressure":1030.25,"windSpeed":0.83,"windGust":0.88,"windBearing":93,"cloudCover":0.2,"uvIndex":0,"visibility":16.09,"ozone":273.79},{"time":1551254400,"summary":"Heiter","icon":"clear-day","precipIntensity":0,"precipProbability":0,"temperature":6.29,"apparentTemperature":6.29,"dewPoint":-1.28,"humidity":0.58,"pressure":1029.92,"windSpeed":0.72,"windGust":0.78,"windBearing":105,"cloudCover":0.19,"uvIndex":1,"visibility":16.09,"ozone":273.91},{"time":1551258000,"summary":"Heiter","icon":"clear-day","precipIntensity":0,"precipProbability":0,"temperature":8.45,"apparentTemperature":8.45,"dewPoint":-0.11,"humidity":0.55,"pressure":1029.54,"windSpeed":0.68,"windGust":0.77,"windBearing":116,"cloudCover":0.16,"uvIndex":1,"visibility":16.09,"ozone":274.33},{"time":1551261600,"summary":"Heiter","icon":"clear-day","precipIntensity":0,"precipProbability":0,"temperature":10.79,"apparentTemperature":10.79,"dewPoint":0.73,"humidity":0.5,"pressure":1028.98,"windSpeed":0.81,"windGust":0.9,"windBearing":125,"cloudCover":0.14,"uvIndex":2,"visibility":16.09,"ozone":275.21},{"time":1551265200,"summary":"Heiter","icon":"clear-day","precipIntensity":0,"precipProbability":0,"temperature":13.36,"apparentTemperature":13.36,"dewPoint":1.44,"humidity":0.44,"pressure":1028.33,"windSpeed":1.06,"windGust":1.12,"windBearing":131,"cloudCover":0.11,"uvIndex":3,"visibility":16.09,"ozone":276.39},{"time":1551268800,"summary":"Heiter","icon":"clear-day","precipIntensity":0,"precipProbability":0,"temperature":15.58,"apparentTemperature":15.58,"dewPoint":1.98,"humidity":0.4,"pressure":1027.59,"windSpeed":1.26,"windGust":1.28,"windBearing":140,"cloudCover":0.08,"uvIndex":3,"visibility":16.09,"ozone":277.31},{"time":1551272400,"summary":"Heiter","icon":"clear-day","precipIntensity":0,"precipProbability":0,"temperature":17.3,"apparentTemperature":17.3,"dewPoint":2.23,"humidity":0.36,"pressure":1026.71,"windSpeed":1.3,"windGust":1.31,"windBearing":154,"cloudCover":0.05,"uvIndex":2,"visibility":16.09,"ozone":277.73},{"time":1551276000,"summary":"Heiter","icon":"clear-day","precipIntensity":0,"precipProbability":0,"temperature":18.44,"apparentTemperature":18.44,"dewPoint":2.27,"humidity":0.34,"pressure":1025.7,"windSpeed":1.28,"windGust":1.28,"windBearing":172,"cloudCover":0.02,"uvIndex":2,"visibility":16.09,"ozone":277.96},{"time":1551279600,"summary":"Heiter","icon":"clear-day","precipIntensity":0,"precipProbability":0,"temperature":18.49,"apparentTemperature":18.49,"dewPoint":2.21,"humidity":0.34,"pressure":1024.91,"windSpeed":1.24,"windGust":1.27,"windBearing":184,"cloudCover":0,"uvIndex":1,"visibility":16.09,"ozone":278.36},{"time":1551283200,"summary":"Heiter","icon":"clear-day","precipIntensity":0,"precipProbability":0,"temperature":17.44,"apparentTemperature":17.44,"dewPoint":2.05,"humidity":0.36,"pressure":1024.53,"windSpeed":1.18,"windGust":1.25,"windBearing":191,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":278.97},{"time":1551286800,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":14.98,"apparentTemperature":14.98,"dewPoint":1.79,"humidity":0.41,"pressure":1024.34,"windSpeed":1.12,"windGust":1.27,"windBearing":194,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":279.76},{"time":1551290400,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":12.61,"apparentTemperature":12.61,"dewPoint":1.52,"humidity":0.47,"pressure":1024.09,"windSpeed":1.11,"windGust":1.39,"windBearing":195,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":280.33},{"time":1551294000,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":10.99,"apparentTemperature":10.99,"dewPoint":1.18,"humidity":0.51,"pressure":1023.68,"windSpeed":1.2,"windGust":1.51,"windBearing":194,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":280.69},{"time":1551297600,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":9.98,"apparentTemperature":9.98,"dewPoint":0.83,"humidity":0.53,"pressure":1023.18,"windSpeed":1.34,"windGust":1.64,"windBearing":191,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":280.94},{"time":1551301200,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":9.18,"apparentTemperature":8.72,"dewPoint":0.53,"humidity":0.55,"pressure":1022.72,"windSpeed":1.49,"windGust":1.77,"windBearing":190,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":281.11},{"time":1551304800,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":8.5,"apparentTemperature":7.72,"dewPoint":0.36,"humidity":0.57,"pressure":1022.32,"windSpeed":1.71,"windGust":1.9,"windBearing":194,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":281.02},{"time":1551308400,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":7.97,"apparentTemperature":6.87,"dewPoint":0.24,"humidity":0.58,"pressure":1021.93,"windSpeed":1.94,"windGust":2.05,"windBearing":200,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":280.74},{"time":1551312000,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":7.47,"apparentTemperature":6.15,"dewPoint":0.17,"humidity":0.6,"pressure":1021.49,"windSpeed":2.11,"windGust":2.19,"windBearing":206,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":280.75},{"time":1551315600,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":6.82,"apparentTemperature":5.37,"dewPoint":0.09,"humidity":0.62,"pressure":1020.9,"windSpeed":2.12,"windGust":2.28,"windBearing":208,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":281.23},{"time":1551319200,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":6.09,"apparentTemperature":4.58,"dewPoint":0.06,"humidity":0.65,"pressure":1020.22,"windSpeed":2.06,"windGust":2.36,"windBearing":210,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":281.97},{"time":1551322800,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":5.44,"apparentTemperature":3.82,"dewPoint":0.01,"humidity":0.68,"pressure":1019.67,"windSpeed":2.06,"windGust":2.58,"windBearing":211,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":282.78},{"time":1551326400,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":4.78,"apparentTemperature":2.93,"dewPoint":-0.17,"humidity":0.7,"pressure":1019.37,"windSpeed":2.18,"windGust":3.01,"windBearing":214,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":283.53},{"time":1551330000,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":4.24,"apparentTemperature":2.13,"dewPoint":-0.34,"humidity":0.72,"pressure":1019.2,"windSpeed":2.36,"windGust":3.59,"windBearing":216,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":284.44},{"time":1551333600,"summary":"Heiter","icon":"clear-night","precipIntensity":0,"precipProbability":0,"temperature":4.24,"apparentTemperature":1.93,"dewPoint":-0.18,"humidity":0.73,"pressure":1019.01,"windSpeed":2.58,"windGust":4.25,"windBearing":218,"cloudCover":0,"uvIndex":0,"visibility":16.09,"ozone":285.68},{"time":1551337200,"summary":"Heiter","icon":"clear-day","precipIntensity":0,"precipProbability":0,"temperature":5.15,"apparentTemperature":2.83,"dewPoint":0.57,"humidity":0.72,"pressure":1018.8,"windSpeed":2.82,"windGust":5.04,"windBearing":219,"cloudCover":0.2,"uvIndex":0,"visibility":16.09,"ozone":287.28},{"time":1551340800,"summary":"Leicht bewölkt","icon":"partly-cloudy-day","precipIntensity":0,"precipProbability":0,"temperature":6.58,"apparentTemperature":4.36,"dewPoint":1.66,"humidity":0.71,"pressure":1018.55,"windSpeed":3.07,"windGust":5.91,"windBearing":219,"cloudCover":0.47,"uvIndex":1,"visibility":16.09,"ozone":289.23},{"time":1551344400,"summary":"Überwiegend bewölkt","icon":"partly-cloudy-day","precipIntensity":0,"precipProbability":0,"temperature":8.36,"apparentTemperature":6.28,"dewPoint":2.82,"humidity":0.68,"pressure":1018.24,"windSpeed":3.44,"windGust":6.82,"windBearing":222,"cloudCover":0.66,"uvIndex":1,"visibility":16.09,"ozone":291.68},{"time":1551348000,"summary":"Überwiegend bewölkt","icon":"partly-cloudy-day","precipIntensity":0,"precipProbability":0,"temperature":10.57,"apparentTemperature":10.57,"dewPoint":4.07,"humidity":0.64,"pressure":1017.81,"windSpeed":4.01,"windGust":7.74,"windBearing":226,"cloudCover":0.7,"uvIndex":2,"visibility":16.09,"ozone":294.4},{"time":1551351600,"summary":"Überwiegend bewölkt","icon":"partly-cloudy-day","precipIntensity":0,"precipProbability":0,"temperature":13.39,"apparentTemperature":13.39,"dewPoint":5.36,"humidity":0.58,"pressure":1017.26,"windSpeed":4.7,"windGust":8.69,"windBearing":231,"cloudCover":0.66,"uvIndex":2,"visibility":16.09,"ozone":297.66},{"time":1551355200,"summary":"Leicht bewölkt","icon":"partly-cloudy-day","precipIntensity":0,"precipProbability":0,"temperature":15.38,"apparentTemperature":15.38,"dewPoint":6.21,"humidity":0.54,"pressure":1016.79,"windSpeed":5.32,"windGust":9.65,"windBearing":237,"cloudCover":0.58,"uvIndex":2,"visibility":16.09,"ozone":302.31},{"time":1551358800,"summary":"Leicht bewölkt","icon":"partly-cloudy-day","precipIntensity":0,"precipProbability":0,"temperature":16.34,"apparentTemperature":16.34,"dewPoint":6.33,"humidity":0.52,"pressure":1016.42,"windSpeed":5.9,"windGust":10.76,"windBearing":244,"cloudCover":0.47,"uvIndex":2,"visibility":16.09,"ozone":309.63},{"time":1551362400,"summary":"Leicht bewölkt","icon":"partly-cloudy-day","precipIntensity":0,"precipProbability":0,"temperature":16.41,"apparentTemperature":16.41,"dewPoint":6.01,"humidity":0.5,"pressure":1016.08,"windSpeed":6.41,"windGust":11.88,"windBearing":253,"cloudCover":0.32,"uvIndex":1,"visibility":16.09,"ozone":318.45},{"time":1551366000,"summary":"Heiter","icon":"clear-day","precipIntensity":0,"precipProbability":0,"temperature":16.25,"apparentTemperature":16.25,"dewPoint":5.68,"humidity":0.5,"pressure":1015.79,"windSpeed":6.54,"windGust":12.43,"windBearing":257,"cloudCover":0.23,"uvIndex":1,"visibility":16.09,"ozone":325.57},{"time":1551369600,"summary":"Heiter","icon":"clear-day","precipIntensity":0,"precipProbability":0,"temperature":15.56,"apparentTemperature":15.56,"dewPoint":5.41,"humidity":0.51,"pressure":1015.57,"windSpeed":5.99,"windGust":11.93,"windBearing":252,"cloudCover":0.24,"uvIndex":0,"visibility":16.09,"ozone":329.15},{"time":1551373200,"summary":"Leicht bewölkt","icon":"partly-cloudy-night","precipIntensity":0,"precipProbability":0,"temperature":14.88,"apparentTemperature":14.88,"dewPoint":5.13,"humidity":0.52,"pressure":1015.39,"windSpeed":5.11,"windGust":10.86,"windBearing":244,"cloudCover":0.32,"uvIndex":0,"visibility":16.09,"ozone":331.01},{"time":1551376800,"summary":"Leicht bewölkt","icon":"partly-cloudy-night","precipIntensity":0.0102,"precipProbability":0.02,"precipType":"rain","temperature":14.14,"apparentTemperature":14.14,"dewPoint":5.03,"humidity":0.54,"pressure":1015.15,"windSpeed":4.55,"windGust":10.16,"windBearing":239,"cloudCover":0.42,"uvIndex":0,"visibility":16.09,"ozone":334.57},{"time":1551380400,"summary":"Leicht bewölkt","icon":"partly-cloudy-night","precipIntensity":0.0406,"precipProbability":0.06,"precipType":"rain","temperature":13.44,"apparentTemperature":13.44,"dewPoint":5.32,"humidity":0.58,"pressure":1014.77,"windSpeed":4.64,"windGust":10.41,"windBearing":240,"cloudCover":0.53,"uvIndex":0,"visibility":16.09,"ozone":342.62},{"time":1551384000,"summary":"Überwiegend bewölkt","icon":"partly-cloudy-night","precipIntensity":0.1346,"precipProbability":0.14,"precipType":"rain","temperature":12.71,"apparentTemperature":12.71,"dewPoint":5.82,"humidity":0.63,"pressure":1014.32,"windSpeed":4.96,"windGust":11.04,"windBearing":244,"cloudCover":0.67,"uvIndex":0,"visibility":13.53,"ozone":352.36}]},"daily":{"summary":"Leichter Regen von Freitag bis Montag mit fallender Temperatur von 11°C am nächsten Dienstag.","icon":"rain","data":[{"time":1551135600,"summary":"Leicht bewölkt in der Nacht.","icon":"partly-cloudy-night","sunriseTime":1551161800,"sunsetTime":1551200555,"moonPhase":0.75,"precipIntensity":0,"precipIntensityMax":0.0051,"precipIntensityMaxTime":1551186000,"precipProbability":0,"temperatureHigh":21.42,"temperatureHighTime":1551193200,"temperatureLow":3.19,"temperatureLowTime":1551247200,"apparentTemperatureHigh":21.42,"apparentTemperatureHighTime":1551193200,"apparentTemperatureLow":3.19,"apparentTemperatureLowTime":1551247200,"dewPoint":0.24,"humidity":0.55,"pressure":1034.3,"windSpeed":0.51,"windGust":2.01,"windGustTime":1551189600,"windBearing":16,"cloudCover":0.02,"uvIndex":3,"uvIndexTime":1551182400,"visibility":10.2,"ozone":285.07,"temperatureMin":1.38,"temperatureMinTime":1551153600,"temperatureMax":21.42,"temperatureMaxTime":1551193200,"apparentTemperatureMin":1.38,"apparentTemperatureMinTime":1551153600,"apparentTemperatureMax":21.42,"apparentTemperatureMaxTime":1551193200},{"time":1551222000,"summary":"Den ganzen Tag lang heiter.","icon":"clear-day","sunriseTime":1551248079,"sunsetTime":1551287056,"moonPhase":0.78,"precipIntensity":0.0025,"precipIntensityMax":0.0051,"precipIntensityMaxTime":1551229200,"precipProbability":0.02,"precipType":"rain","temperatureHigh":18.49,"temperatureHighTime":1551279600,"temperatureLow":4.24,"temperatureLowTime":1551330000,"apparentTemperatureHigh":18.49,"apparentTemperatureHighTime":1551279600,"apparentTemperatureLow":1.93,"apparentTemperatureLowTime":1551333600,"dewPoint":-0.17,"humidity":0.5,"pressure":1027.91,"windSpeed":0.59,"windGust":1.9,"windGustTime":1551304800,"windBearing":139,"cloudCover":0.13,"uvIndex":3,"uvIndexTime":1551265200,"visibility":16.09,"ozone":276.58,"temperatureMin":3.19,"temperatureMinTime":1551247200,"temperatureMax":18.49,"temperatureMaxTime":1551279600,"apparentTemperatureMin":3.19,"apparentTemperatureMinTime":1551247200,"apparentTemperatureMax":18.49,"apparentTemperatureMaxTime":1551279600},{"time":1551308400,"summary":"Den ganzen Tag lang überwiegend bewölkt.","icon":"partly-cloudy-night","sunriseTime":1551334356,"sunsetTime":1551373557,"moonPhase":0.81,"precipIntensity":0.033,"precipIntensityMax":0.3175,"precipIntensityMaxTime":1551391200,"precipProbability":0.38,"precipType":"rain","temperatureHigh":16.41,"temperatureHighTime":1551362400,"temperatureLow":8.43,"temperatureLowTime":1551423600,"apparentTemperatureHigh":16.41,"apparentTemperatureHighTime":1551362400,"apparentTemperatureLow":6.74,"apparentTemperatureLowTime":1551423600,"dewPoint":3.24,"humidity":0.62,"pressure":1017.5,"windSpeed":3.8,"windGust":12.43,"windGustTime":1551366000,"windBearing":235,"cloudCover":0.34,"uvIndex":2,"uvIndexTime":1551348000,"visibility":15.27,"ozone":307.52,"temperatureMin":4.24,"temperatureMinTime":1551330000,"temperatureMax":16.41,"temperatureMaxTime":1551362400,"apparentTemperatureMin":1.93,"apparentTemperatureMinTime":1551333600,"apparentTemperatureMax":16.41,"apparentTemperatureMaxTime":1551362400},{"time":1551394800,"summary":"Überwiegend bewölkt bis abends.","icon":"partly-cloudy-day","sunriseTime":1551420633,"sunsetTime":1551460057,"moonPhase":0.84,"precipIntensity":0.188,"precipIntensityMax":0.8179,"precipIntensityMaxTime":1551409200,"precipProbability":0.88,"precipType":"rain","temperatureHigh":14.23,"temperatureHighTime":1551452400,"temperatureLow":5.98,"temperatureLowTime":1551506400,"apparentTemperatureHigh":14.23,"apparentTemperatureHighTime":1551452400,"apparentTemperatureLow":4.09,"apparentTemperatureLowTime":1551510000,"dewPoint":5.37,"humidity":0.71,"pressure":1014.79,"windSpeed":2.63,"windGust":8.53,"windGustTime":1551394800,"windBearing":284,"cloudCover":0.62,"uvIndex":2,"uvIndexTime":1551438000,"visibility":13.52,"ozone":343.54,"temperatureMin":8.43,"temperatureMinTime":1551423600,"temperatureMax":14.23,"temperatureMaxTime":1551452400,"apparentTemperatureMin":6.74,"apparentTemperatureMinTime":1551423600,"apparentTemperatureMax":14.23,"apparentTemperatureMaxTime":1551452400},{"time":1551481200,"summary":"Den ganzen Tag lang überwiegend bewölkt.","icon":"partly-cloudy-day","sunriseTime":1551506909,"sunsetTime":1551546556,"moonPhase":0.87,"precipIntensity":0.0381,"precipIntensityMax":0.2667,"precipIntensityMaxTime":1551549600,"precipProbability":0.29,"precipType":"rain","temperatureHigh":13.86,"temperatureHighTime":1551542400,"temperatureLow":9.92,"temperatureLowTime":1551596400,"apparentTemperatureHigh":13.86,"apparentTemperatureHighTime":1551542400,"apparentTemperatureLow":6.67,"apparentTemperatureLowTime":1551596400,"dewPoint":4.98,"humidity":0.73,"pressure":1017.33,"windSpeed":3.4,"windGust":11.14,"windGustTime":1551564000,"windBearing":223,"cloudCover":0.7,"uvIndex":2,"uvIndexTime":1551520800,"visibility":16.09,"ozone":338.68,"temperatureMin":5.98,"temperatureMinTime":1551506400,"temperatureMax":13.86,"temperatureMaxTime":1551542400,"apparentTemperatureMin":4.09,"apparentTemperatureMinTime":1551510000,"apparentTemperatureMax":13.86,"apparentTemperatureMaxTime":1551542400},{"time":1551567600,"summary":"Den ganzen Tag lang leichter Wind und Nacht leichter Regen.","icon":"rain","sunriseTime":1551593184,"sunsetTime":1551633056,"moonPhase":0.9,"precipIntensity":0.3886,"precipIntensityMax":0.8484,"precipIntensityMaxTime":1551650400,"precipProbability":1,"precipType":"rain","temperatureHigh":12.89,"temperatureHighTime":1551618000,"temperatureLow":9.58,"temperatureLowTime":1551664800,"apparentTemperatureHigh":12.89,"apparentTemperatureHighTime":1551618000,"apparentTemperatureLow":7.11,"apparentTemperatureLowTime":1551661200,"dewPoint":6.11,"humidity":0.71,"pressure":1010.92,"windSpeed":6.64,"windGust":18.8,"windGustTime":1551650400,"windBearing":229,"cloudCover":0.94,"uvIndex":2,"uvIndexTime":1551607200,"visibility":10.01,"ozone":332.44,"temperatureMin":9.78,"temperatureMinTime":1551600000,"temperatureMax":12.89,"temperatureMaxTime":1551618000,"apparentTemperatureMin":6.37,"apparentTemperatureMinTime":1551600000,"apparentTemperatureMax":12.89,"apparentTemperatureMaxTime":1551618000},{"time":1551654000,"summary":"Den ganzen Tag lang überwiegend bewölkt sowie leichter Wind bis Nachmittag.","icon":"wind","sunriseTime":1551679459,"sunsetTime":1551719555,"moonPhase":0.93,"precipIntensity":0.3023,"precipIntensityMax":0.8128,"precipIntensityMaxTime":1551654000,"precipProbability":0.92,"precipType":"rain","temperatureHigh":14.28,"temperatureHighTime":1551711600,"temperatureLow":7.89,"temperatureLowTime":1551769200,"apparentTemperatureHigh":14.28,"apparentTemperatureHighTime":1551711600,"apparentTemperatureLow":4.87,"apparentTemperatureLowTime":1551769200,"dewPoint":5.51,"humidity":0.67,"pressure":1003.91,"windSpeed":6.15,"windGust":20.06,"windGustTime":1551657600,"windBearing":230,"cloudCover":0.91,"uvIndex":2,"uvIndexTime":1551693600,"visibility":12.46,"ozone":369.63,"temperatureMin":9.58,"temperatureMinTime":1551664800,"temperatureMax":14.28,"temperatureMaxTime":1551711600,"apparentTemperatureMin":7.11,"apparentTemperatureMinTime":1551661200,"apparentTemperatureMax":14.28,"apparentTemperatureMaxTime":1551711600},{"time":1551740400,"summary":"Nachmittags Nebel.","icon":"fog","sunriseTime":1551765733,"sunsetTime":1551806054,"moonPhase":0.96,"precipIntensity":0.2083,"precipIntensityMax":0.4597,"precipIntensityMaxTime":1551780000,"precipProbability":0.72,"precipType":"rain","temperatureHigh":11.26,"temperatureHighTime":1551794400,"temperatureLow":5.99,"temperatureLowTime":1551855600,"apparentTemperatureHigh":11.26,"apparentTemperatureHighTime":1551794400,"apparentTemperatureLow":2.28,"apparentTemperatureLowTime":1551855600,"dewPoint":3.41,"humidity":0.65,"pressure":1001.87,"windSpeed":5.37,"windGust":16.22,"windGustTime":1551754800,"windBearing":230,"cloudCover":0.79,"uvIndex":1,"uvIndexTime":1551776400,"visibility":8.96,"ozone":442.27,"temperatureMin":7.89,"temperatureMinTime":1551769200,"temperatureMax":11.26,"temperatureMaxTime":1551794400,"apparentTemperatureMin":4.82,"apparentTemperatureMinTime":1551772800,"apparentTemperatureMax":11.26,"apparentTemperatureMaxTime":1551794400}]},"flags":{"sources":["meteoalarm","cmc","gfs","icon","isd","madis"],"meteoalarm-license":"Based on data from EUMETNET - MeteoAlarm [https://www.meteoalarm.eu/]. Time delays between this website and the MeteoAlarm website are possible; for the most up to date information about alert levels as published by the participating National Meteorological Services please use the MeteoAlarm website.","nearest-station":8.711,"units":"si"},"offset":1}'; -use constant URL => 'https://api.darksky.net/forecast/'; +Readonly my $URL => 'https://api.darksky.net/forecast/'; my %codes = ( 'clear-day' => 32, @@ -141,10 +132,11 @@ my %codes = ( 'tornado' => 0, ); +### begin public function sub new { ### geliefert wird ein Hash my ( $class, $argsRef ) = @_; - my $apioptions = parseApiOptions( $argsRef->{apioptions} ); + my $apioptions = _parseApiOptions( $argsRef->{apioptions} ); my $self = { devName => $argsRef->{devName}, @@ -175,23 +167,6 @@ sub new { return $self; } -sub parseApiOptions($) { - my $apioptions = shift; - - my @params; - my %h; - - @params = split( ',', $apioptions ); - while (@params) { - my $param = shift(@params); - next if ( $param eq '' ); - my ( $key, $value ) = split( ':', $param, 2 ); - $h{$key} = $value; - } - - return \%h; -} - sub setAlerts { my $self = shift; my $alerts = shift // 0; @@ -243,7 +218,29 @@ sub getWeather { return $self->{cached}; } -sub _RetrieveDataFromDarkSky($) { +### begin privat function +sub _parseApiOptions { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $apioptions = shift; + + my @params; + my %h; + + @params = split( ',', $apioptions ); + while (@params) { + my $param = shift(@params); + next if ( $param eq '' ); + my ( $key, $value ) = split( ':', $param, 2 ); + $h{$key} = $value; + } + + return \%h; +} + +sub _RetrieveDataFromDarkSky { + return 0 unless ( __PACKAGE__ eq caller(0) ); + my $self = shift; # retrieve data from cache @@ -291,7 +288,7 @@ sub _RetrieveDataFromDarkSky($) { if ( $self->{extend} ne 'none' ); $paramRef->{url} = - URL + $URL . $self->{key} . '/' . $self->{lat} . ',' . $self->{long} @@ -300,19 +297,26 @@ sub _RetrieveDataFromDarkSky($) { . $options; if ( lc( $self->{key} ) eq 'demo' ) { - _RetrieveDataFinished( $paramRef, undef, DEMODATA ); + _RetrieveDataFinished( $paramRef, undef, $DEMODATA ); } - else { main::HttpUtils_NonblockingGet($paramRef); } + else { ::HttpUtils_NonblockingGet($paramRef); } } + + return; } -sub _RetrieveDataFinished($$$) { - my ( $paramRef, $err, $response ) = @_; - my $self = $paramRef->{self}; +sub _RetrieveDataFinished { + return 0 unless ( 'main' eq caller(0) ); + + my $paramRef = shift; + my $err = shift; + my $response = shift; + my $self = $paramRef->{self}; if ( !$err ) { $self->{cached}->{status} = 'ok'; - $self->{cached}->{validity} = 'up-to-date', $self->{fetchTime} = time(); + $self->{cached}->{validity} = 'up-to-date'; + $self->{fetchTime} = time(); _ProcessingRetrieveData( $self, $response ); } else { @@ -320,16 +324,21 @@ sub _RetrieveDataFinished($$$) { _ErrorHandling( $self, $err ); _ProcessingRetrieveData( $self, $response ); } + + return; } -sub _ProcessingRetrieveData($$) { - my ( $self, $response ) = @_; +sub _ProcessingRetrieveData { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $response = shift; if ( $self->{cached}->{status} eq 'ok' and defined($response) and $response ) { - if ( $response =~ m/^{.*}$/ ) { + if ( $response =~ m/^{.*}$/x ) { my $data = eval { decode_json($response) }; if ($@) { @@ -345,417 +354,7 @@ sub _ProcessingRetrieveData($$) { 'Code: ' . $data->{code} . ' Error: ' . $data->{error} ); } else { - # print Dumper $data; ## für Debugging - - $self->{cached}->{current_date_time} = - strftimeWrapper( "%a, %e %b %Y %H:%M", - localtime( $self->{fetchTime} ) ); - $self->{cached}->{timezone} = $data->{timezone}; - $self->{cached}->{license}{text} = - $data->{flags}->{'meteoalarm-license'}; - $self->{cached}->{current} = { - 'temperature' => int( - sprintf( "%.1f", $data->{currently}->{temperature} ) + - 0.5 - ), - 'temp_c' => int( - sprintf( "%.1f", $data->{currently}->{temperature} ) + - 0.5 - ), - 'dewPoint' => int( - sprintf( "%.1f", $data->{currently}->{dewPoint} ) + 0.5 - ), - 'humidity' => $data->{currently}->{humidity} * 100, - 'condition' => encode_utf8( $data->{currently}->{summary} ), - 'pressure' => int( - sprintf( "%.1f", $data->{currently}->{pressure} ) + 0.5 - ), - 'wind' => int( - sprintf( "%.1f", - ( $data->{currently}->{windSpeed} * 3.6 ) ) + 0.5 - ), - 'wind_speed' => int( - sprintf( "%.1f", - ( $data->{currently}->{windSpeed} * 3.6 ) ) + 0.5 - ), - 'wind_direction' => $data->{currently}->{windBearing}, - 'windGust' => int( - sprintf( "%.1f", - ( $data->{currently}->{windGust} * 3.6 ) ) + 0.5 - ), - 'cloudCover' => $data->{currently}->{cloudCover} * 100, - 'uvIndex' => $data->{currently}->{uvIndex}, - 'visibility' => int( - sprintf( "%.1f", $data->{currently}->{visibility} ) + - 0.5 - ), - 'ozone' => $data->{currently}->{ozone}, - 'code' => $codes{ $data->{currently}->{icon} }, - 'iconAPI' => $data->{currently}->{icon}, - 'pubDate' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( $data->{currently}->{'time'} ) - ), - 'precipProbability' => - $data->{currently}->{precipProbability} * 100, - 'apparentTemperature' => int( - sprintf( "%.1f", - $data->{currently}->{apparentTemperature} ) + 0.5 - ), - 'precipIntensity' => $data->{currently}->{precipIntensity}, - }; - - if ( ref( $data->{daily}->{data} ) eq "ARRAY" - and scalar( @{ $data->{daily}->{data} } ) > 0 ) - { - ### löschen des alten Datensatzes - delete $self->{cached}->{forecast}; - - my $i = 0; - foreach ( @{ $data->{daily}->{data} } ) { - push( - @{ $self->{cached}->{forecast}->{daily} }, - { - 'pubDate' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - $data->{daily}->{data}->[$i]->{'time'} - ) - ), - 'day_of_week' => strftime( - "%a", - localtime( - $data->{daily}->{data}->[$i]->{'time'} - ) - ), - 'low_c' => int( - sprintf( "%.1f", - $data->{daily}->{data}->[$i] - ->{temperatureLow} ) + 0.5 - ), - 'high_c' => int( - sprintf( "%.1f", - $data->{daily}->{data}->[$i] - ->{temperatureHigh} ) + 0.5 - ), - 'tempLow' => int( - sprintf( "%.1f", - $data->{daily}->{data}->[$i] - ->{temperatureLow} ) + 0.5 - ), - 'tempLowTime' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - $data->{daily}->{data}->[$i] - ->{temperatureLowTime} - ) - ), - 'tempHigh' => int( - sprintf( "%.1f", - $data->{daily}->{data}->[$i] - ->{temperatureHigh} ) + 0.5 - ), - 'tempHighTime' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - $data->{daily}->{data}->[$i] - ->{temperatureHighTime} - ) - ), - 'apparentTempLow' => int( - sprintf( "%.1f", - $data->{daily}->{data}->[$i] - ->{apparentTemperatureLow} ) + 0.5 - ), - 'apparentTempLowTime' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - $data->{daily}->{data}->[$i] - ->{apparentTemperatureLowTime} - ) - ), - 'apparentTempHigh' => int( - sprintf( "%.1f", - $data->{daily}->{data}->[$i] - ->{apparentTemperatureHigh} ) + 0.5 - ), - 'apparentTempHighTime' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - $data->{daily}->{data}->[$i] - ->{apparentTemperatureHighTime} - ) - ), - 'code' => $codes{ - $data->{daily}->{data}->[$i]->{icon} - }, - 'iconAPI' => - $data->{daily}->{data}->[$i]->{icon}, - 'condition' => encode_utf8( - $data->{daily}->{data}->[$i]->{summary} - ), - 'ozone' => - $data->{daily}->{data}->[$i]->{ozone}, - 'uvIndex' => - $data->{daily}->{data}->[$i]->{uvIndex}, - 'uvIndexTime' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - $data->{daily}->{data}->[$i] - ->{uvIndexTime} - ) - ), - 'dewPoint' => int( - sprintf( "%.1f", - $data->{daily}->{data}->[$i]->{dewPoint} - ) + 0.5 - ), - 'humidity' => - $data->{daily}->{data}->[$i]->{humidity} * - 100, - 'cloudCover' => - $data->{daily}->{data}->[$i]->{cloudCover} * - 100, - 'wind_direction' => - $data->{daily}->{data}->[$i]->{windBearing}, - 'wind' => int( - sprintf( - "%.1f", - ( - $data->{daily}->{data}->[$i] - ->{windSpeed} * 3.6 - ) - ) + 0.5 - ), - 'wind_speed' => int( - sprintf( - "%.1f", - ( - $data->{daily}->{data}->[$i] - ->{windSpeed} * 3.6 - ) - ) + 0.5 - ), - 'windGust' => int( - sprintf( - "%.1f", - ( - $data->{daily}->{data}->[$i] - ->{windGust} * 3.6 - ) - ) + 0.5 - ), - 'windGustTime' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - $data->{daily}->{data}->[$i] - ->{windGustTime} - ) - ), - 'moonPhase' => - $data->{daily}->{data}->[$i]->{moonPhase}, - 'sunsetTime' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - $data->{daily}->{data}->[$i] - ->{sunsetTime} - ) - ), - 'sunriseTime' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - $data->{daily}->{data}->[$i] - ->{sunriseTime} - ) - ), - 'pressure' => int( - sprintf( "%.1f", - $data->{daily}->{data}->[$i]->{pressure} - ) + 0.5 - ), - 'visibility' => int( - sprintf( "%.1f", - $data->{daily}->{data}->[$i] - ->{visibility} ) + 0.5 - ), - } - ); - - $self->{cached}->{forecast} - ->{daily}[$i]{precipIntensityMax} = ( - defined( - $data->{daily}->{data}->[$i] - ->{precipIntensityMax} - ) - ? $data->{daily}->{data}->[$i]->{precipIntensityMax} - : '-' - ); - $self->{cached}->{forecast} - ->{daily}[$i]{precipIntensity} = ( - defined( - $data->{daily}->{data}->[$i]->{precipIntensity} - ) - ? $data->{daily}->{data}->[$i]->{precipIntensity} - : '-' - ); - $self->{cached}->{forecast} - ->{daily}[$i]{precipProbability} = ( - defined( - $data->{daily}->{data}->[$i] - ->{precipProbability} - ) - ? $data->{daily}->{data}->[$i]->{precipProbability} - * 100 - : '-' - ); - $self->{cached}->{forecast}->{daily}[$i]{precipType} = ( - defined( - $data->{daily}->{data}->[$i]->{precipType} - ) - ? $data->{daily}->{data}->[$i]->{precipType} - : '-' - ); - $self->{cached}->{forecast} - ->{daily}[$i]{precipIntensityMaxTime} = ( - defined( - $data->{daily}->{data}->[$i] - ->{precipIntensityMaxTime} - ) - ? strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - $data->{daily}->{data}->[$i] - ->{precipIntensityMaxTime} - ) - ) - : '-' - ); - - $i++; - } - - if ( ref( $data->{hourly}->{data} ) eq "ARRAY" - and scalar( @{ $data->{hourly}->{data} } ) > 0 ) - { - ### löschen des alten Datensatzes - delete $self->{cached}->{forecast}->{hourly}; - - my $i = 0; - foreach ( @{ $data->{hourly}->{data} } ) { - push( - @{ $self->{cached}->{forecast}->{hourly} }, - { - 'pubDate' => strftimeWrapper( - "%a, %e %b %Y %H:%M", - localtime( - $data->{hourly}->{data}->[$i] - ->{'time'} - ) - ), - 'day_of_week' => strftime( - "%a, %H:%M", - localtime( - $data->{hourly}->{data}->[$i] - ->{'time'} - ) - ), - 'temperature' => sprintf( "%.1f", - $data->{hourly}->{data}->[$i] - ->{temperature} ), - 'code' => $codes{ - $data->{hourly}->{data}->[$i]->{icon} - }, - 'iconAPI' => - $data->{hourly}->{data}->[$i]->{icon}, - 'condition' => encode_utf8( - $data->{hourly}->{data}->[$i]->{summary} - ), - 'ozone' => - $data->{hourly}->{data}->[$i]->{ozone}, - 'uvIndex' => - $data->{hourly}->{data}->[$i]->{uvIndex}, - 'dewPoint' => sprintf( "%.1f", - $data->{hourly}->{data}->[$i] - ->{dewPoint} ), - 'humidity' => - $data->{hourly}->{data}->[$i]->{humidity} - * 100, - 'cloudCover' => - $data->{hourly}->{data}->[$i] - ->{cloudCover} * 100, - 'wind_direction' => - $data->{hourly}->{data}->[$i] - ->{windBearing}, - 'wind' => int( - sprintf( - "%.1f", - ( - $data->{hourly}->{data}->[$i] - ->{windSpeed} * 3.6 - ) - ) + 0.5 - ), - 'wind_speed' => int( - sprintf( - "%.1f", - ( - $data->{hourly}->{data}->[$i] - ->{windSpeed} * 3.6 - ) - ) + 0.5 - ), - 'windGust' => int( - sprintf( - "%.1f", - ( - $data->{hourly}->{data}->[$i] - ->{windGust} * 3.6 - ) - ) + 0.5 - ), - - 'pressure' => sprintf( "%.1f", - $data->{hourly}->{data}->[$i] - ->{pressure} ), - 'visibility' => sprintf( "%.1f", - $data->{hourly}->{data}->[$i] - ->{visibility} ), - } - ); - - $self->{cached}->{forecast} - ->{hourly}[$i]{precipIntensity} = ( - defined( - $data->{hourly}->{data}->[$i] - ->{precipIntensity} - ) - ? $data->{hourly}->{data}->[$i] - ->{precipIntensity} - : '-' - ); - $self->{cached}->{forecast} - ->{hourly}[$i]{precipProbability} = ( - defined( - $data->{hourly}->{data}->[$i] - ->{precipProbability} - ) - ? $data->{hourly}->{data}->[$i] - ->{precipProbability} * 100 - : '-' - ); - $self->{cached}->{forecast} - ->{hourly}[$i]{precipType} = ( - defined( - $data->{hourly}->{data}->[$i]->{precipType} - ) - ? $data->{hourly}->{data}->[$i]->{precipType} - : '-' - ); - - $i++; - } - } - } + $self = _FillSelfHashWithWeatherResponse( $self, $data ); } } else { _ErrorHandling( $self, 'DarkSky Weather ' . $response ); } @@ -763,55 +362,397 @@ sub _ProcessingRetrieveData($$) { ## Aufruf der callbackFn _CallWeatherCallbackFn($self); + + return; } -sub _CallWeatherCallbackFn($) { +sub _FillSelfHashWithWeatherResponse { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $data = shift; + + # print Dumper $data; ## für Debugging + $self->{cached}->{current_date_time} = + _strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( $self->{fetchTime} ) ); + $self->{cached}->{timezone} = $data->{timezone}; + $self->{cached}->{license}{text} = + $data->{flags}->{'meteoalarm-license'}; + + ## löschen des alten current Datensatzes + delete $self->{cached}->{current}; + + ## löschen des alten forecast Datensatzes + delete $self->{cached}->{forecast}; + + $self = _FillSelfHashWithWeatherResponseForCurrent( $self, $data ); + + if ( ref( $data->{daily}->{data} ) eq "ARRAY" + and scalar( @{ $data->{daily}->{data} } ) > 0 ) + { + $self = + _FillSelfHashWithWeatherResponseForForecastDaily( $self, $data ); + + } + + if ( ref( $data->{hourly}->{data} ) eq "ARRAY" + and scalar( @{ $data->{hourly}->{data} } ) > 0 ) + { + $self = + _FillSelfHashWithWeatherResponseForForecastHourly( $self, $data ); + } + + return $self; +} + +sub _FillSelfHashWithWeatherResponseForCurrent { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $data = shift; + + $self->{cached}->{current} = { + 'temperature' => + int( sprintf( "%.0f", $data->{currently}->{temperature} ) ), + 'temp_c' => int( sprintf( "%.0f", $data->{currently}->{temperature} ) ), + 'dewPoint' => int( sprintf( "%.0f", $data->{currently}->{dewPoint} ) ), + 'humidity' => $data->{currently}->{humidity} * 100, + 'condition' => encode_utf8( $data->{currently}->{summary} ), + 'pressure' => + int( sprintf( "%.1f", $data->{currently}->{pressure} ) + 0.5 ), + 'wind' => int( + sprintf( "%.1f", ( $data->{currently}->{windSpeed} * 3.6 ) ) + 0.5 + ), + 'wind_speed' => int( + sprintf( "%.1f", ( $data->{currently}->{windSpeed} * 3.6 ) ) + 0.5 + ), + 'wind_direction' => $data->{currently}->{windBearing}, + 'windGust' => int( + sprintf( "%.1f", ( $data->{currently}->{windGust} * 3.6 ) ) + 0.5 + ), + 'cloudCover' => $data->{currently}->{cloudCover} * 100, + 'uvIndex' => $data->{currently}->{uvIndex}, + 'visibility' => + int( sprintf( "%.1f", $data->{currently}->{visibility} ) + 0.5 ), + 'ozone' => $data->{currently}->{ozone}, + 'code' => $codes{ $data->{currently}->{icon} }, + 'iconAPI' => $data->{currently}->{icon}, + 'pubDate' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{currently}->{'time'} ) + ), + 'precipProbability' => $data->{currently}->{precipProbability} * 100, + 'apparentTemperature' => + int( sprintf( "%.0f", $data->{currently}->{apparentTemperature} ) ), + 'precipIntensity' => $data->{currently}->{precipIntensity}, + }; + + return $self; +} + +sub _FillSelfHashWithWeatherResponseForForecastDaily { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $data = shift; + + my $i = 0; + foreach ( @{ $data->{daily}->{data} } ) { + push( + @{ $self->{cached}->{forecast}->{daily} }, + { + 'pubDate' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{daily}->{data}->[$i]->{'time'} ) + ), + 'day_of_week' => strftime( + "%a", localtime( $data->{daily}->{data}->[$i]->{'time'} ) + ), + 'low_c' => int( + sprintf( "%.0f", + $data->{daily}->{data}->[$i]->{temperatureLow} ) + ), + 'high_c' => int( + sprintf( "%.0f", + $data->{daily}->{data}->[$i]->{temperatureHigh} ) + ), + 'tempLow' => int( + sprintf( "%.0f", + $data->{daily}->{data}->[$i]->{temperatureLow} ) + ), + 'tempLowTime' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( + $data->{daily}->{data}->[$i]->{temperatureLowTime} + ) + ), + 'tempHigh' => int( + sprintf( "%.0f", + $data->{daily}->{data}->[$i]->{temperatureHigh} ) + ), + 'tempHighTime' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( + $data->{daily}->{data}->[$i]->{temperatureHighTime} + ) + ), + 'apparentTempLow' => int( + sprintf( "%.0f", + $data->{daily}->{data}->[$i]->{apparentTemperatureLow} ) + ), + 'apparentTempLowTime' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( + $data->{daily}->{data}->[$i] + ->{apparentTemperatureLowTime} + ) + ), + 'apparentTempHigh' => int( + sprintf( "%.0f", + $data->{daily}->{data}->[$i]->{apparentTemperatureHigh} + ) + ), + 'apparentTempHighTime' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( + $data->{daily}->{data}->[$i] + ->{apparentTemperatureHighTime} + ) + ), + 'code' => $codes{ $data->{daily}->{data}->[$i]->{icon} }, + 'iconAPI' => $data->{daily}->{data}->[$i]->{icon}, + 'condition' => + encode_utf8( $data->{daily}->{data}->[$i]->{summary} ), + 'ozone' => $data->{daily}->{data}->[$i]->{ozone}, + 'uvIndex' => $data->{daily}->{data}->[$i]->{uvIndex}, + 'uvIndexTime' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{daily}->{data}->[$i]->{uvIndexTime} ) + ), + 'dewPoint' => int( + sprintf( "%.0f", $data->{daily}->{data}->[$i]->{dewPoint} ) + ), + 'humidity' => $data->{daily}->{data}->[$i]->{humidity} * 100, + 'cloudCover' => $data->{daily}->{data}->[$i]->{cloudCover} * + 100, + 'wind_direction' => $data->{daily}->{data}->[$i]->{windBearing}, + 'wind' => int( + sprintf( "%.1f", + ( $data->{daily}->{data}->[$i]->{windSpeed} * 3.6 ) ) + + 0.5 + ), + 'wind_speed' => int( + sprintf( "%.1f", + ( $data->{daily}->{data}->[$i]->{windSpeed} * 3.6 ) ) + + 0.5 + ), + 'windGust' => int( + sprintf( "%.1f", + ( $data->{daily}->{data}->[$i]->{windGust} * 3.6 ) ) + + 0.5 + ), + 'windGustTime' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{daily}->{data}->[$i]->{windGustTime} ) + ), + 'moonPhase' => $data->{daily}->{data}->[$i]->{moonPhase}, + 'sunsetTime' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{daily}->{data}->[$i]->{sunsetTime} ) + ), + 'sunriseTime' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{daily}->{data}->[$i]->{sunriseTime} ) + ), + 'pressure' => int( + sprintf( "%.1f", $data->{daily}->{data}->[$i]->{pressure} ) + + 0.5 + ), + 'visibility' => int( + sprintf( "%.1f", + $data->{daily}->{data}->[$i]->{visibility} ) + 0.5 + ), + } + ); + + $self->{cached}->{forecast}->{daily}[$i]{precipIntensityMax} = ( + defined( $data->{daily}->{data}->[$i]->{precipIntensityMax} ) + ? $data->{daily}->{data}->[$i]->{precipIntensityMax} + : '-' + ); + $self->{cached}->{forecast}->{daily}[$i]{precipIntensity} = ( + defined( $data->{daily}->{data}->[$i]->{precipIntensity} ) + ? $data->{daily}->{data}->[$i]->{precipIntensity} + : '-' + ); + $self->{cached}->{forecast}->{daily}[$i]{precipProbability} = ( + defined( $data->{daily}->{data}->[$i]->{precipProbability} ) + ? $data->{daily}->{data}->[$i]->{precipProbability} * 100 + : '-' + ); + $self->{cached}->{forecast}->{daily}[$i]{precipType} = ( + defined( $data->{daily}->{data}->[$i]->{precipType} ) + ? $data->{daily}->{data}->[$i]->{precipType} + : '-' + ); + $self->{cached}->{forecast}->{daily}[$i]{precipIntensityMaxTime} = ( + defined( $data->{daily}->{data}->[$i]->{precipIntensityMaxTime} ) + ? _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( + $data->{daily}->{data}->[$i]->{precipIntensityMaxTime} + ) + ) + : '-' + ); + + $i++; + } + + return $self; +} + +sub _FillSelfHashWithWeatherResponseForForecastHourly { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $data = shift; + + my $i = 0; + + foreach ( @{ $data->{hourly}->{data} } ) { + push( + @{ $self->{cached}->{forecast}->{hourly} }, + { + 'pubDate' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{hourly}->{data}->[$i]->{'time'} ) + ), + 'day_of_week' => strftime( + "%a, %H:%M", + localtime( $data->{hourly}->{data}->[$i]->{'time'} ) + ), + 'temperature' => sprintf( "%.0f", + $data->{hourly}->{data}->[$i]->{temperature} ), + 'code' => $codes{ $data->{hourly}->{data}->[$i]->{icon} }, + 'iconAPI' => $data->{hourly}->{data}->[$i]->{icon}, + 'condition' => + encode_utf8( $data->{hourly}->{data}->[$i]->{summary} ), + 'ozone' => $data->{hourly}->{data}->[$i]->{ozone}, + 'uvIndex' => $data->{hourly}->{data}->[$i]->{uvIndex}, + 'dewPoint' => + sprintf( "%.0f", $data->{hourly}->{data}->[$i]->{dewPoint} ), + 'humidity' => $data->{hourly}->{data}->[$i]->{humidity} * 100, + 'cloudCover' => $data->{hourly}->{data}->[$i]->{cloudCover} * + 100, + 'wind_direction' => + $data->{hourly}->{data}->[$i]->{windBearing}, + 'wind' => int( + sprintf( "%.1f", + ( $data->{hourly}->{data}->[$i]->{windSpeed} * 3.6 ) ) + + 0.5 + ), + 'wind_speed' => int( + sprintf( "%.1f", + ( $data->{hourly}->{data}->[$i]->{windSpeed} * 3.6 ) ) + + 0.5 + ), + 'windGust' => int( + sprintf( "%.1f", + ( $data->{hourly}->{data}->[$i]->{windGust} * 3.6 ) ) + + 0.5 + ), + + 'pressure' => + sprintf( "%.1f", $data->{hourly}->{data}->[$i]->{pressure} ), + 'visibility' => sprintf( + "%.1f", $data->{hourly}->{data}->[$i]->{visibility} + ), + } + ); + + $self->{cached}->{forecast}->{hourly}[$i]{precipIntensity} = ( + defined( $data->{hourly}->{data}->[$i]->{precipIntensity} ) + ? $data->{hourly}->{data}->[$i]->{precipIntensity} + : '-' + ); + $self->{cached}->{forecast}->{hourly}[$i]{precipProbability} = ( + defined( $data->{hourly}->{data}->[$i]->{precipProbability} ) + ? $data->{hourly}->{data}->[$i]->{precipProbability} * 100 + : '-' + ); + $self->{cached}->{forecast}->{hourly}[$i]{precipType} = ( + defined( $data->{hourly}->{data}->[$i]->{precipType} ) + ? $data->{hourly}->{data}->[$i]->{precipType} + : '-' + ); + + $i++; + } + + return $self; +} + +sub _CallWeatherCallbackFn { + return 0 unless ( __PACKAGE__ eq caller(0) ); + my $self = shift; # ## Aufruf der callbackFn - main::Weather_RetrieveCallbackFn( $self->{devName} ); + return ::Weather_RetrieveCallbackFn( $self->{devName} ); } -sub _ErrorHandling($$) { - my ( $self, $err ) = @_; +sub _ErrorHandling { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $err = shift; $self->{cached}->{current_date_time} = - strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( $self->{fetchTime} ) ), - $self->{cached}->{status} = $err; + __strftimeWrapper( "%a, %e %b %Y %H:%M", + localtime( $self->{fetchTime} ) ); + $self->{cached}->{status} = $err; $self->{cached}->{validity} = 'stale'; + + return; } -sub _CreateForecastRef($) { +sub _CreateForecastRef { + return 0 unless ( __PACKAGE__ eq caller(0) ); + my $self = shift; my $forecastRef = ( { lat => $self->{lat}, long => $self->{long}, - apiMaintainer => -'Marko Oldenburg (CoolTux)', - apiVersion => version->parse( DarkSkyAPI->VERSION() )->normal, + apiMaintainer => 'Marko Oldenburg (' + . $META->{x_fhem_maintainer}[0] . ')', + apiVersion => version->parse( __PACKAGE__->VERSION() )->normal, } ); return $forecastRef; } -sub strftimeWrapper(@) { - my $string = POSIX::strftime(@_); +sub _strftimeWrapper { + my @data = @_; + return 0 unless ( __PACKAGE__ eq caller(0) ); - $string =~ s/\xe4/ä/g; - $string =~ s/\xc4/Ä/g; - $string =~ s/\xf6/ö/g; - $string =~ s/\xd6/Ö/g; - $string =~ s/\xfc/ü/g; - $string =~ s/\xdc/Ü/g; - $string =~ s/\xdf/ß/g; - $string =~ s/\xdf/ß/g; - $string =~ s/\xe1/á/g; - $string =~ s/\xe9/é/g; - $string =~ s/\xc1/Á/g; - $string =~ s/\xc9/É/g; + my $string = POSIX::strftime(@data); + + $string =~ s/\xe4/ä/xg; + $string =~ s/\xc4/Ä/xg; + $string =~ s/\xf6/ö/xg; + $string =~ s/\xd6/Ö/xg; + $string =~ s/\xfc/ü/xg; + $string =~ s/\xdc/Ü/xg; + $string =~ s/\xdf/ß/xg; + $string =~ s/\xdf/ß/xg; + $string =~ s/\xe1/á/xg; + $string =~ s/\xe9/é/xg; + $string =~ s/\xc1/Á/xg; + $string =~ s/\xc9/É/xg; return $string; } diff --git a/lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm b/lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm new file mode 100644 index 0000000..e4f5adc --- /dev/null +++ b/lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm @@ -0,0 +1,1029 @@ +# $Id: $ +############################################################################### +# +# Developed with VSCodium and richterger perl plugin +# +# (c) 2019-2023 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net) +# All rights reserved +# +# Special thanks goes to: +# - Harry (harryman) for many tests and patch that implements onecall API +# +# +# 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 +# any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# +############################################################################### + +### Beispielaufruf +# https://api.openweathermap.org/data/2.5/weather?lat=[lat]&lon=[long]&APPID=[API] Current +# https://api.openweathermap.org/data/2.5/forecast?lat=[lat]&lon=[long]&APPID=[API] Forecast +# https://api.openweathermap.org/data/3.0/onecall?lat=[lat]&lon=[long]&APPID=[API] Current,Forecast +# https://openweathermap.org/weather-conditions Icons und Conditions ID's + +package FHEM::APIs::Weather::OpenWeatherMapAPI; +use strict; +use warnings; +use FHEM::Meta; + +use POSIX; +use HttpUtils; +use experimental qw /switch/; + +my $META = {}; +my $ret = FHEM::Meta::getMetadata( __FILE__, $META ); +return "$@" if ($@); +return $ret if ($ret); +$::packages{OpenWeatherMapAPI}{META} = $META; + +use version 0.77; our $VERSION = $META->{version}; + +# use Data::Dumper; + +# try to use JSON::MaybeXS wrapper +# for chance of better performance + open code +eval { + require JSON::MaybeXS; + import JSON::MaybeXS qw( decode_json encode_json ); + 1; +} or do { + + # try to use JSON wrapper + # for chance of better performance + eval { + # JSON preference order + local $ENV{PERL_JSON_BACKEND} = + 'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP' + unless ( defined( $ENV{PERL_JSON_BACKEND} ) ); + + require JSON; + import JSON qw( decode_json encode_json ); + 1; + } or do { + + # In rare cases, Cpanel::JSON::XS may + # be installed but JSON|JSON::MaybeXS not ... + eval { + require Cpanel::JSON::XS; + import Cpanel::JSON::XS qw(decode_json encode_json); + 1; + } or do { + + # In rare cases, JSON::XS may + # be installed but JSON not ... + eval { + require JSON::XS; + import JSON::XS qw(decode_json encode_json); + 1; + } or do { + + # Fallback to built-in JSON which SHOULD + # be available since 5.014 ... + eval { + require JSON::PP; + import JSON::PP qw(decode_json encode_json); + 1; + } or do { + + # Fallback to JSON::backportPP in really rare cases + require JSON::backportPP; + import JSON::backportPP qw(decode_json encode_json); + 1; + }; + }; + }; + }; +}; + +my $missingModul = ''; +## no critic (Conditional "use" statement. Use "require" to conditionally include a module (Modules::ProhibitConditionalUseStatements)) +eval { use Encode qw /encode_utf8/; 1 } or $missingModul .= 'Encode '; + +# use Data::Dumper; # for Debug only +## API URL +eval { use Readonly; 1 } + or $missingModul .= 'Readonly '; # apt install libreadonly-perl +## use critic + +# Readonly my $URL => 'https://api.openweathermap.org/data/2.5/'; +Readonly my $URL => 'https://api.openweathermap.org/data/'; +## URL . 'weather?' for current data +## URL . 'onecall?' for current,forecast data + +my %codes = ( + 200 => 45, + 201 => 45, + 202 => 45, + 210 => 4, + 211 => 4, + 212 => 3, + 221 => 4, + 230 => 45, + 231 => 45, + 232 => 45, + 300 => 9, + 301 => 9, + 302 => 9, + 310 => 9, + 311 => 9, + 312 => 9, + 313 => 9, + 314 => 9, + 321 => 9, + 500 => 35, + 501 => 35, + 502 => 35, + 503 => 35, + 504 => 35, + 511 => 35, + 520 => 35, + 521 => 35, + 522 => 35, + 531 => 35, + 600 => 14, + 601 => 16, + 602 => 13, + 611 => 46, + 612 => 46, + 613 => 46, + 615 => 5, + 616 => 5, + 620 => 14, + 621 => 46, + 622 => 42, + 701 => 19, + 711 => 22, + 721 => 19, + 731 => 23, + 741 => 20, + 751 => 23, + 761 => 19, + 762 => 3200, + 771 => 1, + 781 => 0, + 800 => 32, + 801 => 30, + 802 => 26, + 803 => 26, + 804 => 28, +); + +### begin public function +sub new { + ### geliefert wird ein Hash + my $class = shift; + my $argsRef = shift; + + my $apioptions = _parseApiOptions( $argsRef->{apioptions} ); + + my $self = { + devName => $argsRef->{devName}, + key => ( + ( defined( $argsRef->{apikey} ) && $argsRef->{apikey} ) + ? $argsRef->{apikey} + : 'none' + ), + lang => $argsRef->{language}, + lat => ( split( ',', $argsRef->{location} ) )[0], + long => ( split( ',', $argsRef->{location} ) )[1], + fetchTime => 0, + endpoint => 'none', + forecast => '', + alerts => 0, + }; + + $self->{cachemaxage} = ( + defined( $apioptions->{cachemaxage} ) + ? $apioptions->{cachemaxage} + : 900 + ); + + $self->{apiversion} = + ( $apioptions->{version} ? $apioptions->{version} : '2.5' ); + + $self->{cached} = _CreateForecastRef($self); + + bless $self, $class; + return $self; +} + +sub setAlerts { + my $self = shift; + my $alerts = shift // 0; + + $self->{alerts} = $alerts; + return; +} + +sub setForecast { + my $self = shift; + my $forecast = shift // ''; + + $self->{forecast} = $forecast; + return; +} + +sub setFetchTime { + my $self = shift; + + $self->{fetchTime} = time(); + return; +} + +sub setRetrieveData { + my $self = shift; + + _RetrieveDataFromOpenWeatherMap($self); + return; +} + +sub setLocation { + my $self = shift; + my $lat = shift; + my $long = shift; + + $self->{lat} = $lat; + $self->{long} = $long; + + return; +} + +sub getFetchTime { + my $self = shift; + + return $self->{fetchTime}; +} + +sub getWeather { + my $self = shift; + + return $self->{cached}; +} + +### begin privat function +sub _parseApiOptions { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $apioptions = shift; + + my @params; + my %h; + + @params = split( ',', $apioptions ); + while (@params) { + my $param = shift(@params); + next if ( $param eq '' ); + my ( $key, $value ) = split( ':', $param, 2 ); + $h{$key} = $value; + } + + return \%h; +} + +sub _RetrieveDataFromOpenWeatherMap { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + + # retrieve data from cache + if ( ( time() - $self->{fetchTime} ) < $self->{cachemaxage} + && $self->{cached}->{lat} == $self->{lat} + && $self->{cached}->{long} == $self->{long} + && $self->{endpoint} eq 'none' ) + { + return _CallWeatherCallbackFn($self); + } + + $self->{cached}->{lat} = $self->{lat} + unless ( $self->{cached}->{lat} == $self->{lat} ); + $self->{cached}->{long} = $self->{long} + unless ( $self->{cached}->{long} == $self->{long} ); + + my $paramRef = { + timeout => 15, + self => $self, + endpoint => $self->{endpoint} eq 'none' + ? ( $self->{apiversion} == 3.0 ? 'onecall' : 'weather' ) + : 'forecast', + callback => \&_RetrieveDataFinished, + }; + + $self->{endpoint} = $paramRef->{endpoint}; + + if ( $self->{lat} eq 'error' + || $self->{long} eq 'error' + || $self->{key} eq 'none' + || $missingModul ) + { + _RetrieveDataFinished( + $paramRef, +'The given location is invalid. (wrong latitude or longitude?) put both as an attribute in the global device or set define option location=[LAT],[LONG]', + undef + ) if ( $self->{lat} eq 'error' || $self->{long} eq 'error' ); + + _RetrieveDataFinished( $paramRef, + 'No given api key. (define myWeather Weather apikey=[KEY])', + undef ) + if ( $self->{key} eq 'none' ); + + _RetrieveDataFinished( $paramRef, + 'Perl modul ' . $missingModul . ' is missing.', undef ) + if ($missingModul); + } + else { + $paramRef->{url} = + $URL + . $self->{apiversion} . '/' + . $paramRef->{endpoint} . '?' . 'lat=' + . $self->{lat} . '&' . 'lon=' + . $self->{long} . '&' + . 'APPID=' + . $self->{key} . '&' + . 'units=' + . 'metric' . '&' . 'lang=' + . $self->{lang} . '&' + . 'exclude=' + . _CreateExcludeString( $self->{forecast}, $self->{alerts} ); + + ::HttpUtils_NonblockingGet($paramRef); + } + + return; +} + +sub _CreateExcludeString { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $forecast = shift; + my $alerts = shift; + + my @exclude = qw/alerts minutely hourly daily/; + my @forecast = split( ',', $forecast ); + my @alerts = ( $alerts ? 'alerts' : '' ); + + my %in_forecast = map { $_ => 1 } @forecast, @alerts; + my @diff = grep { not $in_forecast{$_} } @exclude; + + return join( ',', @diff ); +} + +sub _RetrieveDataFinished { + return 0 unless ( 'main' eq caller(0) ); + + my $paramRef = shift; + my $err = shift; + my $response = shift; + my $self = $paramRef->{self}; + + if ( !$err ) { + $self->{cached}->{status} = 'ok'; + $self->{cached}->{validity} = 'up-to-date'; + $self->{fetchTime} = time(); + _ProcessingRetrieveData( $self, $response ); + } + else { + $self->{fetchTime} = time() if ( not defined( $self->{fetchTime} ) ); + _ErrorHandling( $self, $err ); + _ProcessingRetrieveData( $self, $response ); + } + + return; +} + +sub _ProcessingRetrieveData { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $response = shift; + + if ( $self->{cached}->{status} eq 'ok' + && defined($response) + && $response ) + { + if ( $response =~ m/^{.*}$/x ) { + my $data = eval { decode_json($response) }; + + if ($@) { + _ErrorHandling( $self, + 'OpenWeatherMap Weather decode JSON err ' . $@ ); + } + elsif (defined( $data->{cod} ) + && $data->{cod} + && $data->{cod} != 200 + && defined( $data->{message} ) + && $data->{message} ) + { + _ErrorHandling( $self, $data->{cod} . ': ' . $data->{message} ); + } + else { + $self = _FillSelfHashWithWeatherResponse( $self, $data ); + } + } + else { _ErrorHandling( $self, 'OpenWeatherMap ' . $response ); } + } + + $self->{endpoint} = 'none' + if ( $self->{endpoint} eq 'onecall' + or $self->{endpoint} eq 'forecast' ); + + _RetrieveDataFromOpenWeatherMap($self) + if ( $self->{endpoint} eq 'weather' ); + + _CallWeatherCallbackFn($self) if ( $self->{endpoint} eq 'none' ); + + return; +} + +sub _FillSelfHashWithWeatherResponse { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $data = shift; + + ### Debug + # print '!!! DEBUG !!! - Endpoint: ' . $self->{endpoint} . "\n"; + # print '!!! DEBUG !!! - Response: ' . Dumper $data; + ###### Ab hier wird die ResponseHash Referenze für die Rückgabe zusammen gestellt + $self->{cached}->{current_date_time} = + _strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( $self->{fetchTime} ) ); + $self->{cached}->{country} = $data->{sys}->{country}; + $self->{cached}->{city} = encode_utf8( $data->{name} ); + $self->{cached}->{license}{text} = 'none'; + + given ( $self->{endpoint} ) { + when ('onecall') { + ## löschen des alten current Datensatzes + delete $self->{cached}->{current}; + + ## löschen des alten forecast Datensatzes + delete $self->{cached}->{forecast}; + + ## löschen des alten Alerts Datensatzes + delete $self->{cached}->{alerts}; + + $self = + _FillSelfHashWithWeatherResponseForOnecallCurrent( $self, $data ); + + if ( ref( $data->{hourly} ) eq "ARRAY" + && scalar( @{ $data->{hourly} } ) > 0 ) + { + $self = + _FillSelfHashWithWeatherResponseForOnecallHourly( $self, + $data ); + } + + if ( ref( $data->{daily} ) eq "ARRAY" + && scalar( @{ $data->{daily} } ) > 0 ) + { + $self = + _FillSelfHashWithWeatherResponseForOnecallDaily( $self, + $data ); + } + + if ( ref( $data->{alerts} ) eq "ARRAY" + && scalar( @{ $data->{alerts} } ) > 0 ) + { + $self = + _FillSelfHashWithWeatherResponseForOnecallAlerts( $self, + $data ); + } + } + + when ('weather') { + ## löschen des alten current Datensatzes + delete $self->{cached}->{current}; + + ## löschen des alten Alerts Datensatzes + delete $self->{cached}->{alerts}; + + $self = + _FillSelfHashWithWeatherResponseForWeatherCurrent( $self, $data ); + } + + when ('forecast') { + ## löschen des alten forecast Datensatzes + delete $self->{cached}->{forecast}; + + if ( ref( $data->{list} ) eq "ARRAY" + and scalar( @{ $data->{list} } ) > 0 ) + { + $self = + _FillSelfHashWithWeatherResponseForForecastHourly( $self, + $data ); + } + } + } + + return $self; +} + +sub _FillSelfHashWithWeatherResponseForOnecallAlerts { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $data = shift; + + my $i = 0; + for ( @{ $data->{alerts} } ) { + push( + @{ $self->{cached}->{alerts} }, + { + 'End' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( ( $data->{alerts}->[$i]->{end} ) ) + ), + 'Start' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( ( $data->{alerts}->[$i]->{start} ) ) + ), + 'Description' => + encode_utf8( $data->{alerts}->[$i]->{description} ), + 'SenderName' => + encode_utf8( $data->{alerts}->[$i]->{sender_name} ), + 'Event' => encode_utf8( $data->{alerts}->[$i]->{event} ), + }, + ); + + $i++; + } + + return $self; +} + +sub _FillSelfHashWithWeatherResponseForWeatherCurrent { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $data = shift; + + $self->{cached}->{current} = { + 'temperature' => int( sprintf( "%.0f", $data->{main}->{temp} ) ), + 'temp_c' => int( sprintf( "%.0f", $data->{main}->{temp} ) ), + 'low_c' => int( sprintf( "%.0f", $data->{main}->{temp_min} ) ), + 'high_c' => int( sprintf( "%.0f", $data->{main}->{temp_max} ) ), + 'tempLow' => int( sprintf( "%.0f", $data->{main}->{temp_min} ) ), + 'tempHigh' => int( sprintf( "%.0f", $data->{main}->{temp_max} ) ), + 'tempFeelsLike_c' => + int( sprintf( "%.0f", $data->{main}->{feels_like} ) ), + 'humidity' => $data->{main}->{humidity}, + 'condition' => encode_utf8( $data->{weather}->[0]->{description} ), + 'pressure' => int( sprintf( "%.1f", $data->{main}->{pressure} ) + 0.5 ), + 'wind' => + int( sprintf( "%.1f", ( $data->{wind}->{speed} * 3.6 ) ) + 0.5 ), + 'wind_speed' => + int( sprintf( "%.1f", ( $data->{wind}->{speed} * 3.6 ) ) + 0.5 ), + 'wind_gust' => + int( sprintf( "%.1f", ( $data->{wind}->{gust} * 3.6 ) ) + 0.5 ), + 'wind_direction' => $data->{wind}->{deg}, + 'cloudCover' => $data->{clouds}->{all}, + 'code' => $codes{ $data->{weather}->[0]->{id} }, + 'iconAPI' => $data->{weather}->[0]->{icon}, + 'sunsetTime' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{sys}->{sunset} ) + ), + 'sunriseTime' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{sys}->{sunrise} ) + ), + 'pubDate' => + _strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( $data->{dt} ) ), + }; + + $self->{cached}->{current}->{'visibility'} = + int( sprintf( "%.1f", $data->{visibility} ) + 0.5 ) + if ( exists $data->{visibility} ); + + return $self; +} + +sub _FillSelfHashWithWeatherResponseForForecastHourly { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $data = shift; + + my $i = 0; + for ( @{ $data->{list} } ) { + push( + @{ $self->{cached}->{forecast}->{hourly} }, + { + 'pubDate' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{list}->[$i]->{dt} ) + ), + 'day_of_week' => strftime( + "%a, %H:%M", localtime( $data->{list}->[$i]->{dt} ) + ), + 'temperature' => + int( sprintf( "%.0f", $data->{list}->[$i]->{main}->{temp} ) ), + 'temp_c' => + int( sprintf( "%.0f", $data->{list}->[$i]->{main}->{temp} ) ), + 'low_c' => int( + sprintf( "%.0f", $data->{list}->[$i]->{main}->{temp_min} ) + ), + 'high_c' => int( + sprintf( "%.0f", + $data->{list}->[$i]->{main}->{temp_max} - 273.15 ) + ), + 'tempLow' => int( + sprintf( "%.0f", + $data->{list}->[$i]->{main}->{temp_min} - 273.15 ) + ), + 'tempHigh' => int( + sprintf( "%.0f", + $data->{list}->[$i]->{main}->{temp_max} - 273.15 ) + ), + 'humidity' => $data->{list}->[$i]->{main}->{humidity}, + 'condition' => encode_utf8( + $data->{list}->[$i]->{weather}->[0]->{description} + ), + 'pressure' => int( + sprintf( "%.1f", $data->{list}->[$i]->{main}->{pressure} ) + + 0.5 + ), + 'wind' => int( + sprintf( "%.1f", + ( $data->{list}->[$i]->{wind}->{speed} * 3.6 ) ) + 0.5 + ), + 'wind_speed' => int( + sprintf( "%.1f", + ( $data->{list}->[$i]->{wind}->{speed} * 3.6 ) ) + 0.5 + ), + 'wind_gust' => int( + sprintf( "%.1f", + ( $data->{list}->[$i]->{wind}->{gust} * 3.6 ) ) + 0.5 + ), + 'cloudCover' => $data->{list}->[$i]->{clouds}->{all}, + 'code' => $codes{ $data->{list}->[$i]->{weather}->[0]->{id} }, + 'iconAPI' => $data->{list}->[$i]->{weather}->[0]->{icon}, + 'rain1h' => $data->{list}->[$i]->{rain}->{'1h'}, + 'rain3h' => $data->{list}->[$i]->{rain}->{'3h'}, + 'snow1h' => $data->{list}->[$i]->{snow}->{'1h'}, + 'snow3h' => $data->{list}->[$i]->{snow}->{'3h'}, + } + ); + + $i++; + } + + return $self; +} + +sub _FillSelfHashWithWeatherResponseForOnecallCurrent { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $data = shift; + + $self->{cached}->{current} = { + 'temperature' => int( sprintf( "%.0f", $data->{current}->{temp} ) ), + 'temp_c' => int( sprintf( "%.0f", $data->{current}->{temp} ) ), + 'tempFeelsLike_c' => + int( sprintf( "%.0f", $data->{current}->{feels_like} ) ), + 'dew_point' => int( sprintf( "%.0f", $data->{current}->{dew_point} ) ), + 'humidity' => $data->{current}->{humidity}, + 'condition' => + encode_utf8( $data->{current}->{weather}->[0]->{description} ), + 'pressure' => + int( sprintf( "%.1f", $data->{current}->{pressure} ) + 0.5 ), + 'wind' => int( + sprintf( "%.1f", ( $data->{current}->{wind_speed} * 3.6 ) ) + 0.5 + ), + 'wind_speed' => int( + sprintf( "%.1f", ( $data->{current}->{wind_speed} * 3.6 ) ) + 0.5 + ), + 'wind_gust' => int( + sprintf( "%.1f", ( $data->{current}->{wind_gust} * 3.6 ) ) + 0.5 + ), + 'wind_direction' => $data->{current}->{wind_deg}, + 'rain_1h' => $data->{rain}->{'1h'}, + 'cloudCover' => $data->{current}->{clouds}, + 'code' => $codes{ $data->{current}->{weather}->[0]->{id} }, + 'iconAPI' => $data->{current}->{weather}->[0]->{icon}, + 'condition' => + encode_utf8( $data->{current}->{weather}->[0]->{description} ), + 'sunsetTime' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{current}->{sunset} ) + ), + 'sunriseTime' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{current}->{sunrise} ) + ), + 'pubDate' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{current}->{dt} ) + ), + 'visibility' => + int( sprintf( "%.1f", $data->{current}->{visibility} ) + 0.5 ), + 'uvi' => $data->{current}->{uvi}, + 'timezone' => $data->{timezone}, + 'timezone_offset' => $data->{timezone_offset}, + }; + + return $self; +} + +sub _FillSelfHashWithWeatherResponseForOnecallDaily { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $data = shift; + + my $i = 0; + for ( @{ $data->{daily} } ) { + push( + @{ $self->{cached}->{forecast}->{daily} }, + { + 'pubDate' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{daily}->[$i]->{dt} ) + ), + 'day_of_week' => strftime( + "%a, %H:%M", localtime( $data->{daily}->[$i]->{dt} ) + ), + 'sunrise' => strftime( + "%H:%M", localtime( $data->{daily}->[$i]->{sunrise} ) + ), + 'sunset' => strftime( + "%a, %H:%M", localtime( $data->{daily}->[$i]->{sunset} ) + ), + 'moonrise' => strftime( + "%a, %H:%M", localtime( $data->{daily}->[$i]->{moonrise} ) + ), + 'moon_phase' => $data->{daily}->[$i]->{moon_phase}, + 'moonset' => strftime( + "%a, %H:%M", localtime( $data->{daily}->[$i]->{moonset} ) + ), + 'temperature' => + int( sprintf( "%.0f", $data->{daily}->[$i]->{temp}->{day} ) ), + 'temperature_morn' => int( + sprintf( "%.0f", $data->{daily}->[$i]->{temp}->{morn} ) + ), + 'temperature_eve' => + int( sprintf( "%.0f", $data->{daily}->[$i]->{temp}->{eve} ) ), + 'temperature_night' => int( + sprintf( "%.0f", $data->{daily}->[$i]->{temp}->{night} ) + ), + 'tempFeelsLike_morn' => int( + sprintf( "%.0f", + $data->{daily}->[$i]->{feels_like}->{morn} ) + ), + 'tempFeelsLike_eve' => int( + sprintf( + "%.0f", $data->{daily}->[$i]->{feels_like}->{eve} + ) + ), + 'tempFeelsLike_night' => int( + sprintf( "%.0f", + $data->{daily}->[$i]->{feels_like}->{night} ) + ), + 'tempFeelsLike_day' => int( + sprintf( + "%.0f", $data->{daily}->[$i]->{feels_like}->{day} + ) + ), + 'temp_c' => + int( sprintf( "%.0f", $data->{daily}->[$i]->{temp}->{day} ) ), + 'low_c' => + int( sprintf( "%.0f", $data->{daily}->[$i]->{temp}->{min} ) ), + 'high_c' => + int( sprintf( "%.0f", $data->{daily}->[$i]->{temp}->{max} ) ), + 'tempLow' => + int( sprintf( "%.0f", $data->{daily}->[$i]->{temp}->{min} ) ), + 'tempHigh' => + int( sprintf( "%.0f", $data->{daily}->[$i]->{temp}->{max} ) ), + 'dew_point' => + int( sprintf( "%.0f", $data->{daily}->[$i]->{dew_point} ) ), + 'humidity' => $data->{daily}->[$i]->{humidity}, + 'condition' => encode_utf8( + $data->{daily}->[$i]->{weather}->[0]->{description} + ), + 'code' => $codes{ $data->{daily}->[$i]->{weather}->[0]->{id} }, + 'iconAPI' => $data->{daily}->[$i]->{weather}->[0]->{icon}, + 'pressure' => int( + sprintf( "%.1f", $data->{daily}->[$i]->{pressure} ) + 0.5 + ), + 'wind' => int( + sprintf( "%.1f", + ( $data->{daily}->[$i]->{wind_speed} * 3.6 ) ) + 0.5 + ), + 'wind_speed' => int( + sprintf( "%.1f", + ( $data->{daily}->[$i]->{wind_speed} * 3.6 ) ) + 0.5 + ), + 'wind_gust' => int( + sprintf( "%.1f", + ( $data->{daily}->[$i]->{wind_gust} * 3.6 ) ) + 0.5 + ), + 'wind_direction' => int( + sprintf( "%.1f", ( $data->{daily}->[$i]->{wind_deg} ) ) + ), + 'cloudCover' => $data->{daily}->[$i]->{clouds}, + 'code' => $codes{ $data->{daily}->[$i]->{weather}->[0]->{id} }, + 'rain' => $data->{daily}->[$i]->{rain}, + 'snow' => $data->{daily}->[$i]->{snow}, + 'uvi' => $data->{daily}->[$i]->{uvi}, + }, + ); + + $i++; + } + + return $self; +} + +sub _FillSelfHashWithWeatherResponseForOnecallHourly { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $data = shift; + + my $i = 0; + for ( @{ $data->{hourly} } ) { + push( + @{ $self->{cached}->{forecast}->{hourly} }, + { + 'pubDate' => _strftimeWrapper( + "%a, %e %b %Y %H:%M", + localtime( $data->{hourly}->[$i]->{dt} ) + ), + 'day_of_week' => strftime( + "%a, %H:%M", localtime( $data->{hourly}->[$i]->{dt} ) + ), + 'temperature' => + int( sprintf( "%.0f", $data->{hourly}->[$i]->{temp} ) ), + 'temp_c' => + int( sprintf( "%.0f", $data->{hourly}->[$i]->{temp} ) ), + 'tempFeelsLike' => + int( sprintf( "%.0f", $data->{hourly}->[$i]->{feels_like} ) ), + 'dew_point' => + int( sprintf( "%.0f", $data->{hourly}->[$i]->{dew_point} ) ), + 'humidity' => $data->{hourly}->[$i]->{humidity}, + 'condition' => encode_utf8( + $data->{hourly}->[$i]->{weather}->[0]->{description} + ), + 'pressure' => int( + sprintf( "%.1f", $data->{hourly}->[$i]->{pressure} ) + 0.5 + ), + 'wind' => int( + sprintf( "%.1f", + ( $data->{hourly}->[$i]->{wind_speed} * 3.6 ) ) + 0.5 + ), + 'wind_speed' => int( + sprintf( "%.1f", + ( $data->{hourly}->[$i]->{wind_speed} * 3.6 ) ) + 0.5 + ), + 'wind_gust' => int( + sprintf( "%.1f", + ( $data->{hourly}->[$i]->{wind_gust} * 3.6 ) ) + 0.5 + ), + 'wind_direction' => $data->{hourly}->[$i]->{wind_deg}, + 'cloudCover' => $data->{hourly}->[$i]->{clouds}, + 'code' => $codes{ $data->{hourly}->[$i]->{weather}->[0]->{id} }, + 'iconAPI' => $data->{hourly}->[$i]->{weather}->[0]->{icon}, + 'rain1h' => $data->{hourly}->[$i]->{rain}->{'1h'}, + 'snow1h' => $data->{hourly}->[$i]->{snow}->{'1h'}, + 'uvi' => $data->{hourly}->[$i]->{uvi}, + 'visibility' => int( + sprintf( "%.1f", $data->{hourly}->[$i]->{visibility} ) + 0.5 + ), + }, + ); + + $i++; + } + + return $self; +} + +sub _CallWeatherCallbackFn { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + + # print 'Dumperausgabe: ' . Dumper $self; + ### Aufruf der callbackFn + return ::Weather_RetrieveCallbackFn( $self->{devName} ); +} + +sub _ErrorHandling { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $err = shift; + + $self->{cached}->{current_date_time} = + _strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( $self->{fetchTime} ) ); + $self->{cached}->{status} = $err; + $self->{cached}->{validity} = 'stale'; + + return; +} + +sub _CreateForecastRef { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + + my $forecastRef = ( + { + lat => $self->{lat}, + long => $self->{long}, + apiMaintainer => 'Marko Oldenburg (' + . $META->{x_fhem_maintainer}[0] . ')', + apiVersion => version->parse( __PACKAGE__->VERSION() )->normal, + } + ); + + return $forecastRef; +} + +sub _strftimeWrapper { + my @data = @_; + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $string = POSIX::strftime(@data); + + $string =~ s/\xe4/ä/xg; + $string =~ s/\xc4/Ä/xg; + $string =~ s/\xf6/ö/xg; + $string =~ s/\xd6/Ö/xg; + $string =~ s/\xfc/ü/xg; + $string =~ s/\xdc/Ü/xg; + $string =~ s/\xdf/ß/xg; + $string =~ s/\xdf/ß/xg; + $string =~ s/\xe1/á/xg; + $string =~ s/\xe9/é/xg; + $string =~ s/\xc1/Á/xg; + $string =~ s/\xc9/É/xg; + + return $string; +} + +############################################################################## + +1; + +=pod + +=encoding utf8 + +=for :application/json;q=META.json OpenWeatherMapAPI.pm +{ + "abstract": "Weather API for Weather OpenWeatherMap", + "x_lang": { + "de": { + "abstract": "Wetter API für OpenWeatherMap" + } + }, + "version": "v3.0.2", + "author": [ + "Marko Oldenburg " + ], + "x_fhem_maintainer": [ + "CoolTux" + ], + "x_fhem_maintainer_github": [ + "CoolTuxNet" + ], + "prereqs": { + "runtime": { + "requires": { + "FHEM::Meta": 0, + "HttpUtils": 0, + "strict": 0, + "warnings": 0, + "constant": 0, + "POSIX": 0, + "JSON::PP": 0 + }, + "recommends": { + "JSON": 0 + }, + "suggests": { + "JSON::XS": 0, + "Cpanel::JSON::XS": 0 + } + } + } +} +=end :application/json;q=META.json + +=cut + +__END__ diff --git a/wundergroundAPI.pm b/lib/FHEM/APIs/Weather/wundergroundAPI.pm similarity index 90% rename from wundergroundAPI.pm rename to lib/FHEM/APIs/Weather/wundergroundAPI.pm index 2cf4704..3e7de9e 100644 --- a/wundergroundAPI.pm +++ b/lib/FHEM/APIs/Weather/wundergroundAPI.pm @@ -1,20 +1,22 @@ # $Id$ -package wundergroundAPI; +package FHEM::APIs::Weather::wundergroundAPI; use strict; use warnings; use FHEM::Meta; -use Data::Dumper; - -FHEM::Meta::Load(__PACKAGE__); -use version 0.77; our $VERSION = $main::packages{wundergroundAPI}{META}{version}; - -package wundergroundAPI::Weather; -use strict; -use warnings; use POSIX; -use Encode; use HttpUtils; +use experimental qw /switch/; + +my $META = {}; +my $ret = FHEM::Meta::getMetadata( __FILE__, $META ); +return "$@" if ($@); +return $ret if ($ret); +$::packages{wundergroundAPI}{META} = $META; + +use version 0.77; our $VERSION = $META->{version}; + +# use Data::Dumper; # try to use JSON::MaybeXS wrapper # for chance of better performance + open code @@ -71,16 +73,25 @@ eval { }; }; +my $missingModul = ''; + # use Data::Dumper; # for Debug only ## API URL -use constant DEMODATA => +eval { use Readonly; 1 } + or $missingModul .= 'Readonly '; # apt install libreadonly-perl +## use critic + +# use Data::Dumper; # for Debug only +## API URL +Readonly my $DEMODATA => '{"daily":{"dayOfWeek":["Freitag","Samstag","Sonntag","Montag","Dienstag","Mittwoch"],"expirationTimeUtc":[1555688120,1555688120,1555688120,1555688120,1555688120,1555688120],"moonPhase":["Vollmond","abnehmender Halbmond","abnehmender Halbmond","abnehmender Halbmond","abnehmender Halbmond","abnehmender Halbmond"],"moonPhaseCode":["F","WNG","WNG","WNG","WNG","WNG"],"moonPhaseDay":[15,16,17,18,19,20],"moonriseTimeLocal":["2019-04-19T20:09:54+0200","2019-04-20T21:30:54+0200","2019-04-21T22:48:07+0200","","2019-04-23T00:00:38+0200","2019-04-24T01:05:27+0200"],"moonriseTimeUtc":[1555697394,1555788654,1555879687,null,1555970438,1556060727],"moonsetTimeLocal":["2019-04-19T06:31:01+0200","2019-04-20T06:54:19+0200","2019-04-21T07:20:19+0200","2019-04-22T07:50:19+0200","2019-04-23T08:25:54+0200","2019-04-24T09:09:28+0200"],"moonsetTimeUtc":[1555648261,1555736059,1555824019,1555912219,1556000754,1556089768],"narrative":["Meistens klar. Tiefsttemperatur 5C.","Meistens klar. Höchsttemperaturen 19 bis 21C und Tiefsttemperaturen 4 bis 6C.","Meistens klar. Höchsttemperaturen 20 bis 22C und Tiefsttemperaturen 6 bis 8C.","Meistens klar. Höchsttemperaturen 20 bis 22C und Tiefsttemperaturen 9 bis 11C.","Teilweise bedeckt und windig. Höchsttemperaturen 21 bis 23C und Tiefsttemperaturen 11 bis 13C.","Teilweise bedeckt. Höchsttemperaturen 22 bis 24C und Tiefsttemperaturen 12 bis 14C."],"qpf":[0.0,0.0,0.0,0.0,0.0,0.0],"qpfSnow":[0.0,0.0,0.0,0.0,0.0,0.0],"sunriseTimeLocal":["2019-04-19T06:00:46+0200","2019-04-20T05:58:38+0200","2019-04-21T05:56:31+0200","2019-04-22T05:54:25+0200","2019-04-23T05:52:20+0200","2019-04-24T05:50:15+0200"],"sunriseTimeUtc":[1555646446,1555732718,1555818991,1555905265,1555991540,1556077815],"sunsetTimeLocal":["2019-04-19T20:11:02+0200","2019-04-20T20:12:46+0200","2019-04-21T20:14:29+0200","2019-04-22T20:16:13+0200","2019-04-23T20:17:56+0200","2019-04-24T20:19:40+0200"],"sunsetTimeUtc":[1555697462,1555783966,1555870469,1555956973,1556043476,1556129980],"temperatureMax":[null,20,21,21,22,23],"temperatureMin":[5,5,7,10,12,13],"validTimeLocal":["2019-04-19T07:00:00+0200","2019-04-20T07:00:00+0200","2019-04-21T07:00:00+0200","2019-04-22T07:00:00+0200","2019-04-23T07:00:00+0200","2019-04-24T07:00:00+0200"],"validTimeUtc":[1555650000,1555736400,1555822800,1555909200,1555995600,1556082000],"daypart":[{"cloudCover":[null,0,25,8,0,0,7,26,55,46,62,44],"dayOrNight":[null,"N","D","N","D","N","D","N","D","N","D","N"],"daypartName":[null,"Heute Abend","Morgen","Morgen Abend","Sonntag","Sonntagnacht","Montag","Montagnacht","Dienstag","Dienstagnacht","Mittwoch","Mittwochnacht"],"iconCode":[null,31,34,33,32,31,34,33,24,29,30,29],"iconCodeExtend":[null,3100,3400,3300,3200,3100,3400,3300,3010,2900,3000,2900],"narrative":[null,"Meistens klar. Tiefsttemperatur 5C. Wind aus NO mit 2 bis 4 m/s.","Meistens klar. Höchsttemperatur 20C. Wind aus NNO mit 2 bis 4 m/s.","Meistens klar. Tiefsttemperatur 5C. Wind aus NO mit 2 bis 4 m/s.","Meistens klar. Höchsttemperatur 21C. Wind aus O und wechselhaft.","Meistens klar. Tiefsttemperatur 7C. Wind aus ONO und wechselhaft.","Meistens klar. Höchsttemperatur 21C. Wind aus O mit 4 bis 9 m/s.","Meistens klar. Tiefsttemperatur 10C. Wind aus O mit 4 bis 9 m/s.","Teilweise bedeckt und windig. Höchsttemperatur 22C. Wind aus OSO mit 9 bis 13 m/s.","Teilweise bedeckt. Tiefsttemperatur 12C. Wind aus SO mit 4 bis 9 m/s.","Teilweise bedeckt. Höchsttemperatur 23C. Wind aus SO mit 4 bis 9 m/s.","Teilweise bedeckt. Tiefsttemperatur 13C. Wind aus SO mit 2 bis 4 m/s."],"precipChance":[null,0,0,0,0,0,20,20,0,0,0,10],"precipType":[null,"rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain"],"qpf":[null,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"qpfSnow":[null,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"qualifierCode":[null,null,null,null,null,null,null,null,null,null,null,null],"qualifierPhrase":[null,null,null,null,null,null,null,null,null,null,null,null],"relativeHumidity":[null,50,44,55,41,55,42,48,45,55,53,64],"snowRange":[null,"","","","","","","","","","",""],"temperature":[null,5,20,5,21,7,21,10,22,12,23,13],"temperatureHeatIndex":[null,21,20,18,20,18,20,18,22,20,23,22],"temperatureWindChill":[null,5,5,5,6,6,7,8,8,10,10,13],"thunderCategory":[null,null,null,null,null,null,null,null,null,null,null,null],"thunderIndex":[null,0,0,0,0,0,0,0,0,0,0,0],"uvDescription":[null,"Niedrig","Mittel","Niedrig","Mittel","Niedrig","Mittel","Niedrig","Mittel","Niedrig","Mittel","Niedrig"],"uvIndex":[null,0,4,0,4,0,4,0,4,0,4,0],"windDirection":[null,45,18,41,85,74,95,98,114,124,139,131],"windDirectionCardinal":[null,"NO","NNO","NO","O","ONO","O","O","OSO","SO","SO","SO"],"windPhrase":[null,"Wind aus NO mit 2 bis 4 m/s.","Wind aus NNO mit 2 bis 4 m/s.","Wind aus NO mit 2 bis 4 m/s.","Wind aus O und wechselhaft.","Wind aus ONO und wechselhaft.","Wind aus O mit 4 bis 9 m/s.","Wind aus O mit 4 bis 9 m/s.","Wind aus OSO mit 9 bis 13 m/s.","Wind aus SO mit 4 bis 9 m/s.","Wind aus SO mit 4 bis 9 m/s.","Wind aus SO mit 2 bis 4 m/s."],"windSpeed":[null,4,3,3,2,2,6,6,9,7,6,4],"wxPhraseLong":[null,"Klar","Meist sonnig","Meist klar","Sonnig","Klar","Meist sonnig","Meist klar","Teilweise bedeckt/Wind","Wolkig","Wolkig","Wolkig"],"wxPhraseShort":[null,"","","","","","","","","","",""]}]},"observations":[{"stationID":"IMUNICH344","obsTimeUtc":"2019-04-19T15:24:22Z","obsTimeLocal":"2019-04-19 17:24:22","neighborhood":"Am Hartmannshofer Baechl 34","softwareType":"weewx-3.8.2","country":"DE","solarRadiation":null,"lon":11.49312592,"realtimeFrequency":null,"epoch":1555687462,"lat":48.18364716,"uv":null,"winddir":null,"humidity":27,"qcStatus":1,"metric_si":{"temp":23,"heatIndex":22,"dewpt":3,"windChill":23,"windSpeed":0,"windGust":1,"pressure":1025.84,"precipRate":0.0,"precipTotal":0.0,"elev":502}}]}'; -use constant URL => 'https://api.weather.com/'; +Readonly my $URL => 'https://api.weather.com/'; +### begin public function sub new { my ( $class, $argsRef ) = @_; - my $apioptions = parseApiOptions( $argsRef->{apioptions} ); + my $apioptions = _parseApiOptions( $argsRef->{apioptions} ); my $self = { devName => $argsRef->{devName}, @@ -125,23 +136,6 @@ sub new { return $self; } -sub parseApiOptions($) { - my $apioptions = shift; - - my @params; - my %h; - - @params = split( ',', $apioptions ); - while (@params) { - my $param = shift(@params); - next if ( $param eq '' ); - my ( $key, $value ) = split( ':', $param, 2 ); - $h{$key} = $value; - } - - return \%h; -} - sub setFetchTime { my $self = shift; @@ -189,7 +183,29 @@ sub getWeather { return $self->{cached}; } -sub _RetrieveDataFromWU($) { +### begin privat function +sub _parseApiOptions { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $apioptions = shift; + + my @params; + my %h; + + @params = split( ',', $apioptions ); + while (@params) { + my $param = shift(@params); + next if ( $param eq '' ); + my ( $key, $value ) = split( ':', $param, 2 ); + $h{$key} = $value; + } + + return \%h; +} + +sub _RetrieveDataFromWU { + return 0 unless ( __PACKAGE__ eq caller(0) ); + my $self = shift; # retrieve data from cache @@ -243,21 +259,27 @@ sub _RetrieveDataFromWU($) { $options .= '&apiKey=' . $self->{key}; $paramRef->{url} = - URL + $URL . 'v3/wx/forecast/daily/' . $self->{days} . 'day' . '?' . $options; if ( lc( $self->{key} ) eq 'demo' ) { - _RetrieveDataFinished( $paramRef, undef, 'DEMODATA' . DEMODATA ); + _RetrieveDataFinished( $paramRef, undef, 'DEMODATA' . $DEMODATA ); } - else { main::HttpUtils_NonblockingGet($paramRef); } + else { ::HttpUtils_NonblockingGet($paramRef); } } + + return; } -sub _RetrieveDataFromPWS($$$) { - my ( $paramRef, $err, $response ) = @_; - my $self = $paramRef->{self}; +sub _RetrieveDataFromPWS { + return 0 unless ( 'main' eq caller(0) ); + + my $paramRef = shift; + my $err = shift; + my $response = shift; + my $self = $paramRef->{self}; my $paramRefPWS = { timeout => 15, @@ -276,14 +298,18 @@ sub _RetrieveDataFromPWS($$$) { $options .= '&numericPrecision=decimal'; $options .= '&apiKey=' . $self->{key}; - $paramRefPWS->{url} = URL . 'v2/pws/observations/current?' . $options; + $paramRefPWS->{url} = $URL . 'v2/pws/observations/current?' . $options; - main::HttpUtils_NonblockingGet($paramRefPWS); + return ::HttpUtils_NonblockingGet($paramRefPWS); } -sub _RetrieveDataFinished($$$) { - my ( $paramRef, $err, $data ) = @_; - my $self = $paramRef->{self}; +sub _RetrieveDataFinished { + return 0 unless ( 'main' eq caller(0) ); + + my $paramRef = shift; + my $err = shift; + my $data = shift; + my $self = $paramRef->{self}; my $response; # we got PWS and forecast data @@ -292,10 +318,10 @@ sub _RetrieveDataFinished($$$) { $err = 'No Data Found for specific PWS' unless ($err); $response = $paramRef->{forecast}; } - elsif ( $paramRef->{forecast} =~ m/^\{(.*)\}$/ ) { + elsif ( $paramRef->{forecast} =~ m/^\{(.*)\}$/x ) { my $fc = $1; - if ( $data =~ m/^\{(.*)\}$/ ) { + if ( $data =~ m/^\{(.*)\}$/x ) { $response = '{' . $fc . ',' . $1 . '}'; } else { @@ -310,7 +336,7 @@ sub _RetrieveDataFinished($$$) { } # just demo data - elsif ( $data =~ m/^DEMODATA(\{.*\})$/ ) { + elsif ( $data =~ m/^DEMODATA(\{.*\})$/x ) { $response = $1; } @@ -321,7 +347,8 @@ sub _RetrieveDataFinished($$$) { if ( !$err ) { $self->{cached}{status} = 'ok'; - $self->{cached}{validity} = 'up-to-date', $self->{fetchTime} = time(); + $self->{cached}{validity} = 'up-to-date'; + $self->{fetchTime} = time(); _ProcessingRetrieveData( $self, $response ); } else { @@ -329,10 +356,15 @@ sub _RetrieveDataFinished($$$) { _ErrorHandling( $self, $err ); _ProcessingRetrieveData( $self, $response ); } + + return; } -sub _ProcessingRetrieveData($$) { - my ( $self, $response ) = @_; +sub _ProcessingRetrieveData { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $response = shift; if ( $self->{cached}{status} eq 'ok' and defined($response) @@ -358,7 +390,7 @@ sub _ProcessingRetrieveData($$) { # print Dumper $data; ## für Debugging $self->{cached}{current_date_time} = - strftimeWrapper( "%a, %e %b %Y %H:%M", + _strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( $self->{fetchTime} ) ); # $self->{cached}{timezone} = $data->{timezone}; @@ -415,7 +447,7 @@ sub _ProcessingRetrieveData($$) { 'solarRadiation' => $data->{solarRadiation}, 'uvIndex' => $data->{uv}, 'humidity' => $data->{humidity}, - 'pubDate' => strftimeWrapper( + 'pubDate' => _strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( main::time_str2num( $data->{obsTimeLocal} ) @@ -444,12 +476,14 @@ sub _ProcessingRetrieveData($$) { { ### löschen des alten Datensatzes delete $self->{cached}{forecast}; - - my $data = + my $data; + $data = exists( $data->{daily} ) ? $data->{daily} : $data; my $days = scalar @{ $data->{temperatureMin} }; - my $i = 0; + my $i; + $i = 0; + while ( $i < $days ) { $data->{moonriseTimeLocal}[$i] =~ s/^(....-..-..T..:..).*/$1/; @@ -467,7 +501,7 @@ sub _ProcessingRetrieveData($$) { 'moonPhase' => $data->{moonPhase}[$i], 'moonPhaseCode' => $data->{moonPhaseCode}[$i], 'moonPhaseDay' => $data->{moonPhaseDay}[$i], - 'moonriseTime' => strftimeWrapper( + 'moonriseTime' => _strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( main::time_str2num( @@ -475,7 +509,7 @@ sub _ProcessingRetrieveData($$) { ) ) ), - 'moonsetTime' => strftimeWrapper( + 'moonsetTime' => _strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( main::time_str2num( @@ -486,7 +520,7 @@ sub _ProcessingRetrieveData($$) { 'narrative' => $data->{narrative}[$i], 'precipProbability' => $data->{qpf}[$i], 'precipProbabilitySnow' => $data->{qpfSnow}[$i], - 'sunriseTime' => strftimeWrapper( + 'sunriseTime' => _strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( main::time_str2num( @@ -494,7 +528,7 @@ sub _ProcessingRetrieveData($$) { ) ) ), - 'sunsetTime' => strftimeWrapper( + 'sunsetTime' => _strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( main::time_str2num( @@ -537,10 +571,10 @@ sub _ProcessingRetrieveData($$) { and ref( $data->{daypart}[0]{daypartName} ) eq "ARRAY" and scalar @{ $data->{daypart}[0]{daypartName} } > 0 ) { - my $data = $data->{daypart}[0]; + $data = $data->{daypart}[0]; my $dayparts = scalar @{ $data->{daypartName} }; - my $i = 0; + $i = 0; my $day = 0; while ( $i < $dayparts ) { @@ -678,26 +712,35 @@ sub _ProcessingRetrieveData($$) { } ## Aufruf der callbackFn - _CallWeatherCallbackFn($self); + return _CallWeatherCallbackFn($self); } -sub _CallWeatherCallbackFn($) { +sub _CallWeatherCallbackFn { + return 0 unless ( __PACKAGE__ eq caller(0) ); + my $self = shift; # ## Aufruf der callbackFn - main::Weather_RetrieveCallbackFn( $self->{devName} ); + return ::Weather_RetrieveCallbackFn( $self->{devName} ); } -sub _ErrorHandling($$) { - my ( $self, $err ) = @_; +sub _ErrorHandling { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $self = shift; + my $err = shift; $self->{cached}{current_date_time} = - strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( $self->{fetchTime} ) ), - $self->{cached}{status} = $err; + _strftimeWrapper( "%a, %e %b %Y %H:%M", localtime( $self->{fetchTime} ) ); + $self->{cached}{status} = $err; $self->{cached}{validity} = 'stale'; + + return; } -sub _CreateForecastRef($) { +sub _CreateForecastRef { + return 0 unless ( __PACKAGE__ eq caller(0) ); + my $self = shift; my $forecastRef = ( @@ -705,28 +748,31 @@ sub _CreateForecastRef($) { lat => $self->{lat}, long => $self->{long}, apiMaintainer => 'Julian Pawlowski (loredo)', - apiVersion => wundergroundAPI->VERSION(), + apiVersion => version->parse( __PACKAGE__->VERSION() )->normal, } ); return $forecastRef; } -sub strftimeWrapper(@) { - my $string = POSIX::strftime(@_); +sub _strftimeWrapper { + my @data = @_; + return 0 unless ( __PACKAGE__ eq caller(0) ); - $string =~ s/\xe4/ä/g; - $string =~ s/\xc4/Ä/g; - $string =~ s/\xf6/ö/g; - $string =~ s/\xd6/Ö/g; - $string =~ s/\xfc/ü/g; - $string =~ s/\xdc/Ü/g; - $string =~ s/\xdf/ß/g; - $string =~ s/\xdf/ß/g; - $string =~ s/\xe1/á/g; - $string =~ s/\xe9/é/g; - $string =~ s/\xc1/Á/g; - $string =~ s/\xc9/É/g; + my $string = POSIX::strftime(@data); + + $string =~ s/\xe4/ä/xg; + $string =~ s/\xc4/Ä/xg; + $string =~ s/\xf6/ö/xg; + $string =~ s/\xd6/Ö/xg; + $string =~ s/\xfc/ü/xg; + $string =~ s/\xdc/Ü/xg; + $string =~ s/\xdf/ß/xg; + $string =~ s/\xdf/ß/xg; + $string =~ s/\xe1/á/xg; + $string =~ s/\xe9/é/xg; + $string =~ s/\xc1/Á/xg; + $string =~ s/\xc9/É/xg; return $string; }