# $Id$ ############################################################################## # # 59_Weather.pm # (c) 2009-2025 Copyright by Dr. Boris Neubert # e-mail: omega at online dot de # # Contributors: # - Marko Oldenburg (CoolTux) # - Lippie # - stefanru (wundergroundAPI) # # # 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; my $missingModul = ''; eval { use Time::HiRes qw /gettimeofday/; 1 } or $missingModul .= "libtime-hires-perl "; eval { use Readonly; 1 } or $missingModul .= "libreadonly-perl "; #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 { 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' ); } if ( defined( $dataRef->{current}->{code} ) && $dataRef->{current}->{code} ) { ::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' ); return 'Cannot define Weather device. Please use "apt install ' . ${missingModul} . ' to install missing perl modules' if ($missingModul); 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__