From 1524a584e5e627b22dad36bc0d5526c51a03dc55 Mon Sep 17 00:00:00 2001 From: CoolTux Date: Sun, 29 Jan 2023 15:27:02 +0000 Subject: [PATCH] 59_Weather: mod packages, add new modul path in to pre-commit git-svn-id: https://svn.fhem.de/fhem/trunk@27146 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/59_Weather.pm | 1098 +--------------- fhem/lib/FHEM/APIs/Weather/DarkSkyAPI.pm | 2 +- .../FHEM/APIs/Weather/OpenWeatherMapAPI.pm | 2 +- fhem/lib/FHEM/APIs/Weather/wundergroundAPI.pm | 2 +- fhem/lib/FHEM/Core/Weather.pm | 1140 +++++++++++++++++ 6 files changed, 1161 insertions(+), 1084 deletions(-) create mode 100644 fhem/lib/FHEM/Core/Weather.pm diff --git a/fhem/CHANGED b/fhem/CHANGED index 0a114b8b9..837ca5b97 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - feature: 59_Weather: mod packages, add new modul path in to pre-commit - bugfix: 00_KNXIO: PBP fixes - bugfix: 10_KNX: fix define-parsing problem, PBP fixes - bugfix: 74_AutomowerConnect: improved error handling, fix missig password bug diff --git a/fhem/FHEM/59_Weather.pm b/fhem/FHEM/59_Weather.pm index 440d2cb6f..b6ca7c5eb 100755 --- a/fhem/FHEM/59_Weather.pm +++ b/fhem/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/fhem/lib/FHEM/APIs/Weather/DarkSkyAPI.pm b/fhem/lib/FHEM/APIs/Weather/DarkSkyAPI.pm index eb3009fe7..59ce1a6cf 100644 --- a/fhem/lib/FHEM/APIs/Weather/DarkSkyAPI.pm +++ b/fhem/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/fhem/lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm b/fhem/lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm index 539dc4969..27cdd7e4f 100644 --- a/fhem/lib/FHEM/APIs/Weather/OpenWeatherMapAPI.pm +++ b/fhem/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/fhem/lib/FHEM/APIs/Weather/wundergroundAPI.pm b/fhem/lib/FHEM/APIs/Weather/wundergroundAPI.pm index 8ffe71698..1f66e126c 100644 --- a/fhem/lib/FHEM/APIs/Weather/wundergroundAPI.pm +++ b/fhem/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/fhem/lib/FHEM/Core/Weather.pm b/fhem/lib/FHEM/Core/Weather.pm new file mode 100644 index 000000000..b65d22e75 --- /dev/null +++ b/fhem/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__