diff --git a/CHANGELOG.md b/CHANGELOG.md index 925089c..5620e3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,63 @@ -### fix: bugfix function WeatherAsHtmlD not working (HEAD -> patch-WeatherAsHtmlD) +### build: v2.2.22 (HEAD -> patch-package) +>Tue, 10 Jan 2023 21:43:26 +0100 + +>Author: Marko Oldenburg (fhemdevelopment@cooltux.net) + +>Commiter: Marko Oldenburg (fhemdevelopment@cooltux.net) + + + + +### docs: add new modul path in to pre-commit (origin/patch-package) +>Tue, 10 Jan 2023 21:37:10 +0100 + +>Author: Marko Oldenburg (fhemdevelopment@cooltux.net) + +>Commiter: Marko Oldenburg (fhemdevelopment@cooltux.net) + +add new modul path in to pre-commit hook file + + + +### revert: mod packages +>Tue, 10 Jan 2023 21:32:56 +0100 + +>Author: Marko Oldenburg (fhemdevelopment@cooltux.net) + +>Commiter: Marko Oldenburg (fhemdevelopment@cooltux.net) + +- packages Weather.pm +- split FHEM/59_Weather.pm in two files and packages (FEHM/59_Weather.pm +and FHEM/Core/Weather.pm + +[optional body] + +[Ticket: no] + + + +### fix: #40 +>Tue, 10 Jan 2023 16:09:53 +0100 + +>Author: Marko Oldenburg (fhemdevelopment@cooltux.net) + +>Commiter: Marko Oldenburg (fhemdevelopment@cooltux.net) + +Undefined subroutine &FHEM::APIs::Weather::DarkSkyAPI::__strftimeWrapper + + + +### docs: add new changelog and change version (tag: v2.2.21) +>Sun, 8 Jan 2023 21:56:15 +0100 + +>Author: Marko Oldenburg (fhemdevelopment@cooltux.net) + +>Commiter: Marko Oldenburg (fhemdevelopment@cooltux.net) + + + + +### fix: bugfix function WeatherAsHtmlD not working >Sun, 8 Jan 2023 21:54:31 +0100 >Author: Marko Oldenburg (fhemdevelopment@cooltux.net) diff --git a/FHEM/59_Weather.pm b/FHEM/59_Weather.pm index 440d2cb..b6ca7c5 100755 --- a/FHEM/59_Weather.pm +++ b/FHEM/59_Weather.pm @@ -27,1106 +27,42 @@ # ############################################################################## -package main; +package FHEM::Weather; use strict; use warnings; -use Time::HiRes qw(gettimeofday); -use experimental qw /switch/; -use Readonly; + +require FHEM::Core::Weather; use FHEM::Meta; -use vars qw($FW_ss); -# use Data::Dumper; # for Debug only +sub ::Weather_Initialize { goto &Initialize } -my %pressure_trend_txt_en = ( 0 => "steady", 1 => "rising", 2 => "falling" ); -my %pressure_trend_txt_de = - ( 0 => "gleichbleibend", 1 => "steigend", 2 => "fallend" ); -my %pressure_trend_txt_nl = ( 0 => "stabiel", 1 => "stijgend", 2 => "dalend" ); -my %pressure_trend_txt_fr = - ( 0 => "stable", 1 => "croissant", 2 => "décroissant" ); -my %pressure_trend_txt_pl = ( 0 => "stabilne", 1 => "rośnie", 2 => "spada" ); -my %pressure_trend_txt_it = - ( 0 => "stabile", 1 => "in aumento", 2 => "in diminuzione" ); -my %pressure_trend_sym = ( 0 => "=", 1 => "+", 2 => "-" ); - -my @directions_txt_en = ( - 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', - 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' -); -my @directions_txt_de = ( - 'N', 'NNO', 'NO', 'ONO', 'O', 'OSO', 'SO', 'SSO', - 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' -); -my @directions_txt_nl = ( - 'N', 'NNO', 'NO', 'ONO', 'O', 'OZO', 'ZO', 'ZZO', - 'Z', 'ZZW', 'ZW', 'WZW', 'W', 'WNW', 'NW', 'NNW' -); -my @directions_txt_fr = ( - 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', - 'S', 'SSO', 'SO', 'OSO', 'O', 'ONO', 'NO', 'NNO' -); -my @directions_txt_pl = ( - 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', - 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' -); -my @directions_txt_it = ( - 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', - 'S', 'SSO', 'SO', 'OSO', 'O', 'ONO', 'NO', 'NNO' -); - -my %wdays_txt_en = ( - 'Mon' => 'Mon', - 'Tue' => 'Tue', - 'Wed' => 'Wed', - 'Thu' => 'Thu', - 'Fri' => 'Fri', - 'Sat' => 'Sat', - 'Sun' => 'Sun' -); -my %wdays_txt_de = ( - 'Mon' => 'Mo', - 'Tue' => 'Di', - 'Wed' => 'Mi', - 'Thu' => 'Do', - 'Fri' => 'Fr', - 'Sat' => 'Sa', - 'Sun' => 'So' -); -my %wdays_txt_nl = ( - 'Mon' => 'Ma', - 'Tue' => 'Di', - 'Wed' => 'Wo', - 'Thu' => 'Do', - 'Fri' => 'Vr', - 'Sat' => 'Za', - 'Sun' => 'Zo' -); -my %wdays_txt_fr = ( - 'Mon' => 'Lun', - 'Tue' => 'Mar', - 'Wed' => 'Mer', - 'Thu' => 'Jeu', - 'Fri' => 'Ven', - 'Sat' => 'Sam', - 'Sun' => 'Dim' -); -my %wdays_txt_pl = ( - 'Mon' => 'Pon', - 'Tue' => 'Wt', - 'Wed' => 'Śr', - 'Thu' => 'Czw', - 'Fri' => 'Pt', - 'Sat' => 'Sob', - 'Sun' => 'Nie' -); -my %wdays_txt_it = ( - 'Mon' => 'Lun', - 'Tue' => 'Mar', - 'Wed' => 'Mer', - 'Thu' => 'Gio', - 'Fri' => 'Ven', - 'Sat' => 'Sab', - 'Sun' => 'Dom' -); - -my %status_items_txt_en = ( - 0 => "Wind", - 1 => "Humidity", - 2 => "Temperature", - 3 => "Right Now", - 4 => "Weather forecast for " -); -my %status_items_txt_de = ( - 0 => "Wind", - 1 => "Feuchtigkeit", - 2 => "Temperatur", - 3 => "Jetzt Sofort", - 4 => "Wettervorhersage für " -); -my %status_items_txt_nl = ( - 0 => "Wind", - 1 => "Vochtigheid", - 2 => "Temperatuur", - 3 => "Actueel", - 4 => "Weersvoorspelling voor " -); -my %status_items_txt_fr = ( - 0 => "Vent", - 1 => "Humidité", - 2 => "Température", - 3 => "Maintenant", - 4 => "Prévisions météo pour " -); -my %status_items_txt_pl = ( - 0 => "Wiatr", - 1 => "Wilgotność", - 2 => "Temperatura", - 3 => "Teraz", - 4 => "Prognoza pogody w " -); -my %status_items_txt_it = ( - 0 => "Vento", - 1 => "Umidità", - 2 => "Temperatura", - 3 => "Adesso", - 4 => "Previsioni del tempo per " -); - -my %wdays_txt_i18n; -my @directions_txt_i18n; -my %pressure_trend_txt_i18n; -my %status_items_txt_i18n; - -my @iconlist = ( - 'storm', 'storm', - 'storm', 'thunderstorm', - 'thunderstorm', 'rainsnow', - 'sleet', 'snow', - 'drizzle', 'drizzle', - 'icy', 'chance_of_rain', - 'chance_of_rain', 'snowflurries', - 'chance_of_snow', 'heavysnow', - 'snow', 'sleet', - 'sleet', 'dust', - 'fog', 'haze', - 'smoke', 'flurries', - 'windy', 'icy', - 'cloudy', 'mostlycloudy_night', - 'mostlycloudy', 'partly_cloudy_night', - 'partly_cloudy', 'sunny', - 'sunny', 'mostly_clear_night', - 'mostly_sunny', 'heavyrain', - 'sunny', 'scatteredthunderstorms', - 'scatteredthunderstorms', 'scatteredthunderstorms', - 'scatteredshowers', 'heavysnow', - 'chance_of_snow', 'heavysnow', - 'partly_cloudy', 'heavyrain', - 'chance_of_snow', 'scatteredshowers' -); - -################################### -sub Weather_LanguageInitialize { - my $lang = shift; - - given ($lang) { - when ('de') { - %wdays_txt_i18n = %wdays_txt_de; - @directions_txt_i18n = @directions_txt_de; - %pressure_trend_txt_i18n = %pressure_trend_txt_de; - %status_items_txt_i18n = %status_items_txt_de; - } - - when ('nl') { - %wdays_txt_i18n = %wdays_txt_nl; - @directions_txt_i18n = @directions_txt_nl; - %pressure_trend_txt_i18n = %pressure_trend_txt_nl; - %status_items_txt_i18n = %status_items_txt_nl; - } - - when ('fr') { - %wdays_txt_i18n = %wdays_txt_fr; - @directions_txt_i18n = @directions_txt_fr; - %pressure_trend_txt_i18n = %pressure_trend_txt_fr; - %status_items_txt_i18n = %status_items_txt_fr; - } - - when ('pl') { - %wdays_txt_i18n = %wdays_txt_pl; - @directions_txt_i18n = @directions_txt_pl; - %pressure_trend_txt_i18n = %pressure_trend_txt_pl; - %status_items_txt_i18n = %status_items_txt_pl; - } - - when ('it') { - %wdays_txt_i18n = %wdays_txt_it; - @directions_txt_i18n = @directions_txt_it; - %pressure_trend_txt_i18n = %pressure_trend_txt_it; - %status_items_txt_i18n = %status_items_txt_it; - } - - default { - %wdays_txt_i18n = %wdays_txt_en; - @directions_txt_i18n = @directions_txt_en; - %pressure_trend_txt_i18n = %pressure_trend_txt_en; - %status_items_txt_i18n = %status_items_txt_en; - } - } - - return; -} - -################################### -sub Weather_DebugCodes { - my $lang = shift; - - my @YahooCodes_i18n = YahooWeatherAPI_getYahooCodes($lang); - - Debug "Weather Code List, see http://developer.yahoo.com/weather/#codes"; - for ( my $c = 0 ; $c <= 47 ; $c++ ) { - Debug - sprintf( "%2d %30s %30s", $c, $iconlist[$c], $YahooCodes_i18n[$c] ); - } - - return; -} - -##################################### -sub Weather_Initialize { +sub Initialize { my $hash = shift; - $hash->{DefFn} = \&Weather_Define; - $hash->{UndefFn} = \&Weather_Undef; - $hash->{GetFn} = \&Weather_Get; - $hash->{SetFn} = \&Weather_Set; - $hash->{AttrFn} = \&Weather_Attr; + $hash->{DefFn} = \&FHEM::Core::Weather::Define; + $hash->{UndefFn} = \&FHEM::Core::Weather::Undef; + $hash->{GetFn} = \&FHEM::Core::Weather::Get; + $hash->{SetFn} = \&FHEM::Core::Weather::Set; + $hash->{AttrFn} = \&FHEM::Core::Weather::Attr; $hash->{AttrList} = 'disable:0,1 ' . 'forecast:multiple-strict,hourly,daily ' . 'forecastLimit ' . 'alerts:0,1 ' - . $readingFnAttributes; - $hash->{NotifyFn} = \&Weather_Notify; + . $::readingFnAttributes; + $hash->{NotifyFn} = \&FHEM::Core::Weather::Notify; $hash->{parseParams} = 1; return FHEM::Meta::InitMod( __FILE__, $hash ); } -################################### - -sub degrees_to_direction { - my $degrees = shift; - my $directions_txt_i18n = shift; - - my $mod = int( ( ( $degrees + 11.25 ) % 360 ) / 22.5 ); - return $directions_txt_i18n->[$mod]; -} - -sub Weather_ReturnWithError { - my $hash = shift; - my $responseRef = shift; - - my $name = $hash->{NAME}; - - readingsBeginUpdate($hash); - readingsBulkUpdate( $hash, 'lastError', $responseRef->{status} ); - - foreach my $r ( keys %{$responseRef} ) { - readingsBulkUpdate( $hash, $r, $responseRef->{$r} ) - if ( ref( $responseRef->{$r} ) ne 'HASH' ); - } - readingsBulkUpdate( $hash, 'state', - 'API Maintainer: ' - . $responseRef->{apiMaintainer} - . ' ErrorMsg: ' - . $responseRef->{status} ); - readingsEndUpdate( $hash, 1 ); - - my $next = 60; # $next= $hash->{INTERVAL}; - Weather_RearmTimer( $hash, gettimeofday() + $next ); - - return; -} - -sub Weather_DeleteForecastReadings { - my $hash = shift; - - my $name = $hash->{NAME}; - my $forecastConfig = Weather_ForcastConfig($hash); - my $forecastLimit = AttrVal( $name, 'forecastLimit', 5 ) + 1; - my $forecastLimitNoForecast = 1; - - $forecastLimit = $forecastLimitNoForecast - if ( !$forecastConfig->{daily} ); - CommandDeleteReading( undef, - $name . ' ' . 'fc([' . $forecastLimit . '-9]|[0-9]{2})_.*' ); - - $forecastLimit = $forecastLimitNoForecast - if ( !$forecastConfig->{hourly} ); - CommandDeleteReading( undef, - $name . ' ' . 'hfc([' . $forecastLimit . '-9]|[0-9]{2})_.*' ); - - return; -} - -sub Weather_DeleteAlertsReadings { - my $hash = shift; - my $alertsLimit = shift // 0; - - my $name = $hash->{NAME}; - my $alertsConfig = Weather_ForcastConfig($hash); - my $alertsLimitNoAlerts = 0; - - $alertsLimit = $alertsLimitNoAlerts - if ( !$alertsConfig->{alerts} ); - - CommandDeleteReading( undef, - $name . ' ' . 'warn_([' . $alertsLimit . '-9]|[0-9]{2})_.*' ); - - return; -} - -sub Weather_RetrieveCallbackFn { - my $name = shift; - - return - unless ( IsDevice($name) ); - - my $hash = $defs{$name}; - my $responseRef = $hash->{fhem}->{api}->getWeather; - - if ( $responseRef->{status} eq 'ok' ) { - Weather_WriteReadings( $hash, $responseRef ); - } - else { - Weather_ReturnWithError( $hash, $responseRef ); - } - - return; -} - -sub Weather_ForcastConfig { - my $hash = shift; - - my $name = $hash->{NAME}; - my %forecastConfig; - - $forecastConfig{hourly} = - ( AttrVal( $name, 'forecast', '' ) =~ m{hourly}xms ? 1 : 0 ); - - $forecastConfig{daily} = - ( AttrVal( $name, 'forecast', '' ) =~ m{daily}xms ? 1 : 0 ); - - $forecastConfig{alerts} = AttrVal( $name, 'alerts', 0 ); - - return \%forecastConfig; -} - -sub Weather_WriteReadings { - my $hash = shift; - my $dataRef = shift; - - my $forecastConfig = Weather_ForcastConfig($hash); - my $name = $hash->{NAME}; - - readingsBeginUpdate($hash); - - # housekeeping information - readingsBulkUpdate( $hash, 'lastError', '' ); - foreach my $r ( keys %{$dataRef} ) { - readingsBulkUpdate( $hash, $r, $dataRef->{$r} ) - if ( ref( $dataRef->{$r} ) ne 'HASH' - && ref( $dataRef->{$r} ) ne 'ARRAY' ); - readingsBulkUpdate( $hash, '.license', $dataRef->{license}->{text} ); - } - - ### current - if ( defined( $dataRef->{current} ) - && ref( $dataRef->{current} ) eq 'HASH' ) - { - while ( my ( $r, $v ) = each %{ $dataRef->{current} } ) { - readingsBulkUpdate( $hash, $r, $v ) - if ( ref( $dataRef->{$r} ) ne 'HASH' - && ref( $dataRef->{$r} ) ne 'ARRAY' ); - } - - readingsBulkUpdate( $hash, 'icon', - $iconlist[ $dataRef->{current}->{code} ] ); - if ( defined( $dataRef->{current}->{wind_direction} ) - && $dataRef->{current}->{wind_direction} - && defined( $dataRef->{current}->{wind_speed} ) - && $dataRef->{current}->{wind_speed} ) - { - my $wdir = - degrees_to_direction( $dataRef->{current}->{wind_direction}, - \@directions_txt_i18n ); - readingsBulkUpdate( $hash, 'wind_condition', - 'Wind: ' - . $wdir . ' ' - . $dataRef->{current}->{wind_speed} - . ' km/h' ); - } - } - - ### forecast - if ( ref( $dataRef->{forecast} ) eq 'HASH' - && ( $forecastConfig->{hourly} || $forecastConfig->{daily} ) ) - { - ## hourly - if ( defined( $dataRef->{forecast}->{hourly} ) - && ref( $dataRef->{forecast}->{hourly} ) eq 'ARRAY' - && scalar( @{ $dataRef->{forecast}->{hourly} } ) > 0 - && $forecastConfig->{hourly} ) - { - my $i = 0; - my $limit = AttrVal( $name, 'forecastLimit', 5 ); - foreach my $fc ( @{ $dataRef->{forecast}->{hourly} } ) { - $i++; - my $f = "hfc" . $i . "_"; - - while ( my ( $r, $v ) = each %{$fc} ) { - readingsBulkUpdate( $hash, $f . $r, $v ) - if ( ref( $dataRef->{$r} ) ne 'HASH' - && ref( $dataRef->{$r} ) ne 'ARRAY' ); - } - readingsBulkUpdate( - $hash, - $f . 'icon', - $iconlist[ $dataRef->{forecast}->{hourly}[ $i - 1 ]{code} ] - ); - - if ( - defined( - $dataRef->{forecast}->{hourly}[ $i - 1 ]{wind_direction} - ) - && $dataRef->{forecast}->{hourly}[ $i - 1 ]{wind_direction} - && defined( - $dataRef->{forecast}->{hourly}[ $i - 1 ]{wind_speed} - ) - && $dataRef->{forecast}->{hourly}[ $i - 1 ]{wind_speed} - ) - { - my $wdir = degrees_to_direction( - $dataRef->{forecast} - ->{hourly}[ $i - 1 ]{wind_direction}, - \@directions_txt_i18n - ); - readingsBulkUpdate( - $hash, - $f . 'wind_condition', - 'Wind: ' - . $wdir . ' ' - . $dataRef->{forecast}->{hourly}[ $i - 1 ]{wind_speed} - . ' km/h' - ); - } - - last if ( $i == $limit && $limit > 0 ); - } - } - - ## daily - if ( defined( $dataRef->{forecast}->{daily} ) - && ref( $dataRef->{forecast}->{daily} ) eq 'ARRAY' - && scalar( @{ $dataRef->{forecast}->{daily} } ) > 0 - && $forecastConfig->{daily} ) - { - my $i = 0; - my $limit = AttrVal( $name, 'forecastLimit', 5 ); - foreach my $fc ( @{ $dataRef->{forecast}->{daily} } ) { - $i++; - my $f = "fc" . $i . "_"; - - while ( my ( $r, $v ) = each %{$fc} ) { - readingsBulkUpdate( $hash, $f . $r, $v ) - if ( ref( $dataRef->{$r} ) ne 'HASH' - && ref( $dataRef->{$r} ) ne 'ARRAY' ); - } - readingsBulkUpdate( - $hash, - $f . 'icon', - $iconlist[ $dataRef->{forecast}->{daily}[ $i - 1 ]{code} ] - ); - - if ( - defined( - $dataRef->{forecast}->{daily}[ $i - 1 ]{wind_direction} - ) - && $dataRef->{forecast}->{daily}[ $i - 1 ]{wind_direction} - && defined( - $dataRef->{forecast}->{daily}[ $i - 1 ]{wind_speed} - ) - && $dataRef->{forecast}->{daily}[ $i - 1 ]{wind_speed} - ) - { - my $wdir = degrees_to_direction( - $dataRef->{forecast}->{daily}[ $i - 1 ]{wind_direction}, - \@directions_txt_i18n - ); - readingsBulkUpdate( - $hash, - $f . 'wind_condition', - 'Wind: ' - . $wdir . ' ' - . $dataRef->{forecast}->{daily}[ $i - 1 ]{wind_speed} - . ' km/h' - ); - } - - last if ( $i == $limit && $limit > 0 ); - } - } - } - - ### alerts - if ( defined( $dataRef->{alerts} ) - && ref( $dataRef->{alerts} ) eq 'ARRAY' - && scalar( @{ $dataRef->{alerts} } ) > 0 - && $forecastConfig->{alerts} ) - { - my $i = 0; - foreach my $warn ( @{ $dataRef->{alerts} } ) { - my $w = "warn_" . $i . "_"; - - while ( my ( $r, $v ) = each %{$warn} ) { - readingsBulkUpdate( $hash, $w . $r, $v ) - if ( ref( $dataRef->{$r} ) ne 'HASH' - && ref( $dataRef->{$r} ) ne 'ARRAY' ); - } - - $i++; - } - - Weather_DeleteAlertsReadings( $hash, - scalar( @{ $dataRef->{alerts} } ) ); - readingsBulkUpdate( $hash, 'warnCount', - scalar( @{ $dataRef->{alerts} } ) ); - } - else { - Weather_DeleteAlertsReadings($hash); - readingsBulkUpdate( $hash, 'warnCount', - scalar( @{ $dataRef->{alerts} } ) ) - if ( defined( $dataRef->{alerts} ) - && ref( $dataRef->{alerts} ) eq 'ARRAY' ); - } - - ### state - my $val = 'T: ' - . $dataRef->{current}->{temperature} . ' °C' . ' ' - . substr( $status_items_txt_i18n{1}, 0, 1 ) . ': ' - . $dataRef->{current}->{humidity} . ' %' . ' ' - . substr( $status_items_txt_i18n{0}, 0, 1 ) . ': ' - . $dataRef->{current}->{wind} . ' km/h' . ' P: ' - . $dataRef->{current}->{pressure} . ' hPa'; - - Log3 $hash, 4, "$name: $val"; - readingsBulkUpdate( $hash, 'state', $val ); - - readingsEndUpdate( $hash, 1 ); - - Weather_RearmTimer( $hash, gettimeofday() + $hash->{INTERVAL} ); - - return; - -} - -################################### -sub Weather_GetUpdate { - my $hash = shift; - - my $name = $hash->{NAME}; - - if ( IsDisabled($name) ) { - Log3 $hash, 5, - "Weather $name: retrieval of weather data is disabled by attribute."; - readingsBeginUpdate($hash); - readingsBulkUpdate( $hash, "pubDateComment", "disabled by attribute" ); - readingsBulkUpdate( $hash, "validity", "stale" ); - readingsEndUpdate( $hash, 1 ); - Weather_RearmTimer( $hash, gettimeofday() + $hash->{INTERVAL} ); - } - else { - $hash->{fhem}->{api}->setRetrieveData; - } - - return; -} - -################################### -sub Weather_Get { - my $hash = shift // return; - my $aRef = shift // return; - - my $name = shift @$aRef // return; - my $reading = shift @$aRef // return; - my $value; - - if ( defined( $hash->{READINGS}->{$reading} ) ) { - $value = $hash->{READINGS}->{$reading}->{VAL}; - } - else { - my $rt = ''; - if ( defined( $hash->{READINGS} ) ) { - $rt = join( ":noArg ", sort keys %{ $hash->{READINGS} } ); - } - - return "Unknown reading $reading, choose one of " . $rt; - } - - return "$name $reading => $value"; -} - -################################### -sub Weather_Set { - my $hash = shift // return; - my $aRef = shift // return; - - my $name = shift @$aRef // return; - my $cmd = shift @$aRef - // return qq{"set $name" needs at least one argument}; - - # usage check - if ( scalar( @{$aRef} ) == 0 - && $cmd eq 'update' ) - { - Weather_DisarmTimer($hash); - Weather_GetUpdate($hash); - - return; - } - elsif ( scalar( @{$aRef} ) == 1 - && $cmd eq "newLocation" ) - { - if ( $hash->{API} eq 'DarkSkyAPI' - || $hash->{API} eq 'OpenWeatherMapAPI' - || $hash->{API} eq 'wundergroundAPI' ) - { - my ( $lat, $long ); - ( $lat, $long ) = split( ',', $aRef->[0] ) - if ( defined( $aRef->[0] ) && $aRef->[0] ); - ( $lat, $long ) = split( ',', $hash->{fhem}->{LOCATION} ) - unless ( defined($lat) - && defined($long) - && $lat =~ m{(-?\d+(\.\d+)?)}xms - && $long =~ m{(-?\d+(\.\d+)?)}xms ); - - $hash->{fhem}->{api}->setLocation( $lat, $long ); - Weather_DisarmTimer($hash); - Weather_GetUpdate($hash); - return; - } - else { return 'this API is not ' . $aRef->[0] . ' supported' } - } - else { - return "Unknown argument $cmd, choose one of update:noArg newLocation"; - } -} - -################################### -sub Weather_RearmTimer { - my $hash = shift; - my $t = shift; - - Log3( $hash, 4, "Weather $hash->{NAME}: Rearm new Timer" ); - InternalTimer( $t, "Weather_GetUpdate", $hash, 0 ); - - return; -} - -sub Weather_DisarmTimer { - my $hash = shift; - - RemoveInternalTimer($hash); - - return; -} - -sub Weather_Notify { - my $hash = shift; - my $dev = shift; - - my $name = $hash->{NAME}; - my $type = $hash->{TYPE}; - - return if ( $dev->{NAME} ne "global" ); - - # set forcast and alerts values to api object - if ( grep { /^MODIFIED.$name$/x } @{ $dev->{CHANGED} } ) { - $hash->{fhem}->{api}->setForecast( AttrVal( $name, 'forecast', '' ) ); - $hash->{fhem}->{api}->setAlerts( AttrVal( $name, 'alerts', 0 ) ); - - Weather_GetUpdate($hash); - } - - return - if ( - !grep { -/^INITIALIZED|REREADCFG|DELETEATTR.$name.disable|ATTR.$name.disable.[0-1]$/x - } @{ $dev->{CHANGED} } - ); - - # update weather after initialization or change of configuration - # wait 10 to 29 seconds to avoid congestion due to concurrent activities - Weather_DisarmTimer($hash); - my $delay = 10 + int( rand(20) ); - - Log3 $hash, 5, -"Weather $name: FHEM initialization or rereadcfg triggered update, delay $delay seconds."; - Weather_RearmTimer( $hash, gettimeofday() + $delay ); - - ### quick run GetUpdate then Demo - Weather_GetUpdate($hash) - if ( defined( $hash->{APIKEY} ) && lc( $hash->{APIKEY} ) eq 'demo' ); - - return; -} - -##################################### -sub Weather_Define { - my $hash = shift // return; - my $aRef = shift // return; - my $hRef = shift // undef; - - return $@ unless ( FHEM::Meta::SetInternals($hash) ); - use version 0.60; our $VERSION = FHEM::Meta::Get( $hash, 'version' ); - - my $usage = -"syntax: define Weather [API=] [apikey=] [location=] [interval=] [lang=]"; - - # check minimum syntax - return $usage unless ( scalar @{$aRef} == 2 ); - my $name = $aRef->[0]; - - my $location = $hRef->{location} // undef; - my $apikey = $hRef->{apikey} // undef; - my $lang = $hRef->{lang} // undef; - my $interval = $hRef->{interval} // 3600; - my $API = $hRef->{API} // "DarkSkyAPI,cachemaxage:600"; - - # evaluate API options - my ( $api, $apioptions ) = split( ',', $API, 2 ); - $apioptions = "" unless ( defined($apioptions) ); - eval { require 'FHEM/APIs/Weather/' . $api . '.pm'; }; - return "$name: cannot load API $api: $@" if ($@); - - $hash->{NOTIFYDEV} = "global"; - $hash->{fhem}->{interfaces} = "temperature;humidity;wind"; - $hash->{fhem}->{LOCATION} = ( - ( defined($location) && $location ) - ? $location - : AttrVal( 'global', 'latitude', 'error' ) . ',' - . AttrVal( 'global', 'longitude', 'error' ) - ); - $hash->{INTERVAL} = $interval; - $hash->{LANG} = ( - ( defined($lang) && $lang ) - ? $lang - : lc( AttrVal( 'global', 'language', 'de' ) ) - ); - $hash->{API} = $api; - $hash->{MODEL} = $api; - $hash->{APIKEY} = $apikey; - $hash->{APIOPTIONS} = $apioptions; - $hash->{VERSION} = version->parse($VERSION)->normal; - $hash->{fhem}->{allowCache} = 1; - - readingsSingleUpdate( $hash, 'current_date_time', TimeNow(), 0 ); - readingsSingleUpdate( $hash, 'current_date_time', 'none', 0 ); - - readingsSingleUpdate( $hash, 'state', 'Initialized', 1 ); - Weather_LanguageInitialize( $hash->{LANG} ); - - my $apistring = 'FHEM::APIs::Weather::' . $api; - $hash->{fhem}->{api} = $apistring->new( - { - devName => $hash->{NAME}, - apikey => $hash->{APIKEY}, - location => $hash->{fhem}->{LOCATION}, - apioptions => $hash->{APIOPTIONS}, - language => $hash->{LANG}, - } - ); - - return; -} - -##################################### -sub Weather_Undef { - my $hash = shift; - my $arg = shift; - - RemoveInternalTimer($hash); - return; -} - -sub Weather_Attr { - my ( $cmd, $name, $attrName, $attrVal ) = @_; - my $hash = $defs{$name}; - - given ($attrName) { - when ('forecast') { - if ( $cmd eq 'set' ) { - $hash->{fhem}->{api}->setForecast($attrVal); - } - elsif ( $cmd eq 'del' ) { - $hash->{fhem}->{api}->setForecast(); - } - - InternalTimer( gettimeofday() + 0.5, - \&Weather_DeleteForecastReadings, $hash ); - } - - when ('forecastLimit') { - InternalTimer( gettimeofday() + 0.5, - \&Weather_DeleteForecastReadings, $hash ); - } - - when ('alerts') { - if ( $cmd eq 'set' ) { - $hash->{fhem}->{api}->setAlerts($attrVal); - } - elsif ( $cmd eq 'del' ) { - $hash->{fhem}->{api}->setAlerts(); - } - - InternalTimer( gettimeofday() + 0.5, - \&Weather_DeleteAlertsReadings, $hash ); - } - } - - return; -} - -##################################### - -# Icon Parameter - -Readonly my $ICONWIDTH => 175; -Readonly my $ICONSCALE => 0.5; - -##################################### - -sub WeatherIconIMGTag { - my $icon = shift; - - my $width = int( $ICONSCALE * $ICONWIDTH ); - my $url = FW_IconURL("weather/$icon"); - my $style = " width=$width"; - - return "\"$icon\""; -} - -##################################### - -sub WeatherAsHtmlV { - my $d = shift; - my $op1 = shift; - my $op2 = shift; - - my ( $f, $items ) = Weather_CheckOptions( $d, $op1, $op2 ); - - my $h = $defs{$d}; - my $width = int( $ICONSCALE * $ICONWIDTH ); - - my $ret = ''; - my $fc; - if ( - defined($f) - && ( $f eq 'h' - || $f eq 'd' ) - ) - { - $fc = ( $f eq 'd' ? 'fc' : 'hfc' ); - } - else { - $fc = ( - ( - defined( $h->{READINGS}->{fc1_day_of_week} ) - && $h->{READINGS}->{fc1_day_of_week} - ) ? 'fc' : 'hfc' - ); - } - - $ret .= sprintf( -'', - $width, - WeatherIconIMGTag( ReadingsVal( $d, "icon", "" ) ), - ReadingsVal( $d, "condition", "" ), - ReadingsVal( $d, "temp_c", "" ), - ReadingsVal( $d, "humidity", "" ), - ReadingsVal( $d, "wind_condition", "" ) - ); - - for ( my $i = 1 ; $i < $items ; $i++ ) { - if ( defined( $h->{READINGS}->{"${fc}${i}_low_c"} ) - && $h->{READINGS}->{"${fc}${i}_low_c"} ) - { - $ret .= sprintf( -'', - $width, - WeatherIconIMGTag( ReadingsVal( $d, "${fc}${i}_icon", "" ) ), - ReadingsVal( $d, "${fc}${i}_day_of_week", "" ), - ReadingsVal( $d, "${fc}${i}_condition", "" ), - ReadingsVal( $d, "${fc}${i}_low_c", " - " ), - ReadingsVal( $d, "${fc}${i}_high_c", " - " ), - ReadingsVal( $d, "${fc}${i}_wind_condition", " - " ) - ); - } - else { - $ret .= sprintf( -'', - $width, - WeatherIconIMGTag( ReadingsVal( $d, "${fc}${i}_icon", "" ) ), - ReadingsVal( $d, "${fc}${i}_day_of_week", "" ), - ReadingsVal( $d, "${fc}${i}_condition", "" ), - ReadingsVal( $d, "${fc}${i}_temperature", " - " ), - ReadingsVal( $d, "${fc}${i}_wind_condition", " - " ) - ); - } - } - - $ret .= "
%s%s
%s°C %s%%
%s
%s%s: %s
min %s°C max %s°C
%s
%s%s: %s
%s°C
%s
"; - return $ret; -} - -sub WeatherAsHtml { - my $d = shift; - my $op1 = shift; - my $op2 = shift; - - my ( $f, $items ) = Weather_CheckOptions( $d, $op1, $op2 ); - - return WeatherAsHtmlV( $d, $f, $items ); -} - -sub WeatherAsHtmlH { - my $d = shift; - my $op1 = shift; - my $op2 = shift; - - my ( $f, $items ) = Weather_CheckOptions( $d, $op1, $op2 ); - - my $h = $defs{$d}; - my $width = int( $ICONSCALE * $ICONWIDTH ); - - my $format = -'
%s
%s
%s°C %s%%
%s
'; - - my $ret = ''; - my $fc; - if ( - defined($f) - && ( $f eq 'h' - || $f eq 'd' ) - ) - { - $fc = ( $f eq 'd' ? 'fc' : 'hfc' ); - } - else { - $fc = ( - ( - defined( $h->{READINGS}->{fc1_day_of_week} ) - && $h->{READINGS}->{fc1_day_of_week} - ) ? 'fc' : 'hfc' - ); - } - - # icons - $ret .= sprintf( '', - $width, WeatherIconIMGTag( ReadingsVal( $d, "icon", "" ) ) ); - for ( my $i = 1 ; $i < $items ; $i++ ) { - $ret .= sprintf( '', - $width, - WeatherIconIMGTag( ReadingsVal( $d, "${fc}${i}_icon", "" ) ) ); - } - $ret .= ''; - - # condition - $ret .= sprintf( '', - ReadingsVal( $d, "condition", "" ) ); - for ( my $i = 1 ; $i < $items ; $i++ ) { - $ret .= sprintf( - '', - ReadingsVal( $d, "${fc}${i}_day_of_week", "" ), - ReadingsVal( $d, "${fc}${i}_condition", "" ) - ); - } - $ret .= ''; - - # temp/hum | min - $ret .= sprintf( - '', - ReadingsVal( $d, "temp_c", "" ), - ReadingsVal( $d, "humidity", "" ) - ); - for ( my $i = 1 ; $i < $items ; $i++ ) { - if ( defined( $h->{READINGS}->{"${fc}${i}_low_c"} ) - && $h->{READINGS}->{"${fc}${i}_low_c"} ) - { - $ret .= sprintf( '', - ReadingsVal( $d, "${fc}${i}_low_c", " - " ) ); - } - else { - $ret .= sprintf( '', - ReadingsVal( $d, "${fc}${i}_temperature", " - " ) ); - } - } - - $ret .= ''; - - # wind | max - $ret .= sprintf( '', - ReadingsVal( $d, "wind_condition", "" ) ); - for ( my $i = 1 ; $i < $items ; $i++ ) { - if ( defined( $h->{READINGS}->{"${fc}${i}_high_c"} ) - && $h->{READINGS}->{"${fc}${i}_high_c"} ) - { - $ret .= sprintf( '', - ReadingsVal( $d, "${fc}${i}_high_c", " - " ) ); - } - } - - $ret .= "
%s%s
%s%s: %s
%s°C %s%%min %s°C %s°C
%smax %s°C
"; - - return $ret; -} - -sub WeatherAsHtmlD { - my $d = shift; - my $op1 = shift; - my $op2 = shift; - - my ( $f, $items ) = Weather_CheckOptions( $d, $op1, $op2 ); - my $ret; - - if ($FW_ss) { - $ret = WeatherAsHtmlV( $d, $f, $items ); - } - else { - $ret = WeatherAsHtmlH( $d, $f, $items ); - } - - return $ret; -} - -sub Weather_CheckOptions { - my $d = shift; - my $op1 = shift; - my $op2 = shift; - - return "$d is not a Weather instance
" - if ( !$defs{$d} || $defs{$d}->{TYPE} ne "Weather" ); - - my $hash = $defs{$d}; - my $items = $op2; - my $f = $op1; - - if ( defined($op1) && $op1 && $op1 =~ m{[0-9]}xms ) { $items = $op1; } - if ( defined($op2) && $op2 && $op2 =~ m{[dh]}xms ) { $f = $op2; } - - $f =~ tr/dh/./cd if ( defined $f && $f ); - $items =~ tr/0-9/./cd if ( defined($items) && $items ); - - $items = AttrVal( $d, 'forecastLimit', 5 ) - if ( !$items ); - - my $forecastConfig = Weather_ForcastConfig($hash); - $f = ( - $forecastConfig->{daily} - ? 'd' - : ( $forecastConfig->{daily} && $forecastConfig->{hourly} ? $f : 'h' ) - ) if !( defined($f) and $f ); - - $f = 'h' if ( !$f || length($f) > 1 ); - - return ( $f, $items + 1 ); -} - -##################################### - 1; +__END__ + + =pod =item device =item summary provides current weather condition and forecast @@ -1563,7 +499,7 @@ sub Weather_CheckOptions { Der Wochentag der Prognose wird in der Sprache Ihres FHEM-Systems angezeigt. Geben Sie zur Überprüfung {$ ENV {LANG}} in die Befehlszeile von FHEM ein. Wenn nichts angezeigt wird oder eine unerwartete Spracheinstellung angezeigt wird, fügen Sie export LANG = de_DE.UTF-8 oder etwas Ähnliches zu Ihrem FHEM-Startskript hinzu. Starten Sie FHEM erneut und überprüfen Sie es erneut. Wenn Sie beim Starten von FHEM eine Ländereinstellung erhalten, fehlt möglicherweise das erforderliche Sprachpaket. Sie kann abhängig von Ihrem Betriebssystem und Ihren Präferenzen installiert werden (z. B. Gebietsschemas dpkg-reconfigure, apt-get install language-pack-de oder ähnliches).
Je nach verwendeter API ist es durchaus möglich, dass weitere - Readings geschrieben werden. Die Bedeutung dieser Readings kann man + readings geschrieben werden. Die Bedeutung dieser readings kann man der API-Beschreibung des Anbieters entnehmen.
@@ -1607,7 +543,7 @@ sub Weather_CheckOptions { ], "release_status": "stable", "license": "GPL_2", - "version": "v2.2.21", + "version": "v2.2.22", "author": [ "Marko Oldenburg " ], diff --git a/controls_Weather.txt b/controls_Weather.txt index f65e2d5..88ff76a 100644 --- a/controls_Weather.txt +++ b/controls_Weather.txt @@ -1,4 +1,5 @@ -UPD 2023-01-08_22:01:24 57800 FHEM/59_Weather.pm -UPD 2023-01-10_16:09:04 50095 lib/FHEM/APIs/Weather/DarkSkyAPI.pm -UPD 2023-01-08_17:15:24 33411 lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm -UPD 2023-01-07_19:00:05 36090 lib/FHEM/APIs/Weather/wundergroundAPI.pm +UPD 2023-01-10_21:41:34 25809 FHEM/59_Weather.pm +UPD 2023-01-10_21:20:58 33948 lib/FHEM/Core/Weather.pm +UPD 2023-01-10_21:25:52 50106 lib/FHEM/APIs/Weather/DarkSkyAPI.pm +UPD 2023-01-10_21:25:49 33422 lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm +UPD 2023-01-10_21:25:53 36101 lib/FHEM/APIs/Weather/wundergroundAPI.pm diff --git a/hooks/pre-commit b/hooks/pre-commit index 4fc1c9a..733d777 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -5,6 +5,7 @@ use POSIX qw(strftime); use strict; my @filenames = ( 'FHEM/59_Weather.pm', + 'lib/FHEM/Core/Weather.pm', 'lib/FHEM/APIs/Weather/DarkSkyAPI.pm', 'lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm', 'lib/FHEM/APIs/Weather/wundergroundAPI.pm', diff --git a/lib/FHEM/APIs/Weather/DarkSkyAPI.pm b/lib/FHEM/APIs/Weather/DarkSkyAPI.pm index 5db3abf..18fd5db 100644 --- a/lib/FHEM/APIs/Weather/DarkSkyAPI.pm +++ b/lib/FHEM/APIs/Weather/DarkSkyAPI.pm @@ -699,7 +699,7 @@ sub _CallWeatherCallbackFn { my $self = shift; # ## Aufruf der callbackFn - return ::Weather_RetrieveCallbackFn( $self->{devName} ); + return FHEM::Core::Weather::RetrieveCallbackFn( $self->{devName} ); } sub _ErrorHandling { diff --git a/lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm b/lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm index c7cc603..bd2a2dc 100644 --- a/lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm +++ b/lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm @@ -951,7 +951,7 @@ sub _CallWeatherCallbackFn { # print 'Dumperausgabe: ' . Dumper $self; ### Aufruf der callbackFn - return ::Weather_RetrieveCallbackFn( $self->{devName} ); + return FHEM::Core::Weather::RetrieveCallbackFn( $self->{devName} ); } sub _ErrorHandling { diff --git a/lib/FHEM/APIs/Weather/wundergroundAPI.pm b/lib/FHEM/APIs/Weather/wundergroundAPI.pm index 8ffe716..1f66e12 100644 --- a/lib/FHEM/APIs/Weather/wundergroundAPI.pm +++ b/lib/FHEM/APIs/Weather/wundergroundAPI.pm @@ -733,7 +733,7 @@ sub _CallWeatherCallbackFn { my $self = shift; # ## Aufruf der callbackFn - return ::Weather_RetrieveCallbackFn( $self->{devName} ); + return FHEM::Core::Weather::RetrieveCallbackFn( $self->{devName} ); } sub _ErrorHandling { diff --git a/lib/FHEM/Core/Weather.pm b/lib/FHEM/Core/Weather.pm new file mode 100644 index 0000000..b65d22e --- /dev/null +++ b/lib/FHEM/Core/Weather.pm @@ -0,0 +1,1140 @@ +# $Id$ +############################################################################## +# +# 59_Weather.pm +# (c) 2009-2023 Copyright by Dr. Boris Neubert +# e-mail: omega at online dot de +# +# Contributors: +# - Marko Oldenburg (CoolTux) +# - Lippie +# +# +# 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 FHEM::Core::Weather; + +use strict; +use warnings; + +use Time::HiRes qw(gettimeofday); +use experimental qw /switch/; +use Readonly; + +use FHEM::Meta; + +use vars qw($FW_ss); + +# use Data::Dumper; # for Debug only + +my %pressure_trend_txt_en = ( 0 => "steady", 1 => "rising", 2 => "falling" ); +my %pressure_trend_txt_de = + ( 0 => "gleichbleibend", 1 => "steigend", 2 => "fallend" ); +my %pressure_trend_txt_nl = ( 0 => "stabiel", 1 => "stijgend", 2 => "dalend" ); +my %pressure_trend_txt_fr = + ( 0 => "stable", 1 => "croissant", 2 => "décroissant" ); +my %pressure_trend_txt_pl = ( 0 => "stabilne", 1 => "rośnie", 2 => "spada" ); +my %pressure_trend_txt_it = + ( 0 => "stabile", 1 => "in aumento", 2 => "in diminuzione" ); +my %pressure_trend_sym = ( 0 => "=", 1 => "+", 2 => "-" ); + +my @directions_txt_en = ( + 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', + 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' +); +my @directions_txt_de = ( + 'N', 'NNO', 'NO', 'ONO', 'O', 'OSO', 'SO', 'SSO', + 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' +); +my @directions_txt_nl = ( + 'N', 'NNO', 'NO', 'ONO', 'O', 'OZO', 'ZO', 'ZZO', + 'Z', 'ZZW', 'ZW', 'WZW', 'W', 'WNW', 'NW', 'NNW' +); +my @directions_txt_fr = ( + 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', + 'S', 'SSO', 'SO', 'OSO', 'O', 'ONO', 'NO', 'NNO' +); +my @directions_txt_pl = ( + 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', + 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' +); +my @directions_txt_it = ( + 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', + 'S', 'SSO', 'SO', 'OSO', 'O', 'ONO', 'NO', 'NNO' +); + +my %wdays_txt_en = ( + 'Mon' => 'Mon', + 'Tue' => 'Tue', + 'Wed' => 'Wed', + 'Thu' => 'Thu', + 'Fri' => 'Fri', + 'Sat' => 'Sat', + 'Sun' => 'Sun' +); +my %wdays_txt_de = ( + 'Mon' => 'Mo', + 'Tue' => 'Di', + 'Wed' => 'Mi', + 'Thu' => 'Do', + 'Fri' => 'Fr', + 'Sat' => 'Sa', + 'Sun' => 'So' +); +my %wdays_txt_nl = ( + 'Mon' => 'Ma', + 'Tue' => 'Di', + 'Wed' => 'Wo', + 'Thu' => 'Do', + 'Fri' => 'Vr', + 'Sat' => 'Za', + 'Sun' => 'Zo' +); +my %wdays_txt_fr = ( + 'Mon' => 'Lun', + 'Tue' => 'Mar', + 'Wed' => 'Mer', + 'Thu' => 'Jeu', + 'Fri' => 'Ven', + 'Sat' => 'Sam', + 'Sun' => 'Dim' +); +my %wdays_txt_pl = ( + 'Mon' => 'Pon', + 'Tue' => 'Wt', + 'Wed' => 'Śr', + 'Thu' => 'Czw', + 'Fri' => 'Pt', + 'Sat' => 'Sob', + 'Sun' => 'Nie' +); +my %wdays_txt_it = ( + 'Mon' => 'Lun', + 'Tue' => 'Mar', + 'Wed' => 'Mer', + 'Thu' => 'Gio', + 'Fri' => 'Ven', + 'Sat' => 'Sab', + 'Sun' => 'Dom' +); + +my %status_items_txt_en = ( + 0 => "Wind", + 1 => "Humidity", + 2 => "Temperature", + 3 => "Right Now", + 4 => "Weather forecast for " +); +my %status_items_txt_de = ( + 0 => "Wind", + 1 => "Feuchtigkeit", + 2 => "Temperatur", + 3 => "Jetzt Sofort", + 4 => "Wettervorhersage für " +); +my %status_items_txt_nl = ( + 0 => "Wind", + 1 => "Vochtigheid", + 2 => "Temperatuur", + 3 => "Actueel", + 4 => "Weersvoorspelling voor " +); +my %status_items_txt_fr = ( + 0 => "Vent", + 1 => "Humidité", + 2 => "Température", + 3 => "Maintenant", + 4 => "Prévisions météo pour " +); +my %status_items_txt_pl = ( + 0 => "Wiatr", + 1 => "Wilgotność", + 2 => "Temperatura", + 3 => "Teraz", + 4 => "Prognoza pogody w " +); +my %status_items_txt_it = ( + 0 => "Vento", + 1 => "Umidità", + 2 => "Temperatura", + 3 => "Adesso", + 4 => "Previsioni del tempo per " +); + +my %wdays_txt_i18n; +my @directions_txt_i18n; +my %pressure_trend_txt_i18n; +my %status_items_txt_i18n; + +my @iconlist = ( + 'storm', 'storm', + 'storm', 'thunderstorm', + 'thunderstorm', 'rainsnow', + 'sleet', 'snow', + 'drizzle', 'drizzle', + 'icy', 'chance_of_rain', + 'chance_of_rain', 'snowflurries', + 'chance_of_snow', 'heavysnow', + 'snow', 'sleet', + 'sleet', 'dust', + 'fog', 'haze', + 'smoke', 'flurries', + 'windy', 'icy', + 'cloudy', 'mostlycloudy_night', + 'mostlycloudy', 'partly_cloudy_night', + 'partly_cloudy', 'sunny', + 'sunny', 'mostly_clear_night', + 'mostly_sunny', 'heavyrain', + 'sunny', 'scatteredthunderstorms', + 'scatteredthunderstorms', 'scatteredthunderstorms', + 'scatteredshowers', 'heavysnow', + 'chance_of_snow', 'heavysnow', + 'partly_cloudy', 'heavyrain', + 'chance_of_snow', 'scatteredshowers' +); + +################################### +sub _LanguageInitialize { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $lang = shift; + + given ($lang) { + when ('de') { + %wdays_txt_i18n = %wdays_txt_de; + @directions_txt_i18n = @directions_txt_de; + %pressure_trend_txt_i18n = %pressure_trend_txt_de; + %status_items_txt_i18n = %status_items_txt_de; + } + + when ('nl') { + %wdays_txt_i18n = %wdays_txt_nl; + @directions_txt_i18n = @directions_txt_nl; + %pressure_trend_txt_i18n = %pressure_trend_txt_nl; + %status_items_txt_i18n = %status_items_txt_nl; + } + + when ('fr') { + %wdays_txt_i18n = %wdays_txt_fr; + @directions_txt_i18n = @directions_txt_fr; + %pressure_trend_txt_i18n = %pressure_trend_txt_fr; + %status_items_txt_i18n = %status_items_txt_fr; + } + + when ('pl') { + %wdays_txt_i18n = %wdays_txt_pl; + @directions_txt_i18n = @directions_txt_pl; + %pressure_trend_txt_i18n = %pressure_trend_txt_pl; + %status_items_txt_i18n = %status_items_txt_pl; + } + + when ('it') { + %wdays_txt_i18n = %wdays_txt_it; + @directions_txt_i18n = @directions_txt_it; + %pressure_trend_txt_i18n = %pressure_trend_txt_it; + %status_items_txt_i18n = %status_items_txt_it; + } + + default { + %wdays_txt_i18n = %wdays_txt_en; + @directions_txt_i18n = @directions_txt_en; + %pressure_trend_txt_i18n = %pressure_trend_txt_en; + %status_items_txt_i18n = %status_items_txt_en; + } + } + + return; +} + +################################### +## deprecated code of older versions +# sub DebugCodes { +# my $lang = shift; + +# my @YahooCodes_i18n = YahooWeatherAPI_getYahooCodes($lang); + +# ::Debug "Weather Code List, see http://developer.yahoo.com/weather/#codes"; +# for ( my $c = 0 ; $c <= 47 ; $c++ ) { +# ::Debug +# sprintf( "%2d %30s %30s", $c, $iconlist[$c], $YahooCodes_i18n[$c] ); +# } + +# return; +# } + +################################### + +sub _degrees_to_direction { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $degrees = shift; + my $directions_txt_i18n = shift; + + my $mod = int( ( ( $degrees + 11.25 ) % 360 ) / 22.5 ); + return $directions_txt_i18n->[$mod]; +} + +sub _ReturnWithError { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $hash = shift; + my $responseRef = shift; + + my $name = $hash->{NAME}; + + ::readingsBeginUpdate($hash); + ::readingsBulkUpdate( $hash, 'lastError', $responseRef->{status} ); + + foreach my $r ( keys %{$responseRef} ) { + ::readingsBulkUpdate( $hash, $r, $responseRef->{$r} ) + if ( ref( $responseRef->{$r} ) ne 'HASH' ); + } + ::readingsBulkUpdate( $hash, 'state', + 'API Maintainer: ' + . $responseRef->{apiMaintainer} + . ' ErrorMsg: ' + . $responseRef->{status} ); + ::readingsEndUpdate( $hash, 1 ); + + my $next = 60; # $next= $hash->{INTERVAL}; + _RearmTimer( $hash, gettimeofday() + $next ); + + return; +} + +sub _DeleteForecastreadings { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $hash = shift; + + my $name = $hash->{NAME}; + my $forecastConfig = _ForcastConfig($hash); + my $forecastLimit = ::AttrVal( $name, 'forecastLimit', 5 ) + 1; + my $forecastLimitNoForecast = 1; + + $forecastLimit = $forecastLimitNoForecast + if ( !$forecastConfig->{daily} ); + ::CommandDeleteReading( undef, + $name . ' ' . 'fc([' . $forecastLimit . '-9]|[0-9]{2})_.*' ); + + $forecastLimit = $forecastLimitNoForecast + if ( !$forecastConfig->{hourly} ); + ::CommandDeleteReading( undef, + $name . ' ' . 'hfc([' . $forecastLimit . '-9]|[0-9]{2})_.*' ); + + return; +} + +sub DeleteAlertsreadings { + + my $hash = shift; + my $alertsLimit = shift // 0; + + my $name = $hash->{NAME}; + my $alertsConfig = _ForcastConfig($hash); + my $alertsLimitNoAlerts = 0; + + $alertsLimit = $alertsLimitNoAlerts + if ( !$alertsConfig->{alerts} ); + + ::CommandDeleteReading( undef, + $name . ' ' . 'warn_([' . $alertsLimit . '-9]|[0-9]{2})_.*' ); + + return; +} + +sub RetrieveCallbackFn { + my $name = shift; + + return + unless ( ::IsDevice($name) ); + + my $hash = $::defs{$name}; + my $responseRef = $hash->{fhem}->{api}->getWeather; + + if ( $responseRef->{status} eq 'ok' ) { + _Writereadings( $hash, $responseRef ); + } + else { + _ReturnWithError( $hash, $responseRef ); + } + + return; +} + +sub _ForcastConfig { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $hash = shift; + + my $name = $hash->{NAME}; + my %forecastConfig; + + $forecastConfig{hourly} = + ( ::AttrVal( $name, 'forecast', '' ) =~ m{hourly}xms ? 1 : 0 ); + + $forecastConfig{daily} = + ( ::AttrVal( $name, 'forecast', '' ) =~ m{daily}xms ? 1 : 0 ); + + $forecastConfig{alerts} = ::AttrVal( $name, 'alerts', 0 ); + + return \%forecastConfig; +} + +sub _Writereadings { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $hash = shift; + my $dataRef = shift; + + my $forecastConfig = _ForcastConfig($hash); + my $name = $hash->{NAME}; + + ::readingsBeginUpdate($hash); + + # housekeeping information + ::readingsBulkUpdate( $hash, 'lastError', '' ); + foreach my $r ( keys %{$dataRef} ) { + ::readingsBulkUpdate( $hash, $r, $dataRef->{$r} ) + if ( ref( $dataRef->{$r} ) ne 'HASH' + && ref( $dataRef->{$r} ) ne 'ARRAY' ); + ::readingsBulkUpdate( $hash, '.license', $dataRef->{license}->{text} ); + } + + ### current + if ( defined( $dataRef->{current} ) + && ref( $dataRef->{current} ) eq 'HASH' ) + { + while ( my ( $r, $v ) = each %{ $dataRef->{current} } ) { + ::readingsBulkUpdate( $hash, $r, $v ) + if ( ref( $dataRef->{$r} ) ne 'HASH' + && ref( $dataRef->{$r} ) ne 'ARRAY' ); + } + + ::readingsBulkUpdate( $hash, 'icon', + $iconlist[ $dataRef->{current}->{code} ] ); + if ( defined( $dataRef->{current}->{wind_direction} ) + && $dataRef->{current}->{wind_direction} + && defined( $dataRef->{current}->{wind_speed} ) + && $dataRef->{current}->{wind_speed} ) + { + my $wdir = + _degrees_to_direction( $dataRef->{current}->{wind_direction}, + \@directions_txt_i18n ); + ::readingsBulkUpdate( $hash, 'wind_condition', + 'Wind: ' + . $wdir . ' ' + . $dataRef->{current}->{wind_speed} + . ' km/h' ); + } + } + + ### forecast + if ( ref( $dataRef->{forecast} ) eq 'HASH' + && ( $forecastConfig->{hourly} || $forecastConfig->{daily} ) ) + { + ## hourly + if ( defined( $dataRef->{forecast}->{hourly} ) + && ref( $dataRef->{forecast}->{hourly} ) eq 'ARRAY' + && scalar( @{ $dataRef->{forecast}->{hourly} } ) > 0 + && $forecastConfig->{hourly} ) + { + my $i = 0; + my $limit = ::AttrVal( $name, 'forecastLimit', 5 ); + foreach my $fc ( @{ $dataRef->{forecast}->{hourly} } ) { + $i++; + my $f = "hfc" . $i . "_"; + + while ( my ( $r, $v ) = each %{$fc} ) { + ::readingsBulkUpdate( $hash, $f . $r, $v ) + if ( ref( $dataRef->{$r} ) ne 'HASH' + && ref( $dataRef->{$r} ) ne 'ARRAY' ); + } + ::readingsBulkUpdate( + $hash, + $f . 'icon', + $iconlist[ $dataRef->{forecast}->{hourly}[ $i - 1 ]{code} ] + ); + + if ( + defined( + $dataRef->{forecast}->{hourly}[ $i - 1 ]{wind_direction} + ) + && $dataRef->{forecast}->{hourly}[ $i - 1 ]{wind_direction} + && defined( + $dataRef->{forecast}->{hourly}[ $i - 1 ]{wind_speed} + ) + && $dataRef->{forecast}->{hourly}[ $i - 1 ]{wind_speed} + ) + { + my $wdir = _degrees_to_direction( + $dataRef->{forecast} + ->{hourly}[ $i - 1 ]{wind_direction}, + \@directions_txt_i18n + ); + ::readingsBulkUpdate( + $hash, + $f . 'wind_condition', + 'Wind: ' + . $wdir . ' ' + . $dataRef->{forecast}->{hourly}[ $i - 1 ]{wind_speed} + . ' km/h' + ); + } + + last if ( $i == $limit && $limit > 0 ); + } + } + + ## daily + if ( defined( $dataRef->{forecast}->{daily} ) + && ref( $dataRef->{forecast}->{daily} ) eq 'ARRAY' + && scalar( @{ $dataRef->{forecast}->{daily} } ) > 0 + && $forecastConfig->{daily} ) + { + my $i = 0; + my $limit = ::AttrVal( $name, 'forecastLimit', 5 ); + foreach my $fc ( @{ $dataRef->{forecast}->{daily} } ) { + $i++; + my $f = "fc" . $i . "_"; + + while ( my ( $r, $v ) = each %{$fc} ) { + ::readingsBulkUpdate( $hash, $f . $r, $v ) + if ( ref( $dataRef->{$r} ) ne 'HASH' + && ref( $dataRef->{$r} ) ne 'ARRAY' ); + } + ::readingsBulkUpdate( + $hash, + $f . 'icon', + $iconlist[ $dataRef->{forecast}->{daily}[ $i - 1 ]{code} ] + ); + + if ( + defined( + $dataRef->{forecast}->{daily}[ $i - 1 ]{wind_direction} + ) + && $dataRef->{forecast}->{daily}[ $i - 1 ]{wind_direction} + && defined( + $dataRef->{forecast}->{daily}[ $i - 1 ]{wind_speed} + ) + && $dataRef->{forecast}->{daily}[ $i - 1 ]{wind_speed} + ) + { + my $wdir = _degrees_to_direction( + $dataRef->{forecast}->{daily}[ $i - 1 ]{wind_direction}, + \@directions_txt_i18n + ); + ::readingsBulkUpdate( + $hash, + $f . 'wind_condition', + 'Wind: ' + . $wdir . ' ' + . $dataRef->{forecast}->{daily}[ $i - 1 ]{wind_speed} + . ' km/h' + ); + } + + last if ( $i == $limit && $limit > 0 ); + } + } + } + + ### alerts + if ( defined( $dataRef->{alerts} ) + && ref( $dataRef->{alerts} ) eq 'ARRAY' + && scalar( @{ $dataRef->{alerts} } ) > 0 + && $forecastConfig->{alerts} ) + { + my $i = 0; + foreach my $warn ( @{ $dataRef->{alerts} } ) { + my $w = "warn_" . $i . "_"; + + while ( my ( $r, $v ) = each %{$warn} ) { + ::readingsBulkUpdate( $hash, $w . $r, $v ) + if ( ref( $dataRef->{$r} ) ne 'HASH' + && ref( $dataRef->{$r} ) ne 'ARRAY' ); + } + + $i++; + } + + DeleteAlertsreadings( $hash, scalar( @{ $dataRef->{alerts} } ) ); + ::readingsBulkUpdate( $hash, 'warnCount', + scalar( @{ $dataRef->{alerts} } ) ); + } + else { + DeleteAlertsreadings($hash); + ::readingsBulkUpdate( $hash, 'warnCount', + scalar( @{ $dataRef->{alerts} } ) ) + if ( defined( $dataRef->{alerts} ) + && ref( $dataRef->{alerts} ) eq 'ARRAY' ); + } + + ### state + my $val = 'T: ' + . $dataRef->{current}->{temperature} . ' °C' . ' ' + . substr( $status_items_txt_i18n{1}, 0, 1 ) . ': ' + . $dataRef->{current}->{humidity} . ' %' . ' ' + . substr( $status_items_txt_i18n{0}, 0, 1 ) . ': ' + . $dataRef->{current}->{wind} . ' km/h' . ' P: ' + . $dataRef->{current}->{pressure} . ' hPa'; + + ::Log3 $hash, 4, "$name: $val"; + ::readingsBulkUpdate( $hash, 'state', $val ); + + ::readingsEndUpdate( $hash, 1 ); + + _RearmTimer( $hash, gettimeofday() + $hash->{INTERVAL} ); + + return; + +} + +################################### +sub GetUpdate { + my $hash = shift; + + my $name = $hash->{NAME}; + + if ( ::IsDisabled($name) ) { + ::Log3 $hash, 5, + "Weather $name: retrieval of weather data is disabled by attribute."; + ::readingsBeginUpdate($hash); + ::readingsBulkUpdate( $hash, "pubDateComment", + "disabled by attribute" ); + ::readingsBulkUpdate( $hash, "validity", "stale" ); + ::readingsEndUpdate( $hash, 1 ); + _RearmTimer( $hash, gettimeofday() + $hash->{INTERVAL} ); + } + else { + $hash->{fhem}->{api}->setRetrieveData; + } + + return; +} + +################################### +sub Get { + my $hash = shift // return; + my $aRef = shift // return; + + my $name = shift @$aRef // return; + my $reading = shift @$aRef // return; + my $value; + + if ( defined( $hash->{readings}->{$reading} ) ) { + $value = $hash->{readings}->{$reading}->{VAL}; + } + else { + my $rt = ''; + if ( defined( $hash->{readings} ) ) { + $rt = join( ":noArg ", sort keys %{ $hash->{readings} } ); + } + + return "Unknown reading $reading, choose one of " . $rt; + } + + return "$name $reading => $value"; +} + +################################### +sub Set { + my $hash = shift // return; + my $aRef = shift // return; + + my $name = shift @$aRef // return; + my $cmd = shift @$aRef + // return qq{"set $name" needs at least one argument}; + + # usage check + if ( scalar( @{$aRef} ) == 0 + && $cmd eq 'update' ) + { + _DisarmTimer($hash); + GetUpdate($hash); + + return; + } + elsif ( scalar( @{$aRef} ) == 1 + && $cmd eq "newLocation" ) + { + if ( $hash->{API} eq 'DarkSkyAPI' + || $hash->{API} eq 'OpenWeatherMapAPI' + || $hash->{API} eq 'wundergroundAPI' ) + { + my ( $lat, $long ); + ( $lat, $long ) = split( ',', $aRef->[0] ) + if ( defined( $aRef->[0] ) && $aRef->[0] ); + ( $lat, $long ) = split( ',', $hash->{fhem}->{LOCATION} ) + unless ( defined($lat) + && defined($long) + && $lat =~ m{(-?\d+(\.\d+)?)}xms + && $long =~ m{(-?\d+(\.\d+)?)}xms ); + + $hash->{fhem}->{api}->setLocation( $lat, $long ); + _DisarmTimer($hash); + GetUpdate($hash); + return; + } + else { return 'this API is not ' . $aRef->[0] . ' supported' } + } + else { + return "Unknown argument $cmd, choose one of update:noArg newLocation"; + } +} + +################################### +sub _RearmTimer { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $hash = shift; + my $t = shift; + + ::Log3( $hash, 4, "Weather $hash->{NAME}: Rearm new Timer" ); + ::InternalTimer( $t, \&FHEM::Core::Weather::GetUpdate, $hash, 0 ); + + return; +} + +sub _DisarmTimer { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $hash = shift; + + ::RemoveInternalTimer($hash); + + return; +} + +sub Notify { + my $hash = shift; + my $dev = shift; + + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + + return if ( $dev->{NAME} ne "global" ); + + # set forcast and alerts values to api object + if ( grep { /^MODIFIED.$name$/x } @{ $dev->{CHANGED} } ) { + $hash->{fhem}->{api}->setForecast( ::AttrVal( $name, 'forecast', '' ) ); + $hash->{fhem}->{api}->setAlerts( ::AttrVal( $name, 'alerts', 0 ) ); + + GetUpdate($hash); + } + + return + if ( + !grep { +/^INITIALIZED|REREADCFG|DELETEATTR.$name.disable|ATTR.$name.disable.[0-1]$/x + } @{ $dev->{CHANGED} } + ); + + # update weather after initialization or change of configuration + # wait 10 to 29 seconds to avoid congestion due to concurrent activities + _DisarmTimer($hash); + my $delay = 10 + int( rand(20) ); + + ::Log3 $hash, 5, +"Weather $name: FHEM initialization or rereadcfg triggered update, delay $delay seconds."; + _RearmTimer( $hash, gettimeofday() + $delay ); + + ### quick run GetUpdate then Demo + GetUpdate($hash) + if ( defined( $hash->{APIKEY} ) && lc( $hash->{APIKEY} ) eq 'demo' ); + + return; +} + +##################################### +sub Define { + my $hash = shift // return; + my $aRef = shift // return; + my $hRef = shift // undef; + + return $@ unless ( FHEM::Meta::SetInternals($hash) ); + use version 0.60; our $VERSION = FHEM::Meta::Get( $hash, 'version' ); + + my $usage = +"syntax: define Weather [API=] [apikey=] [location=] [interval=] [lang=]"; + + # check minimum syntax + return $usage unless ( scalar @{$aRef} == 2 ); + my $name = $aRef->[0]; + + my $location = $hRef->{location} // undef; + my $apikey = $hRef->{apikey} // undef; + my $lang = $hRef->{lang} // undef; + my $interval = $hRef->{interval} // 3600; + my $API = $hRef->{API} // "DarkSkyAPI,cachemaxage:600"; + + # evaluate API options + my ( $api, $apioptions ) = split( ',', $API, 2 ); + $apioptions = "" unless ( defined($apioptions) ); + eval { require 'FHEM/APIs/Weather/' . $api . '.pm'; }; + return "$name: cannot load API $api: $@" if ($@); + + $hash->{NOTIFYDEV} = "global"; + $hash->{fhem}->{interfaces} = "temperature;humidity;wind"; + $hash->{fhem}->{LOCATION} = ( + ( defined($location) && $location ) + ? $location + : ::AttrVal( 'global', 'latitude', 'error' ) . ',' + . ::AttrVal( 'global', 'longitude', 'error' ) + ); + $hash->{INTERVAL} = $interval; + $hash->{LANG} = ( + ( defined($lang) && $lang ) + ? $lang + : lc( ::AttrVal( 'global', 'language', 'de' ) ) + ); + $hash->{API} = $api; + $hash->{MODEL} = $api; + $hash->{APIKEY} = $apikey; + $hash->{APIOPTIONS} = $apioptions; + $hash->{VERSION} = version->parse($VERSION)->normal; + $hash->{fhem}->{allowCache} = 1; + + ::readingsSingleUpdate( $hash, 'current_date_time', ::TimeNow(), 0 ); + ::readingsSingleUpdate( $hash, 'current_date_time', 'none', 0 ); + + ::readingsSingleUpdate( $hash, 'state', 'Initialized', 1 ); + _LanguageInitialize( $hash->{LANG} ); + + my $apistring = 'FHEM::APIs::Weather::' . $api; + $hash->{fhem}->{api} = $apistring->new( + { + devName => $hash->{NAME}, + apikey => $hash->{APIKEY}, + location => $hash->{fhem}->{LOCATION}, + apioptions => $hash->{APIOPTIONS}, + language => $hash->{LANG}, + } + ); + + return; +} + +##################################### +sub Undef { + my $hash = shift; + my $arg = shift; + + ::RemoveInternalTimer($hash); + return; +} + +sub Attr { + my ( $cmd, $name, $attrName, $AttrVal ) = @_; + my $hash = $::defs{$name}; + + given ($attrName) { + when ('forecast') { + if ( $cmd eq 'set' ) { + $hash->{fhem}->{api}->setForecast($AttrVal); + } + elsif ( $cmd eq 'del' ) { + $hash->{fhem}->{api}->setForecast(); + } + + ::InternalTimer( gettimeofday() + 0.5, + \&FHEM::Core::Weather::DeleteForecastreadings, $hash ); + } + + when ('forecastLimit') { + ::InternalTimer( gettimeofday() + 0.5, + \&FHEM::Core::Weather::DeleteForecastreadings, $hash ); + } + + when ('alerts') { + if ( $cmd eq 'set' ) { + $hash->{fhem}->{api}->setAlerts($AttrVal); + } + elsif ( $cmd eq 'del' ) { + $hash->{fhem}->{api}->setAlerts(); + } + + ::InternalTimer( gettimeofday() + 0.5, + \&FHEM::Core::Weather::DeleteAlertsreadings, $hash ); + } + } + + return; +} + +##################################### + +# Icon Parameter + +Readonly my $ICONWIDTH => 175; +Readonly my $ICONSCALE => 0.5; + +##################################### + +sub _WeatherIconIMGTag { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $icon = shift; + + my $width = int( $ICONSCALE * $ICONWIDTH ); + my $url = ::FW_IconURL("weather/$icon"); + my $style = " width=$width"; + + return "\"$icon\""; +} + +##################################### +sub ::WeatherAsHtmlV { goto &WeatherAsHtmlV } + +sub WeatherAsHtmlV { + my $d = shift; + my $op1 = shift; + my $op2 = shift; + + my ( $f, $items ) = _CheckOptions( $d, $op1, $op2 ); + + my $h = $::defs{$d}; + my $width = int( $ICONSCALE * $ICONWIDTH ); + + my $ret = ''; + my $fc; + if ( + defined($f) + && ( $f eq 'h' + || $f eq 'd' ) + ) + { + $fc = ( $f eq 'd' ? 'fc' : 'hfc' ); + } + else { + $fc = ( + ( + defined( $h->{readings}->{fc1_day_of_week} ) + && $h->{readings}->{fc1_day_of_week} + ) ? 'fc' : 'hfc' + ); + } + + $ret .= sprintf( +'', + $width, + _WeatherIconIMGTag( ::ReadingsVal( $d, "icon", "" ) ), + ::ReadingsVal( $d, "condition", "" ), + ::ReadingsVal( $d, "temp_c", "" ), + ::ReadingsVal( $d, "humidity", "" ), + ::ReadingsVal( $d, "wind_condition", "" ) + ); + + for ( my $i = 1 ; $i < $items ; $i++ ) { + if ( defined( $h->{readings}->{"${fc}${i}_low_c"} ) + && $h->{readings}->{"${fc}${i}_low_c"} ) + { + $ret .= sprintf( +'', + $width, + _WeatherIconIMGTag( ::ReadingsVal( $d, "${fc}${i}_icon", "" ) ), + ::ReadingsVal( $d, "${fc}${i}_day_of_week", "" ), + ::ReadingsVal( $d, "${fc}${i}_condition", "" ), + ::ReadingsVal( $d, "${fc}${i}_low_c", " - " ), + ::ReadingsVal( $d, "${fc}${i}_high_c", " - " ), + ::ReadingsVal( $d, "${fc}${i}_wind_condition", " - " ) + ); + } + else { + $ret .= sprintf( +'', + $width, + _WeatherIconIMGTag( ::ReadingsVal( $d, "${fc}${i}_icon", "" ) ), + ::ReadingsVal( $d, "${fc}${i}_day_of_week", "" ), + ::ReadingsVal( $d, "${fc}${i}_condition", "" ), + ::ReadingsVal( $d, "${fc}${i}_temperature", " - " ), + ::ReadingsVal( $d, "${fc}${i}_wind_condition", " - " ) + ); + } + } + + $ret .= "
%s%s
%s°C %s%%
%s
%s%s: %s
min %s°C max %s°C
%s
%s%s: %s
%s°C
%s
"; + return $ret; +} + +sub ::WeatherAsHtml { goto &WeatherAsHtml } + +sub WeatherAsHtml { + my $d = shift; + my $op1 = shift; + my $op2 = shift; + + my ( $f, $items ) = _CheckOptions( $d, $op1, $op2 ); + + return WeatherAsHtmlV( $d, $f, $items ); +} + +sub ::WeatherAsHtmlH { goto &WeatherAsHtmlH } + +sub WeatherAsHtmlH { + my $d = shift; + my $op1 = shift; + my $op2 = shift; + + my ( $f, $items ) = _CheckOptions( $d, $op1, $op2 ); + + my $h = $::defs{$d}; + my $width = int( $ICONSCALE * $ICONWIDTH ); + + my $format = +'
%s
%s
%s°C %s%%
%s
'; + + my $ret = ''; + my $fc; + if ( + defined($f) + && ( $f eq 'h' + || $f eq 'd' ) + ) + { + $fc = ( $f eq 'd' ? 'fc' : 'hfc' ); + } + else { + $fc = ( + ( + defined( $h->{readings}->{fc1_day_of_week} ) + && $h->{readings}->{fc1_day_of_week} + ) ? 'fc' : 'hfc' + ); + } + + # icons + $ret .= sprintf( '', + $width, _WeatherIconIMGTag( ::ReadingsVal( $d, "icon", "" ) ) ); + for ( my $i = 1 ; $i < $items ; $i++ ) { + $ret .= sprintf( '', + $width, + _WeatherIconIMGTag( ::ReadingsVal( $d, "${fc}${i}_icon", "" ) ) ); + } + $ret .= ''; + + # condition + $ret .= sprintf( '', + ::ReadingsVal( $d, "condition", "" ) ); + for ( my $i = 1 ; $i < $items ; $i++ ) { + $ret .= sprintf( + '', + ::ReadingsVal( $d, "${fc}${i}_day_of_week", "" ), + ::ReadingsVal( $d, "${fc}${i}_condition", "" ) + ); + } + $ret .= ''; + + # temp/hum | min + $ret .= sprintf( + '', + ::ReadingsVal( $d, "temp_c", "" ), + ::ReadingsVal( $d, "humidity", "" ) + ); + for ( my $i = 1 ; $i < $items ; $i++ ) { + if ( defined( $h->{readings}->{"${fc}${i}_low_c"} ) + && $h->{readings}->{"${fc}${i}_low_c"} ) + { + $ret .= sprintf( '', + ::ReadingsVal( $d, "${fc}${i}_low_c", " - " ) ); + } + else { + $ret .= sprintf( '', + ::ReadingsVal( $d, "${fc}${i}_temperature", " - " ) ); + } + } + + $ret .= ''; + + # wind | max + $ret .= sprintf( '', + ::ReadingsVal( $d, "wind_condition", "" ) ); + for ( my $i = 1 ; $i < $items ; $i++ ) { + if ( defined( $h->{readings}->{"${fc}${i}_high_c"} ) + && $h->{readings}->{"${fc}${i}_high_c"} ) + { + $ret .= sprintf( '', + ::ReadingsVal( $d, "${fc}${i}_high_c", " - " ) ); + } + } + + $ret .= "
%s%s
%s%s: %s
%s°C %s%%min %s°C %s°C
%smax %s°C
"; + + return $ret; +} + +sub ::WeatherAsHtmlD { goto &WeatherAsHtmlD } + +sub WeatherAsHtmlD { + my $d = shift; + my $op1 = shift; + my $op2 = shift; + + my ( $f, $items ) = _CheckOptions( $d, $op1, $op2 ); + my $ret; + + if ($FW_ss) { + $ret = WeatherAsHtmlV( $d, $f, $items ); + } + else { + $ret = WeatherAsHtmlH( $d, $f, $items ); + } + + return $ret; +} + +sub _CheckOptions { + return 0 unless ( __PACKAGE__ eq caller(0) ); + + my $d = shift; + my $op1 = shift; + my $op2 = shift; + + return "$d is not a Weather instance
" + if ( !$::defs{$d} || $::defs{$d}->{TYPE} ne "Weather" ); + + my $hash = $::defs{$d}; + my $items = $op2; + my $f = $op1; + + if ( defined($op1) && $op1 && $op1 =~ m{[0-9]}xms ) { $items = $op1; } + if ( defined($op2) && $op2 && $op2 =~ m{[dh]}xms ) { $f = $op2; } + + $f =~ tr/dh/./cd if ( defined $f && $f ); + $items =~ tr/0-9/./cd if ( defined($items) && $items ); + + $items = ::AttrVal( $d, 'forecastLimit', 5 ) + if ( !$items ); + + my $forecastConfig = _ForcastConfig($hash); + $f = ( + $forecastConfig->{daily} + ? 'd' + : ( $forecastConfig->{daily} && $forecastConfig->{hourly} ? $f : 'h' ) + ) if !( defined($f) and $f ); + + $f = 'h' if ( !$f || length($f) > 1 ); + + return ( $f, $items + 1 ); +} + +##################################### + +1; + +__END__