2020-12-13 17:29:15 +00:00
########################################################################################################################
# $Id: 76_SolarForecast.pm 21735 2020-04-20 20:53:24Z DS_Starter $
#########################################################################################################################
# 76_SolarForecast.pm
#
2021-01-17 19:13:02 +00:00
# (c) 2020-2021 by Heiko Maaz e-mail: Heiko dot Maaz at t-online dot de
2020-12-13 17:29:15 +00:00
#
# This Module is used by module 76_SMAPortal to create graphic devices.
# It can't be used standalone without any SMAPortal-Device.
#
# This script 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 <http://www.gnu.org/licenses/>.
#
#########################################################################################################################
package FHEM::SolarForecast ; ## no critic 'package'
use strict ;
use warnings ;
use POSIX ;
use GPUtils qw( GP_Import GP_Export ) ; # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt
use Time::HiRes qw( gettimeofday ) ;
eval "use FHEM::Meta;1" or my $ modMetaAbsent = 1 ; ## no critic 'eval'
use Encode ;
use utf8 ;
2021-01-17 19:13:02 +00:00
eval "use JSON;1;" or my $ jsonabs = "JSON" ; ## no critic 'eval' # Debian: apt-get install libjson-perl
2020-12-13 17:29:15 +00:00
2021-01-17 19:13:02 +00:00
use FHEM::SynoModules::SMUtils qw( evaljson
moduleVersion
2021-01-23 11:46:54 +00:00
trim
2021-01-17 19:13:02 +00:00
) ; # Hilfsroutinen Modul
use Data::Dumper ;
2020-12-13 17:29:15 +00:00
# Run before module compilation
BEGIN {
# Import from main::
GP_Import (
qw(
2021-01-17 19:13:02 +00:00
attr
2020-12-13 17:29:15 +00:00
AnalyzePerlCommand
AttrVal
AttrNum
2021-01-17 19:13:02 +00:00
data
2020-12-13 17:29:15 +00:00
defs
delFromDevAttrList
delFromAttrList
devspec2array
deviceEvents
Debug
2020-12-13 21:48:45 +00:00
fhemTimeLocal
2020-12-13 17:29:15 +00:00
FmtDateTime
2021-01-17 19:13:02 +00:00
FileWrite
FileRead
FileDelete
2020-12-13 17:29:15 +00:00
FmtTime
FW_makeImage
getKeyValue
init_done
InternalTimer
IsDisabled
Log
Log3
2020-12-15 13:41:10 +00:00
modules
parseParams
2020-12-13 17:29:15 +00:00
readingsSingleUpdate
readingsBulkUpdate
readingsBulkUpdateIfChanged
readingsBeginUpdate
readingsDelete
readingsEndUpdate
ReadingsNum
ReadingsTimestamp
ReadingsVal
RemoveInternalTimer
readingFnAttributes
setKeyValue
sortTopicNum
FW_cmd
FW_directNotify
FW_ME
FW_subdir
FW_room
FW_detail
FW_wname
)
) ;
# Export to main context with different name
# my $pkg = caller(0);
# my $main = $pkg;
# $main =~ s/^(?:.+::)?([^:]+)$/main::$1\_/g;
# foreach (@_) {
# *{ $main . $_ } = *{ $pkg . '::' . $_ };
# }
GP_Export (
qw(
Initialize
pageAsHtml
)
) ;
}
# Versions History intern
my % vNotesIntern = (
2021-01-23 08:01:53 +00:00
"0.3.0" = > "21.12.2021 add cloud correction, add rain correction, add reset pvHistory " ,
2021-01-21 22:07:18 +00:00
"0.2.0" = > "20.12.2021 use SMUtils, JSON, implement getter data,html,pvHistory, correct the 'disable' problem " ,
2020-12-13 17:29:15 +00:00
"0.1.0" = > "09.12.2020 initial Version "
) ;
# Voreinstellungen
my % hset = ( # Hash der Set-Funktion
2020-12-27 18:51:53 +00:00
currentForecastDev = > { fn = > \ & _setcurrentForecastDev } ,
2020-12-15 13:41:10 +00:00
moduleArea = > { fn = > \ & _setmoduleArea } ,
moduleEfficiency = > { fn = > \ & _setmoduleEfficiency } ,
inverterEfficiency = > { fn = > \ & _setinverterEfficiency } ,
2020-12-27 18:51:53 +00:00
currentInverterDev = > { fn = > \ & _setinverterDevice } ,
currentMeterDev = > { fn = > \ & _setmeterDevice } ,
2020-12-15 13:41:10 +00:00
pvCorrectionFactor_05 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_06 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_07 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_08 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_09 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_10 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_11 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_12 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_13 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_14 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_15 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_16 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_17 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_18 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_19 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_20 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_21 = > { fn = > \ & _setpvCorrectionFactor } ,
pvCorrectionFactor_Auto = > { fn = > \ & _setpvCorrectionFactorAuto } ,
2020-12-27 18:51:53 +00:00
reset = > { fn = > \ & _setreset } ,
2020-12-16 19:15:48 +00:00
moduleTiltAngle = > { fn = > \ & _setmoduleTiltAngle } ,
) ;
2021-01-17 19:13:02 +00:00
my % hget = ( # Hash für Get-Funktion (needcred => 1: Funktion benötigt gesetzte Credentials)
data = > { fn = > \ & _getdata , needcred = > 0 } ,
html = > { fn = > \ & _gethtml , needcred = > 0 } ,
ftui = > { fn = > \ & _getftui , needcred = > 0 } ,
pvHistory = > { fn = > \ & _getlistPVHistory , needcred = > 0 } ,
) ;
2021-01-22 20:16:33 +00:00
my % htilt = ( # Faktor für Neigungswinkel der Solarmodule (Südausrichtung)
2020-12-27 18:51:53 +00:00
"0" = > 1.00 , # https://www.labri.fr/perso/billaud/travaux/Helios/Helios2/resources/de04/Chapter_4_DE.pdf
2020-12-16 19:15:48 +00:00
"10" = > 1.06 ,
"20" = > 1.15 ,
"30" = > 1.35 ,
"40" = > 1.43 ,
"45" = > 1.44 ,
"50" = > 1.47 ,
"60" = > 1.50 ,
"70" = > 1.44 ,
"80" = > 1.35 ,
"90" = > 1.26
2020-12-13 17:29:15 +00:00
) ;
2021-01-04 20:25:02 +00:00
my % weather_ids = (
# s => 0 , 0 - 3 DWD -> kein signifikantes Wetter
# s => 1 , 45 - 99 DWD -> signifikantes Wetter
'0' = > { s = > '0' , icon = > 'weather_sun' , txtd = > 'sonnig' } ,
'1' = > { s = > '0' , icon = > 'weather_cloudy_light' , txtd = > 'Bewölkung abnehmend' } ,
'2' = > { s = > '0' , icon = > 'weather_cloudy' , txtd = > 'Bewölkung unverändert' } ,
'3' = > { s = > '0' , icon = > 'weather_cloudy_heavy' , txtd = > 'Bewölkung zunehmend' } ,
'4' = > { s = > '0' , icon = > 'unknown' , txtd = > 'Sicht durch Rauch oder Asche vermindert' } ,
'5' = > { s = > '0' , icon = > 'unknown' , txtd = > 'trockener Dunst (relative Feuchte < 80 %)' } ,
'6' = > { s = > '0' , icon = > 'unknown' , txtd = > 'verbreiteter Schwebstaub, nicht vom Wind herangeführt' } ,
'7' = > { s = > '0' , icon = > 'unknown' , txtd = > 'Staub oder Sand bzw. Gischt, vom Wind herangeführt' } ,
'8' = > { s = > '0' , icon = > 'unknown' , txtd = > 'gut entwickelte Staub- oder Sandwirbel' } ,
'9' = > { s = > '0' , icon = > 'unknown' , txtd = > 'Staub- oder Sandsturm im Gesichtskreis, aber nicht an der Station' } ,
'10' = > { s = > '0' , icon = > 'weather_fog' , txtd = > 'Nebel' } ,
'11' = > { s = > '0' , icon = > 'weather_rain_fog' , txtd = > 'Nebel mit Regen' } ,
'12' = > { s = > '0' , icon = > 'weather_fog' , txtd = > 'durchgehender Bodennebel' } ,
'13' = > { s = > '0' , icon = > 'unknown' , txtd = > 'Wetterleuchten sichtbar, kein Donner gehört' } ,
'14' = > { s = > '0' , icon = > 'unknown' , txtd = > 'Niederschlag im Gesichtskreis, nicht den Boden erreichend' } ,
'15' = > { s = > '0' , icon = > 'unknown' , txtd = > 'Niederschlag in der Ferne (> 5 km), aber nicht an der Station' } ,
'16' = > { s = > '0' , icon = > 'unknown' , txtd = > 'Niederschlag in der Nähe (< 5 km), aber nicht an der Station' } ,
'17' = > { s = > '0' , icon = > 'unknown' , txtd = > 'Gewitter (Donner hörbar), aber kein Niederschlag an der Station' } ,
'18' = > { s = > '0' , icon = > 'unknown' , txtd = > 'Markante Böen im Gesichtskreis, aber kein Niederschlag an der Station' } ,
'19' = > { s = > '0' , icon = > 'unknown' , txtd = > 'Tromben (trichterförmige Wolkenschläuche) im Gesichtskreis' } ,
'20' = > { s = > '0' , icon = > 'unknown' , txtd = > 'nach Sprühregen oder Schneegriesel' } ,
'21' = > { s = > '0' , icon = > 'unknown' , txtd = > 'nach Regen' } ,
'22' = > { s = > '0' , icon = > 'unknown' , txtd = > 'nach Schnefall' } ,
'23' = > { s = > '0' , icon = > 'unknown' , txtd = > 'nach Schneeregen oder Eiskörnern' } ,
'24' = > { s = > '0' , icon = > 'unknown' , txtd = > 'nach gefrierendem Regen' } ,
'25' = > { s = > '0' , icon = > 'unknown' , txtd = > 'nach Regenschauer' } ,
'26' = > { s = > '0' , icon = > 'unknown' , txtd = > 'nach Schneeschauer' } ,
'27' = > { s = > '0' , icon = > 'unknown' , txtd = > 'nach Graupel- oder Hagelschauer' } ,
'28' = > { s = > '0' , icon = > 'unknown' , txtd = > 'nach Nebel' } ,
'29' = > { s = > '0' , icon = > 'unknown' , txtd = > 'nach Gewitter' } ,
'30' = > { s = > '0' , icon = > 'unknown' , txtd = > 'leichter oder mäßiger Sandsturm, an Intensität abnehmend' } ,
'31' = > { s = > '0' , icon = > 'unknown' , txtd = > 'leichter oder mäßiger Sandsturm, unveränderte Intensität' } ,
'32' = > { s = > '0' , icon = > 'unknown' , txtd = > 'leichter oder mäßiger Sandsturm, an Intensität zunehmend' } ,
'33' = > { s = > '0' , icon = > 'unknown' , txtd = > 'schwerer Sandsturm, an Intensität abnehmend' } ,
'34' = > { s = > '0' , icon = > 'unknown' , txtd = > 'schwerer Sandsturm, unveränderte Intensität' } ,
'35' = > { s = > '0' , icon = > 'unknown' , txtd = > 'schwerer Sandsturm, an Intensität zunehmend' } ,
'36' = > { s = > '0' , icon = > 'weather_snow_light' , txtd = > 'leichtes oder mäßiges Schneefegen, unter Augenhöhe' } ,
'37' = > { s = > '0' , icon = > 'weather_snow_heavy' , txtd = > 'starkes Schneefegen, unter Augenhöhe' } ,
'38' = > { s = > '0' , icon = > 'weather_snow_light' , txtd = > 'leichtes oder mäßiges Schneetreiben, über Augenhöhe' } ,
'39' = > { s = > '0' , icon = > 'weather_snow_heavy' , txtd = > 'starkes Schneetreiben, über Augenhöhe' } ,
'40' = > { s = > '0' , icon = > 'weather_fog' , txtd = > 'Nebel in einiger Entfernung' } ,
'41' = > { s = > '0' , icon = > 'weather_fog' , txtd = > 'Nebel in Schwaden oder Bänken' } ,
'42' = > { s = > '0' , icon = > 'weather_fog' , txtd = > 'Nebel, Himmel erkennbar, dünner werdend' } ,
'43' = > { s = > '0' , icon = > 'weather_fog' , txtd = > 'Nebel, Himmel nicht erkennbar, dünner werdend' } ,
'44' = > { s = > '0' , icon = > 'weather_fog' , txtd = > 'Nebel, Himmel erkennbar, unverändert' } ,
'45' = > { s = > '1' , icon = > 'weather_fog' , txtd = > 'Nebel' } ,
'46' = > { s = > '0' , icon = > 'weather_fog' , txtd = > 'Nebel, Himmel erkennbar, dichter werdend' } ,
'47' = > { s = > '0' , icon = > 'weather_fog' , txtd = > 'Nebel, Himmel nicht erkennbar, dichter werdend' } ,
'48' = > { s = > '1' , icon = > 'weather_fog' , txtd = > 'Nebel mit Reifbildung' } ,
'49' = > { s = > '0' , icon = > 'weather_fog' , txtd = > 'Nebel mit Reifansatz, Himmel nicht erkennbar' } ,
'50' = > { s = > '0' , icon = > 'weather_rain' , txtd = > 'unterbrochener leichter Sprühregen' } ,
'51' = > { s = > '1' , icon = > 'weather_rain_light' , txtd = > 'leichter Sprühregen' } ,
'52' = > { s = > '0' , icon = > 'weather_rain' , txtd = > 'unterbrochener mäßiger Sprühregen' } ,
'53' = > { s = > '1' , icon = > 'weather_rain_light' , txtd = > 'leichter Sprühregen' } ,
'54' = > { s = > '0' , icon = > 'weather_rain_heavy' , txtd = > 'unterbrochener starker Sprühregen' } ,
'55' = > { s = > '1' , icon = > 'weather_rain_heavy' , txtd = > 'starker Sprühregen' } ,
'56' = > { s = > '1' , icon = > 'weather_rain_light' , txtd = > 'leichter gefrierender Sprühregen' } ,
'57' = > { s = > '1' , icon = > 'weather_rain_heavy' , txtd = > 'mäßiger oder starker gefrierender Sprühregen' } ,
'58' = > { s = > '0' , icon = > 'weather_rain_light' , txtd = > 'leichter Sprühregen mit Regen' } ,
'59' = > { s = > '0' , icon = > 'weather_rain_heavy' , txtd = > 'mäßiger oder starker Sprühregen mit Regen' } ,
'60' = > { s = > '0' , icon = > 'weather_rain_light' , txtd = > 'unterbrochener leichter Regen oder einzelne Regentropfen' } ,
'61' = > { s = > '1' , icon = > 'weather_rain_light' , txtd = > 'leichter Regen' } ,
'62' = > { s = > '0' , icon = > 'weather_rain' , txtd = > 'unterbrochener mäßiger Regen' } ,
'63' = > { s = > '1' , icon = > 'weather_rain' , txtd = > 'mäßiger Regen' } ,
'64' = > { s = > '0' , icon = > 'weather_rain_heavy' , txtd = > 'unterbrochener starker Regen' } ,
'65' = > { s = > '1' , icon = > 'weather_rain_heavy' , txtd = > 'starker Regen' } ,
'66' = > { s = > '1' , icon = > 'weather_rain_snow_light' , txtd = > 'leichter gefrierender Regen' } ,
'67' = > { s = > '1' , icon = > 'weather_rain_snow_heavy' , txtd = > 'mäßiger oder starker gefrierender Regen' } ,
'68' = > { s = > '0' , icon = > 'weather_rain_snow_light' , txtd = > 'leichter Schneeregen' } ,
'69' = > { s = > '0' , icon = > 'weather_rain_snow_heavy' , txtd = > 'mäßiger oder starker Schneeregen' } ,
'70' = > { s = > '0' , icon = > 'weather_snow_light' , txtd = > 'unterbrochener leichter Schneefall oder einzelne Schneeflocken' } ,
'71' = > { s = > '1' , icon = > 'weather_snow_light' , txtd = > 'leichter Schneefall' } ,
'72' = > { s = > '0' , icon = > 'weather_snow' , txtd = > 'unterbrochener mäßiger Schneefall' } ,
'73' = > { s = > '1' , icon = > 'weather_snow' , txtd = > 'mäßiger Schneefall' } ,
'74' = > { s = > '0' , icon = > 'weather_snow_heavy' , txtd = > 'unterbrochener starker Schneefall' } ,
'75' = > { s = > '1' , icon = > 'weather_snow_heavy' , txtd = > 'starker Schneefall' } ,
'76' = > { s = > '0' , icon = > 'weather_frost' , txtd = > 'Eisnadeln (Polarschnee)' } ,
'77' = > { s = > '1' , icon = > 'weather_frost' , txtd = > 'Schneegriesel' } ,
'78' = > { s = > '0' , icon = > 'weather_frost' , txtd = > 'Schneekristalle' } ,
'79' = > { s = > '0' , icon = > 'weather_frost' , txtd = > 'Eiskörner (gefrorene Regentropfen)' } ,
'80' = > { s = > '1' , icon = > 'weather_rain_light' , txtd = > 'leichter Regenschauer' } ,
'81' = > { s = > '1' , icon = > 'weather_rain' , txtd = > 'mäßiger oder starkerRegenschauer' } ,
'82' = > { s = > '1' , icon = > 'weather_rain_heavy' , txtd = > 'sehr starker Regenschauer' } ,
'83' = > { s = > '0' , icon = > 'weather_snow' , txtd = > 'mäßiger oder starker Schneeregenschauer' } ,
'84' = > { s = > '0' , icon = > 'weather_snow_light' , txtd = > 'leichter Schneeschauer' } ,
'85' = > { s = > '1' , icon = > 'weather_snow_light' , txtd = > 'leichter Schneeschauer' } ,
'86' = > { s = > '1' , icon = > 'weather_snow_heavy' , txtd = > 'mäßiger oder starker Schneeschauer' } ,
'87' = > { s = > '0' , icon = > 'weather_snow_heavy' , txtd = > 'mäßiger oder starker Graupelschauer' } ,
'88' = > { s = > '0' , icon = > 'unknown' , txtd = > 'leichter Hagelschauer' } ,
'89' = > { s = > '0' , icon = > 'unknown' , txtd = > 'mäßiger oder starker Hagelschauer' } ,
'90' = > { s = > '0' , icon = > 'weather_thunderstorm' , txtd = > '' } ,
'91' = > { s = > '0' , icon = > 'weather_storm' , txtd = > '' } ,
'92' = > { s = > '0' , icon = > 'weather_thunderstorm' , txtd = > '' } ,
'93' = > { s = > '0' , icon = > 'weather_thunderstorm' , txtd = > '' } ,
'94' = > { s = > '0' , icon = > 'weather_thunderstorm' , txtd = > '' } ,
'95' = > { s = > '1' , icon = > 'weather_thunderstorm' , txtd = > 'leichtes oder mäßiges Gewitter ohne Graupel oder Hagel' } ,
'96' = > { s = > '1' , icon = > 'weather_storm' , txtd = > 'starkes Gewitter ohne Graupel oder Hagel,Gewitter mit Graupel oder Hagel' } ,
'97' = > { s = > '0' , icon = > 'weather_storm' , txtd = > 'starkes Gewitter mit Regen oder Schnee' } ,
'98' = > { s = > '0' , icon = > 'weather_storm' , txtd = > 'starkes Gewitter mit Sandsturm' } ,
'99' = > { s = > '1' , icon = > 'weather_storm' , txtd = > 'starkes Gewitter mit Graupel oder Hagel' } ,
) ;
2021-01-17 19:13:02 +00:00
my @ chours = ( 5 .. 21 ) ; # Stunden des Tages mit möglichen Korrekturwerten
my $ defpvme = 16.52 ; # default Wirkungsgrad Solarmodule
my $ definve = 98.3 ; # default Wirkungsgrad Wechselrichter
my $ kJtokWh = 0.00027778 ; # Umrechnungsfaktor kJ in kWh
my $ defmaxvar = 0.5 ; # max. Varianz pro Tagesberechnung Autokorrekturfaktor
my $ definterval = 70 ; # Standard Abfrageintervall
my $ pvhcache = $ attr { global } { modpath } . "/FHEM/FhemUtils/PVH_SolarForecast_" ; # Filename-Fragment für PV History (wird mit Devicename ergänzt)
2021-01-20 08:49:39 +00:00
my $ calcmaxd = 30 ; # Anzahl Tage für Durchschnittermittlung zur Vorhersagekorrektur
2021-01-23 11:46:54 +00:00
my @ dwdattrmust = qw( Rad1h TTT Neff R101 ww SunUp SunRise SunSet ) ; # Werte die im Attr forecastProperties des DWD_Opendata Devices mindestens gesetzt sein müssen
2021-01-22 20:16:33 +00:00
2021-01-22 14:18:33 +00:00
my $ cloudslope = 0.55 ; # Steilheit des Korrekturfaktors bzgl. effektiver Bewölkung, siehe: https://www.energie-experten.org/erneuerbare-energien/photovoltaik/planung/sonnenstunden
my $ cloud_base = 0 ; # Fußpunktverschiebung bzgl. effektiver Bewölkung
2021-01-17 19:13:02 +00:00
2021-01-22 20:16:33 +00:00
my $ rainslope = 0.30 ; # Steilheit des Korrekturfaktors bzgl. Niederschlag (R101)
my $ rain_base = 0 ; # Fußpunktverschiebung bzgl. effektiver Bewölkung
2021-01-20 21:00:34 +00:00
my @ consdays = qw( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ) ; # Anzahl Tage für Attr numHistDays
2020-12-13 17:29:15 +00:00
################################################################
# Init Fn
################################################################
sub Initialize {
my ( $ hash ) = @ _ ;
2021-01-20 21:00:34 +00:00
my $ fwd = join "," , devspec2array ( "TYPE=FHEMWEB:FILTER=STATE=Initialized" ) ;
my $ cda = join "," , @ consdays ;
2020-12-13 17:29:15 +00:00
$ hash - > { DefFn } = \ & Define ;
$ hash - > { GetFn } = \ & Get ;
$ hash - > { SetFn } = \ & Set ;
2021-01-17 19:13:02 +00:00
$ hash - > { DeleteFn } = \ & Delete ;
2020-12-13 17:29:15 +00:00
$ hash - > { FW_summaryFn } = \ & FwFn ;
$ hash - > { FW_detailFn } = \ & FwFn ;
2021-01-17 19:13:02 +00:00
$ hash - > { ShutdownFn } = \ & Shutdown ;
2020-12-13 17:29:15 +00:00
$ hash - > { AttrFn } = \ & Attr ;
$ hash - > { NotifyFn } = \ & Notify ;
$ hash - > { AttrList } = "autoRefresh:selectnumbers,120,0.2,1800,0,log10 " .
"autoRefreshFW:$fwd " .
"beamColor:colorpicker,RGB " .
"beamColor2:colorpicker,RGB " .
"beamHeight " .
"beamWidth " .
# "consumerList ".
# "consumerLegend:none,icon_top,icon_bottom,text_top,text_bottom ".
# "consumerAdviceIcon ".
"disable:1,0 " .
"forcePageRefresh:1,0 " .
"headerAlignment:center,left,right " .
"headerDetail:all,co,pv,pvco,statusLink " .
"hourCount:slider,4,1,24 " .
"hourStyle " .
"htmlStart " .
"htmlEnd " .
2020-12-15 13:41:10 +00:00
"interval " .
2020-12-27 20:04:17 +00:00
"layoutType:pv,co,pvco,diff " .
"maxVariancePerDay " .
"maxPV " .
2021-01-20 21:00:34 +00:00
"numHistDays:$cda " .
2020-12-13 17:29:15 +00:00
"showDiff:no,top,bottom " .
"showHeader:1,0 " .
"showLink:1,0 " .
"showNight:1,0 " .
"showWeather:1,0 " .
"spaceSize " .
"Wh/kWh:Wh,kWh " .
2021-01-01 19:44:20 +00:00
"weatherColor:colorpicker,RGB " .
"weatherColor_night:colorpicker,RGB " .
2020-12-13 17:29:15 +00:00
$ readingFnAttributes ;
$ hash - > { FW_hideDisplayName } = 1 ; # Forum 88667
# $hash->{FW_addDetailToSummary} = 1;
# $hash->{FW_atPageEnd} = 1; # wenn 1 -> kein Longpoll ohne informid in HTML-Tag
eval { FHEM::Meta:: InitMod ( __FILE__ , $ hash ) } ; # für Meta.pm (https://forum.fhem.de/index.php/topic,97589.0.html)
return ;
}
###############################################################
# SolarForecast Define
###############################################################
sub Define {
my ( $ hash , $ def ) = @ _ ;
my @ a = split ( /\s+/x , $ def ) ;
2021-01-17 19:13:02 +00:00
return "Error: Perl module " . $ jsonabs . " is missing. Install it on Debian with: sudo apt-get install libjson-perl" if ( $ jsonabs ) ;
2020-12-13 17:29:15 +00:00
2021-01-17 19:13:02 +00:00
my $ name = $ hash - > { NAME } ;
my $ type = $ hash - > { TYPE } ;
2020-12-13 17:29:15 +00:00
$ hash - > { HELPER } { MODMETAABSENT } = 1 if ( $ modMetaAbsent ) ; # Modul Meta.pm nicht vorhanden
2021-01-17 19:13:02 +00:00
my $ params = {
hash = > $ hash ,
notes = > \ % vNotesIntern ,
useAPI = > 0 ,
useSMUtils = > 1 ,
useErrCodes = > 0
} ;
use version 0.77 ; our $ VERSION = moduleVersion ( $ params ) ; # Versionsinformationen setzen
2020-12-13 17:29:15 +00:00
createNotifyDev ( $ hash ) ;
2021-01-17 19:13:02 +00:00
my $ file = $ pvhcache . $ name ;
my ( $ error , @ content ) = FileRead ( $ file ) ; # Cache File der PV History lesen wenn vorhanden
if ( ! $ error ) {
my $ json = join "" , @ content ;
my $ success = evaljson ( $ hash , $ json ) ;
if ( $ success ) {
$ data { $ type } { $ name } { pvhist } = decode_json ( $ json ) ;
}
else {
Log3 ( $ name , 2 , qq{ $name - WARNING - the content of file "$file" is not readable and may be corrupt } ) ;
}
}
2020-12-15 13:41:10 +00:00
readingsSingleUpdate ( $ hash , "state" , "initialized" , 1 ) ;
centralTask ( $ hash ) ; # Einstieg in Abfrage
2020-12-13 17:29:15 +00:00
return ;
}
###############################################################
# SolarForecast Set
###############################################################
sub Set {
my ( $ hash , @ a ) = @ _ ;
return "\"set X\" needs at least an argument" if ( @ a < 2 ) ;
my $ name = shift @ a ;
my $ opt = shift @ a ;
my $ arg = join " " , map { my $ p = $ _ ; $ p =~ s/\s//xg ; $ p ; } @ a ; ## no critic 'Map blocks'
my $ prop = shift @ a ;
my $ prop1 = shift @ a ;
my ( $ setlist , @ fcdevs , @ indevs , @ medevs , @ cfs ) ;
my ( $ fcd , $ ind , $ med , $ cf ) = ( "" , "" , "" , "" ) ;
return if ( IsDisabled ( $ name ) ) ;
@ fcdevs = devspec2array ( "TYPE=DWD_OpenData" ) ;
$ fcd = join "," , @ fcdevs if ( @ fcdevs ) ;
for my $ h ( @ chours ) {
push @ cfs , "pvCorrectionFactor_" . sprintf ( "%02d" , $ h ) ;
}
$ cf = join " " , @ cfs if ( @ cfs ) ;
2020-12-16 19:15:48 +00:00
my $ tilt = join "," , sort keys % htilt ;
2020-12-13 17:29:15 +00:00
$ setlist = "Unknown argument $opt, choose one of " .
2020-12-27 18:51:53 +00:00
"currentForecastDev:$fcd " .
"currentInverterDev:textField-long " .
"currentMeterDev:textField-long " .
2020-12-15 13:41:10 +00:00
"inverterEfficiency " .
2020-12-13 17:29:15 +00:00
"moduleArea " .
"moduleEfficiency " .
2020-12-16 19:15:48 +00:00
"moduleTiltAngle:$tilt " .
2020-12-15 13:41:10 +00:00
"pvCorrectionFactor_Auto:on,off " .
2021-01-23 08:01:53 +00:00
"reset:currentForecastDev,currentInverterDev,currentMeterDev,pvHistory " .
2020-12-13 17:29:15 +00:00
$ cf
;
my $ params = {
hash = > $ hash ,
name = > $ name ,
opt = > $ opt ,
2020-12-15 13:41:10 +00:00
arg = > $ arg ,
2020-12-13 17:29:15 +00:00
prop = > $ prop ,
prop1 = > $ prop1
} ;
if ( $ hset { $ opt } && defined & { $ hset { $ opt } { fn } } ) {
my $ ret = q{ } ;
$ ret = & { $ hset { $ opt } { fn } } ( $ params ) ;
return $ ret ;
}
return "$setlist" ;
}
################################################################
2020-12-27 18:51:53 +00:00
# Setter currentForecastDev
2020-12-13 17:29:15 +00:00
################################################################
2020-12-27 18:51:53 +00:00
sub _setcurrentForecastDev { ## no critic "not used"
2020-12-13 17:29:15 +00:00
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
my $ prop = $ paref - > { prop } // return qq{ no PV forecast device specified } ;
if ( ! $ defs { $ prop } || $ defs { $ prop } { TYPE } ne "DWD_OpenData" ) {
2021-01-01 14:36:19 +00:00
return qq{ Forecast device "$prop" doesn't exist or has no TYPE "DWD_OpenData" } ; #' :)
2020-12-13 17:29:15 +00:00
}
readingsSingleUpdate ( $ hash , "currentForecastDev" , $ prop , 1 ) ;
createNotifyDev ( $ hash ) ;
return ;
}
################################################################
2020-12-27 18:51:53 +00:00
# Setter currentInverterDev
2020-12-13 17:29:15 +00:00
################################################################
sub _setinverterDevice { ## no critic "not used"
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
2020-12-15 13:41:10 +00:00
my $ opt = $ paref - > { opt } ;
my $ arg = $ paref - > { arg } ;
2020-12-13 17:29:15 +00:00
2020-12-15 13:41:10 +00:00
if ( ! $ arg ) {
return qq{ The command "$opt" needs an argument ! } ;
}
my ( $ a , $ h ) = parseParams ( $ arg ) ;
my $ indev = $ a - > [ 0 ] // "" ;
if ( ! $ indev || ! $ defs { $ indev } ) {
return qq{ The device "$indev" doesn't exist! } ;
2020-12-13 17:29:15 +00:00
}
2021-01-21 18:23:21 +00:00
if ( ! $ h - > { pv } || ! $ h - > { etoday } ) {
return qq{ The syntax of "$opt" isn't right. Please consider the commandref. } ;
}
2020-12-13 17:29:15 +00:00
2020-12-15 13:41:10 +00:00
readingsSingleUpdate ( $ hash , "currentInverterDev" , $ arg , 1 ) ;
2020-12-13 17:29:15 +00:00
createNotifyDev ( $ hash ) ;
return ;
}
################################################################
2020-12-27 18:51:53 +00:00
# Setter currentMeterDev
2020-12-13 17:29:15 +00:00
################################################################
sub _setmeterDevice { ## no critic "not used"
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
2020-12-15 13:41:10 +00:00
my $ opt = $ paref - > { opt } ;
my $ arg = $ paref - > { arg } ;
2020-12-13 17:29:15 +00:00
2020-12-15 13:41:10 +00:00
if ( ! $ arg ) {
return qq{ The command "$opt" needs an argument ! } ;
}
my ( $ a , $ h ) = parseParams ( $ arg ) ;
my $ medev = $ a - > [ 0 ] // "" ;
if ( ! $ medev || ! $ defs { $ medev } ) {
return qq{ The device "$medev" doesn't exist! } ;
2020-12-13 17:29:15 +00:00
}
2021-01-21 18:23:21 +00:00
if ( ! $ h - > { gcon } ) {
return qq{ The syntax of "$opt" isn't right. Please consider the commandref. } ;
}
2020-12-13 17:29:15 +00:00
2020-12-15 13:41:10 +00:00
readingsSingleUpdate ( $ hash , "currentMeterDev" , $ arg , 1 ) ;
2020-12-13 17:29:15 +00:00
createNotifyDev ( $ hash ) ;
return ;
}
################################################################
# Setter moduleArea
################################################################
sub _setmoduleArea { ## no critic "not used"
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
my $ prop = $ paref - > { prop } // return qq{ no PV module area specified } ;
if ( $ prop !~ /[0-9,.]/x ) {
return qq{ The module area must be specified by numbers and optionally with decimal places } ;
}
$ prop =~ s/,/./x ;
2020-12-27 18:51:53 +00:00
readingsSingleUpdate ( $ hash , "moduleArea" , $ prop . " qm" , 1 ) ;
return ;
}
################################################################
# Setter reset
################################################################
sub _setreset { ## no critic "not used"
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
2021-01-23 08:01:53 +00:00
my $ prop = $ paref - > { prop } // return qq{ no source specified for reset } ;
if ( $ prop eq "pvHistory" ) {
my $ type = $ hash - > { TYPE } ;
delete $ data { $ type } { $ name } { pvhist } ;
return ;
}
2020-12-27 18:51:53 +00:00
readingsDelete ( $ hash , $ prop ) ;
if ( $ prop eq "currentMeterDev" ) {
readingsDelete ( $ hash , "Current_GridConsumption" ) ;
}
if ( $ prop eq "currentInverterDev" ) {
readingsDelete ( $ hash , "Current_PV" ) ;
deleteReadingspec ( $ hash , ".*_PVreal" ) ;
}
2020-12-27 20:04:17 +00:00
createNotifyDev ( $ hash ) ;
2020-12-13 17:29:15 +00:00
return ;
}
################################################################
# Setter moduleEfficiency
################################################################
sub _setmoduleEfficiency { ## no critic "not used"
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
my $ prop = $ paref - > { prop } // return qq{ no PV module efficiency specified } ;
if ( $ prop !~ /[0-9,.]/x ) {
return qq{ The module efficiency must be specified by numbers and optionally with decimal places } ;
}
$ prop =~ s/,/./x ;
readingsSingleUpdate ( $ hash , "moduleEfficiency" , $ prop , 1 ) ;
return ;
}
2020-12-16 19:15:48 +00:00
################################################################
# Setter moduleTiltAngle
################################################################
2020-12-27 20:04:17 +00:00
sub _setmoduleTiltAngle { ## no critic "not used"
2020-12-16 19:15:48 +00:00
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
my $ prop = $ paref - > { prop } // return qq{ no tilt angle was provided } ;
if ( $ prop !~ /[0-9]/x ) {
return qq{ The tilt angle must be specified by numbers } ;
}
readingsSingleUpdate ( $ hash , "moduleTiltAngle" , $ prop , 1 ) ;
return ;
}
2020-12-13 17:29:15 +00:00
################################################################
# Setter inverterEfficiency
################################################################
sub _setinverterEfficiency { ## no critic "not used"
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
my $ prop = $ paref - > { prop } // return qq{ no inverter efficiency specified } ;
if ( $ prop !~ /[0-9,.]/x ) {
return qq{ The inverter efficiency must be specified by numbers and optionally with decimal places } ;
}
$ prop =~ s/,/./x ;
readingsSingleUpdate ( $ hash , "inverterEfficiency" , $ prop , 1 ) ;
return ;
}
################################################################
# Setter pvCorrectionFactor
################################################################
sub _setpvCorrectionFactor { ## no critic "not used"
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
my $ opt = $ paref - > { opt } ;
my $ prop = $ paref - > { prop } // return qq{ no correction value specified } ;
if ( $ prop !~ /[0-9,.]/x ) {
return qq{ The correction value must be specified by numbers and optionally with decimal places } ;
}
$ prop =~ s/,/./x ;
2020-12-17 08:44:29 +00:00
readingsSingleUpdate ( $ hash , $ opt , $ prop . " (manual)" , 1 ) ;
2020-12-13 17:29:15 +00:00
my @ da ;
my $ t = time ; # aktuelle Unix-Zeit
my $ chour = strftime "%H" , localtime ( $ t ) ; # aktuelle Stunde
my $ fcdev = ReadingsVal ( $ name , "currentForecastDev" , "" ) ; # aktuelles Forecast Device
my $ params = {
2020-12-17 15:52:48 +00:00
hash = > $ hash ,
name = > $ name ,
2020-12-13 17:29:15 +00:00
t = > $ t ,
chour = > $ chour ,
daref = > \ @ da
} ;
2020-12-27 18:51:53 +00:00
_transferDWDForecastValues ( $ params ) ;
2020-12-13 17:29:15 +00:00
if ( @ da ) {
push @ da , "state:updated" ; # Abschluß state
2020-12-27 18:51:53 +00:00
createReadingsFromArray ( $ hash , \ @ da , 1 ) ;
2020-12-13 17:29:15 +00:00
}
return ;
}
2020-12-15 13:41:10 +00:00
################################################################
# Setter pvCorrectionFactor_Auto
################################################################
sub _setpvCorrectionFactorAuto { ## no critic "not used"
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ opt = $ paref - > { opt } ;
my $ prop = $ paref - > { prop } // return qq{ no correction value specified } ;
readingsSingleUpdate ( $ hash , "pvCorrectionFactor_Auto" , $ prop , 1 ) ;
2021-01-22 09:51:04 +00:00
if ( $ prop eq "off" ) {
deleteReadingspec ( $ hash , "pvCorrectionFactor_.*_autocalc" ) ;
}
2020-12-15 13:41:10 +00:00
return ;
}
2020-12-13 17:29:15 +00:00
###############################################################
# SolarForecast Get
###############################################################
sub Get {
2021-01-17 19:13:02 +00:00
my ( $ hash , @ a ) = @ _ ;
return "\"get X\" needs at least an argument" if ( @ a < 2 ) ;
my $ name = shift @ a ;
my $ opt = shift @ a ;
my $ arg = join " " , map { my $ p = $ _ ; $ p =~ s/\s//xg ; $ p ; } @ a ; ## no critic 'Map blocks'
2020-12-13 17:29:15 +00:00
2021-01-17 19:13:02 +00:00
my $ getlist = "Unknown argument $opt, choose one of " .
"data:noArg " .
"html:noArg " .
"pvHistory:noArg "
;
return if ( IsDisabled ( $ name ) ) ;
my $ params = {
hash = > $ hash ,
name = > $ name ,
opt = > $ opt ,
arg = > $ arg
} ;
if ( $ hget { $ opt } && defined & { $ hget { $ opt } { fn } } ) {
my $ ret = q{ } ;
if ( ! $ hash - > { CREDENTIALS } && $ hget { $ opt } { needcred } ) {
return qq{ Credentials of $name are not set." } ;
}
$ ret = & { $ hget { $ opt } { fn } } ( $ params ) ;
return $ ret ;
}
return $ getlist ;
}
###############################################################
# Getter data
###############################################################
sub _getdata {
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
return centralTask ( $ hash ) ;
}
###############################################################
# Getter html
###############################################################
sub _gethtml {
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
return pageAsHtml ( $ hash ) ;
}
###############################################################
# Getter ftui
# ohne Eintrag in Get-Liste
###############################################################
sub _getftui {
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
return pageAsHtml ( $ hash , "ftui" ) ;
}
###############################################################
# Getter listPVHistory
###############################################################
sub _getlistPVHistory {
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
2020-12-13 17:29:15 +00:00
2021-01-17 19:13:02 +00:00
my $ ret = listPVHistory ( $ hash ) ;
return $ ret ;
2020-12-13 17:29:15 +00:00
}
################################################################
sub Attr {
my ( $ cmd , $ name , $ aName , $ aVal ) = @ _ ;
my $ hash = $ defs { $ name } ;
my ( $ do , $ val ) ;
# $cmd can be "del" or "set"
# $name is device name
# aName and aVal are Attribute name and value
if ( $ aName eq "disable" ) {
if ( $ cmd eq "set" ) {
$ do = ( $ aVal ) ? 1 : 0 ;
}
2021-01-20 21:00:34 +00:00
$ do = 0 if ( $ cmd eq "del" ) ;
2020-12-13 17:29:15 +00:00
$ val = ( $ do == 1 ? "disabled" : "initialized" ) ;
readingsSingleUpdate ( $ hash , "state" , $ val , 1 ) ;
}
if ( $ aName eq "icon" ) {
$ _ [ 2 ] = "consumerAdviceIcon" ;
}
2020-12-15 13:41:10 +00:00
if ( $ cmd eq "set" ) {
if ( $ aName eq "interval" ) {
unless ( $ aVal =~ /^[0-9]+$/x ) { return "The value for $aName is not valid. Use only figures 0-9 !" ; }
InternalTimer ( gettimeofday ( ) + 1.0 , "FHEM::SolarForecast::centralTask" , $ hash , 0 ) ;
2020-12-27 20:04:17 +00:00
}
if ( $ aName eq "maxVariancePerDay" ) {
unless ( $ aVal =~ /^[0-9.]+$/x ) { return "The value for $aName is not valid. Use only numbers with optional decimal places !" ; }
}
2020-12-15 13:41:10 +00:00
}
2020-12-13 17:29:15 +00:00
return ;
}
###################################################################################
# Eventverarbeitung
###################################################################################
sub Notify {
# Es werden nur die Events von Geräten verarbeitet die im Hash $hash->{NOTIFYDEV} gelistet sind (wenn definiert).
# Dadurch kann die Menge der Events verringert werden. In sub DbRep_Define angeben.
my $ myHash = shift ;
my $ dev_hash = shift ;
2021-01-23 08:01:53 +00:00
my $ myName = $ myHash - > { NAME } ; # Name des eigenen Devices
my $ devName = $ dev_hash - > { NAME } ; # Device welches Events erzeugt hat
2020-12-13 17:29:15 +00:00
return if ( IsDisabled ( $ myName ) || ! $ myHash - > { NOTIFYDEV } ) ;
my $ events = deviceEvents ( $ dev_hash , 1 ) ;
return if ( ! $ events ) ;
2020-12-15 13:41:10 +00:00
return ;
}
2020-12-13 17:29:15 +00:00
2021-01-17 19:13:02 +00:00
################################################################
# Shutdown
################################################################
sub Shutdown {
my $ hash = shift ;
my $ name = $ hash - > { NAME } ;
my $ type = $ hash - > { TYPE } ;
if ( $ data { $ type } { $ name } { pvhist } ) { # Cache File für PV History schreiben
my @ pvh ;
my $ json = encode_json ( $ data { $ type } { $ name } { pvhist } ) ;
push @ pvh , $ json ;
my $ file = $ pvhcache . $ name ;
my $ error = FileWrite ( $ file , @ pvh ) ;
if ( $ error ) {
Log3 ( $ name , 2 , qq{ $name - ERROR writing cache file "$file": $error } ) ;
}
}
return ;
}
#################################################################
# Wenn ein Gerät in FHEM gelöscht wird, wird zuerst die Funktion
# X_Undef aufgerufen um offene Verbindungen zu schließen,
# anschließend wird die Funktion X_Delete aufgerufen.
# Funktion: Aufräumen von dauerhaften Daten, welche durch das
# Modul evtl. für dieses Gerät spezifisch erstellt worden sind.
# Es geht hier also eher darum, alle Spuren sowohl im laufenden
# FHEM-Prozess, als auch dauerhafte Daten bspw. im physikalischen
# Gerät zu löschen die mit dieser Gerätedefinition zu tun haben.
#################################################################
sub Delete {
my $ hash = shift ;
my $ arg = shift ;
my $ name = $ hash - > { NAME } ;
my $ file = $ pvhcache . $ name ;
my $ error = FileDelete ( $ file ) ; # Cache File der PV History löschen
if ( $ error ) {
Log3 ( $ name , 2 , qq{ $name - ERROR deleting cache file "$file": $error } ) ;
}
return ;
}
2020-12-15 13:41:10 +00:00
################################################################
# Zentraler Datenabruf
################################################################
sub centralTask {
my $ hash = shift ;
my $ name = $ hash - > { NAME } ; # Name des eigenen Devices
2020-12-13 17:29:15 +00:00
2020-12-15 13:41:10 +00:00
RemoveInternalTimer ( $ hash , "FHEM::SolarForecast::centralTask" ) ;
my $ interval = controlParams ( $ name ) ;
2020-12-13 17:29:15 +00:00
2020-12-15 13:41:10 +00:00
if ( $ init_done == 1 ) {
if ( ! $ interval ) {
$ hash - > { MODE } = "Manual" ;
}
else {
my $ new = gettimeofday ( ) + $ interval ;
InternalTimer ( $ new , "FHEM::SolarForecast::centralTask" , $ hash , 0 ) ; # Wiederholungsintervall
$ hash - > { MODE } = "Automatic - next polltime: " . FmtTime ( $ new ) ;
2020-12-13 17:29:15 +00:00
}
2020-12-15 13:41:10 +00:00
return if ( IsDisabled ( $ name ) ) ;
readingsSingleUpdate ( $ hash , "state" , "running" , 1 ) ;
my @ da ;
my $ t = time ; # aktuelle Unix-Zeit
my $ chour = strftime "%H" , localtime ( $ t ) ; # aktuelle Stunde
my $ params = {
2020-12-17 15:52:48 +00:00
hash = > $ hash ,
name = > $ name ,
t = > $ t ,
chour = > $ chour ,
daref = > \ @ da
2020-12-15 13:41:10 +00:00
} ;
2021-01-23 11:46:54 +00:00
Log3 ( $ name , 5 , "$name - ################################################################" ) ;
Log3 ( $ name , 5 , "$name - ### New data collection cycle ###" ) ;
Log3 ( $ name , 5 , "$name - ################################################################" ) ;
2020-12-27 18:51:53 +00:00
_transferDWDForecastValues ( $ params ) ; # Forecast Werte übertragen
2021-01-23 11:46:54 +00:00
_transferWeatherValues ( $ params ) ; # Wetterwerte übertragen
2020-12-27 18:51:53 +00:00
_transferInverterValues ( $ params ) ; # WR Werte übertragen
_transferMeterValues ( $ params ) ;
2020-12-15 13:41:10 +00:00
if ( @ da ) {
2020-12-27 18:51:53 +00:00
createReadingsFromArray ( $ hash , \ @ da , 1 ) ;
2020-12-13 17:29:15 +00:00
}
2020-12-15 13:41:10 +00:00
2020-12-27 18:51:53 +00:00
sumNextHours ( $ hash , $ chour , \ @ da ) ; # Zusammenfassung nächste 4 Stunden erstellen
calcVariance ( $ params ) ; # Autokorrektur berechnen
2020-12-15 13:41:10 +00:00
2020-12-27 18:51:53 +00:00
readingsSingleUpdate ( $ hash , "state" , "updated" , 1 ) ; # Abschluß state
2020-12-13 17:29:15 +00:00
}
2020-12-15 13:41:10 +00:00
else {
InternalTimer ( gettimeofday ( ) + 5 , "FHEM::SolarForecast::centralTask" , $ hash , 0 ) ;
2020-12-13 17:29:15 +00:00
}
return ;
}
2020-12-15 13:41:10 +00:00
################################################################
# Steuerparameter berechnen / festlegen
################################################################
sub controlParams {
my $ name = shift ;
my $ interval = AttrVal ( $ name , "interval" , $ definterval ) ; # 0 wenn manuell gesteuert
return $ interval ;
}
2020-12-13 17:29:15 +00:00
################################################################
2020-12-27 18:51:53 +00:00
# Forecast Werte Device (DWD_OpenData) ermitteln und
# übertragen
2020-12-13 17:29:15 +00:00
################################################################
2020-12-27 18:51:53 +00:00
sub _transferDWDForecastValues {
2020-12-17 15:52:48 +00:00
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
my $ t = $ paref - > { t } ;
my $ chour = $ paref - > { chour } ;
my $ daref = $ paref - > { daref } ;
2020-12-13 17:29:15 +00:00
2020-12-17 15:52:48 +00:00
my $ fcname = ReadingsVal ( $ name , "currentForecastDev" , "" ) ; # aktuelles Forecast Device
2020-12-15 13:41:10 +00:00
return if ( ! $ fcname || ! $ defs { $ fcname } ) ;
2020-12-13 17:29:15 +00:00
2020-12-27 18:51:53 +00:00
my ( $ time_str , $ epoche ) ;
2020-12-13 17:29:15 +00:00
2021-01-23 11:46:54 +00:00
my @ aneeded = checkdwdattr ( $ fcname ) ;
if ( @ aneeded ) {
Log3 ( $ name , 2 , qq{ $name - ERROR - the attribute "forecastProperties" of device "$fcname" needs to contain: } . join "," , @ aneeded ) ;
}
2020-12-31 09:09:49 +00:00
# deleteReadingspec ($hash, "NextHour.*");
2020-12-16 09:15:37 +00:00
2020-12-15 21:27:21 +00:00
for my $ num ( 0 .. 47 ) {
2020-12-13 17:29:15 +00:00
my $ fh = $ chour + $ num ;
2020-12-27 18:51:53 +00:00
my $ fd = int ( $ fh / 24 ) ;
2020-12-15 21:27:21 +00:00
$ fh = $ fh - ( $ fd * 24 ) ;
2020-12-13 17:29:15 +00:00
2020-12-15 21:27:21 +00:00
next if ( $ fd > 1 ) ;
2020-12-13 17:29:15 +00:00
2020-12-27 18:51:53 +00:00
my $ v = ReadingsVal ( $ fcname , "fc${fd}_${fh}_Rad1h" , 0 ) ;
2020-12-13 17:29:15 +00:00
2020-12-27 18:51:53 +00:00
Log3 ( $ name , 5 , "$name - collect DWD forecast data: device=$fcname, rad=fc${fd}_${fh}_Rad1h, Val=$v" ) ;
2020-12-16 19:15:48 +00:00
2020-12-13 17:29:15 +00:00
if ( $ num == 0 ) {
$ time_str = "ThisHour" ;
$ epoche = $ t ; # Epoche Zeit
}
else {
$ time_str = "NextHour" . sprintf "%02d" , $ num ;
$ epoche = $ t + ( 3600 * $ num ) ;
}
2020-12-17 15:52:48 +00:00
my $ calcpv = calcPVforecast ( $ name , $ v , $ fh ) ; # Vorhersage gewichtet kalkulieren
2020-12-13 17:29:15 +00:00
2020-12-16 19:15:48 +00:00
push @$ daref , "${time_str}_PVforecast:" . $ calcpv . " Wh" ;
2020-12-27 18:51:53 +00:00
push @$ daref , "${time_str}_Time:" . TimeAdjust ( $ epoche ) ; # Zeit fortschreiben
2020-12-13 17:29:15 +00:00
2021-01-20 08:49:39 +00:00
$ hash - > { HELPER } { "fc${fd}_" . sprintf ( "%02d" , $ fh ) . "_PVforecast" } = $ v . " kJ/m2" ; # original Vorhersage Strahlungsdaten zur Berechnung Auto-Korrekturfaktor in Helper speichern
2020-12-13 17:29:15 +00:00
2020-12-16 19:15:48 +00:00
if ( $ fd == 0 && int $ calcpv > 0 ) { # Vorhersagedaten des aktuellen Tages zum manuellen Vergleich in Reading speichern
2021-01-17 19:13:02 +00:00
push @$ daref , "Today_Hour" . sprintf ( "%02d" , $ fh ) . "_PVforecast:$calcpv Wh" ;
}
if ( $ fd == 0 && sprintf ( "%02d" , $ fh ) eq $ chour ) {
$ paref - > { calcpv } = $ calcpv ;
setPVhistory ( $ paref ) ;
2020-12-16 15:48:38 +00:00
}
2020-12-27 18:51:53 +00:00
}
return ;
}
################################################################
# Wetter Werte aus dem angebenen Wetterdevice extrahieren
################################################################
2021-01-23 11:46:54 +00:00
sub _transferWeatherValues {
2020-12-27 18:51:53 +00:00
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
my $ t = $ paref - > { t } ;
my $ chour = $ paref - > { chour } ;
my $ daref = $ paref - > { daref } ;
my $ fcname = ReadingsVal ( $ name , "currentForecastDev" , "" ) ; # aktuelles Forecast Device
return if ( ! $ fcname || ! $ defs { $ fcname } ) ;
my ( $ time_str , $ epoche ) ;
my $ fc0_SunRise = ReadingsVal ( $ fcname , "fc0_SunRise" , "00:00" ) ; # Sonnenaufgang heute
my $ fc0_SunSet = ReadingsVal ( $ fcname , "fc0_SunSet" , "00:00" ) ; # Sonnenuntergang heute
my $ fc1_SunRise = ReadingsVal ( $ fcname , "fc1_SunRise" , "00:00" ) ; # Sonnenaufgang morgen
my $ fc1_SunSet = ReadingsVal ( $ fcname , "fc1_SunSet" , "00:00" ) ; # Sonnenuntergang morgen
push @$ daref , "Today_SunRise:" . $ fc0_SunRise ;
push @$ daref , "Today_SunSet:" . $ fc0_SunSet ;
push @$ daref , "Tomorrow_SunRise:" . $ fc1_SunRise ;
push @$ daref , "Tomorrow_SunSet:" . $ fc1_SunSet ;
2020-12-31 09:09:49 +00:00
my $ fc0_SunRise_round = ( sprintf "%02d" , ( split ":" , $ fc0_SunRise ) [ 0 ] - 1 ) ;
my $ fc0_SunSet_round = ( sprintf "%02d" , ( split ":" , $ fc0_SunSet ) [ 0 ] + 1 ) ;
2020-12-27 18:51:53 +00:00
for my $ num ( 0 .. 47 ) {
my $ fh = $ chour + $ num ;
my $ fd = int ( $ fh / 24 ) ;
$ fh = $ fh - ( $ fd * 24 ) ;
2020-12-16 15:48:38 +00:00
2020-12-27 18:51:53 +00:00
next if ( $ fd > 1 ) ;
if ( $ num == 0 ) {
$ time_str = "ThisHour" ;
$ epoche = $ t ; # Epoche Zeit
}
else {
$ time_str = "NextHour" . sprintf "%02d" , $ num ;
$ epoche = $ t + ( 3600 * $ num ) ;
}
2020-12-13 17:29:15 +00:00
2021-01-21 22:07:18 +00:00
my $ wid = ReadingsNum ( $ fcname , "fc${fd}_${fh}_ww" , 99 ) ; # 55_DWD -> 0 .. 98 definiert , 99 ist nicht vorhanden # führende 0 einfügen wenn nötig
my $ neff = ReadingsNum ( $ fcname , "fc${fd}_${fh}_Neff" , 0 ) ; # Effektive Wolkendecke
2021-01-22 20:16:33 +00:00
my $ r101 = ReadingsNum ( $ fcname , "fc${fd}_${fh}_R101" , 0 ) ; # Niederschlagswahrscheinlichkeit> 0,1 mm während der letzten Stunde
2020-12-27 18:51:53 +00:00
2020-12-13 17:29:15 +00:00
my $ fhstr = sprintf "%02d" , $ fh ;
2020-12-31 09:09:49 +00:00
if ( $ fd == 0 && ( $ fhstr lt $ fc0_SunRise_round || $ fhstr gt $ fc0_SunSet_round ) ) { # Zeit vor Sonnenaufgang oder nach Sonnenuntergang heute
2021-01-01 19:44:20 +00:00
$ wid += 100 ; # "1" der WeatherID voranstellen wenn Nacht
2020-12-13 17:29:15 +00:00
}
2020-12-31 09:09:49 +00:00
elsif ( $ fd == 1 && ( $ fhstr lt $ fc0_SunRise_round || $ fhstr gt $ fc0_SunSet_round ) ) { # Zeit vor Sonnenaufgang oder nach Sonnenuntergang morgen
2021-01-01 19:44:20 +00:00
$ wid += 100 ; # "1" der WeatherID voranstellen wenn Nacht
2020-12-13 17:29:15 +00:00
}
2021-01-01 14:36:19 +00:00
my $ txt = ReadingsVal ( $ fcname , "fc${fd}_${fh}_wwd" , '' ) ;
2020-12-27 18:51:53 +00:00
2021-01-23 11:46:54 +00:00
Log3 ( $ name , 5 , "$name - collect Weather data: device=$fcname, wid=fc${fd}_${fh}_ww, val=$wid, txt=$txt, cc=$neff, rp=$r101" ) ;
2021-01-01 14:36:19 +00:00
$ hash - > { HELPER } { "${time_str}_WeatherId" } = $ wid ;
$ hash - > { HELPER } { "${time_str}_WeatherTxt" } = $ txt ;
2021-01-21 22:07:18 +00:00
$ hash - > { HELPER } { "${time_str}_CloudCover" } = $ neff ;
2021-01-22 20:16:33 +00:00
$ hash - > { HELPER } { "${time_str}_RainProb" } = $ r101 ;
2020-12-13 17:29:15 +00:00
}
return ;
}
################################################################
# Werte Inverter Device ermitteln und übertragen
################################################################
sub _transferInverterValues {
2020-12-17 15:52:48 +00:00
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
2021-01-17 19:13:02 +00:00
my $ t = $ paref - > { t } ; # aktuelle Unix-Zeit
2020-12-17 15:52:48 +00:00
my $ chour = $ paref - > { chour } ;
my $ daref = $ paref - > { daref } ;
2020-12-13 17:29:15 +00:00
2020-12-17 15:52:48 +00:00
my $ indev = ReadingsVal ( $ name , "currentInverterDev" , "" ) ;
my ( $ a , $ h ) = parseParams ( $ indev ) ;
$ indev = $ a - > [ 0 ] // "" ;
2020-12-15 13:41:10 +00:00
return if ( ! $ indev || ! $ defs { $ indev } ) ;
2020-12-13 17:29:15 +00:00
2020-12-27 18:51:53 +00:00
my $ tlim = "00|23" ; # Stunde 00/23 -> bestimmte Aktionen
2020-12-19 07:34:35 +00:00
2020-12-16 08:11:12 +00:00
if ( $ chour =~ /^($tlim)$/x ) {
2020-12-17 15:52:48 +00:00
deleteReadingspec ( $ hash , "Today_Hour.*_PV.*" ) ;
2020-12-13 17:29:15 +00:00
}
## aktuelle PV-Erzeugung
#########################
2020-12-16 08:11:12 +00:00
my ( $ pvread , $ pvunit ) = split ":" , $ h - > { pv } ; # Readingname/Unit für aktuelle PV Erzeugung
my ( $ edread , $ edunit ) = split ":" , $ h - > { etoday } ; # Readingname/Unit für Tagesenergie
2020-12-15 13:41:10 +00:00
2020-12-17 15:52:48 +00:00
Log3 ( $ name , 5 , "$name - collect Inverter data: device=$indev, pv=$pvread ($pvunit), etoday=$edread ($edunit)" ) ;
2020-12-15 13:41:10 +00:00
my $ pvuf = $ pvunit =~ /^kW$/xi ? 1000 : 1 ;
2020-12-16 08:11:12 +00:00
my $ pv = ReadingsNum ( $ indev , $ pvread , 0 ) * $ pvuf ; # aktuelle Erzeugung (W)
2020-12-13 17:29:15 +00:00
push @$ daref , "Current_PV:" . $ pv . " W" ;
2020-12-15 13:41:10 +00:00
my $ eduf = $ edunit =~ /^kWh$/xi ? 1000 : 1 ;
2020-12-16 08:11:12 +00:00
my $ etoday = ReadingsNum ( $ indev , $ edread , 0 ) * $ eduf ; # aktuelle Erzeugung (W)
2020-12-15 13:41:10 +00:00
2020-12-17 08:44:29 +00:00
my $ edaypast = 0 ;
2020-12-25 14:14:14 +00:00
deleteReadingspec ( $ hash , "Today_Hour00_PVreal" ) ;
2020-12-16 08:11:12 +00:00
for my $ h ( 0 .. int ( $ chour ) - 1 ) { # alle bisherigen Erzeugungen des Tages summieren
2020-12-17 15:52:48 +00:00
$ edaypast += ReadingsNum ( $ name , "Today_Hour" . sprintf ( "%02d" , $ h ) . "_PVreal" , 0 ) ;
2020-12-13 17:29:15 +00:00
}
2021-01-17 19:13:02 +00:00
my $ ethishour = int ( $ etoday - $ edaypast ) ;
2020-12-13 17:29:15 +00:00
2020-12-27 18:51:53 +00:00
if ( $ chour !~ /^($tlim)$/x ) { # nicht setzen wenn Stunde 23 des Tages
if ( $ ethishour < 0 ) {
2021-01-17 19:13:02 +00:00
$ ethishour = 0 ;
2020-12-27 18:51:53 +00:00
}
2021-01-17 19:13:02 +00:00
push @$ daref , "Today_Hour" . sprintf ( "%02d" , $ chour ) . "_PVreal:" . $ ethishour . " Wh" ;
$ paref - > { ethishour } = $ ethishour ;
setPVhistory ( $ paref ) ;
2020-12-25 14:14:14 +00:00
}
2021-01-17 19:13:02 +00:00
2020-12-13 17:29:15 +00:00
return ;
}
################################################################
# Werte Meter Device ermitteln und übertragen
################################################################
sub _transferMeterValues {
2020-12-17 15:52:48 +00:00
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
my $ t = $ paref - > { t } ;
my $ chour = $ paref - > { chour } ;
my $ daref = $ paref - > { daref } ;
2020-12-13 17:29:15 +00:00
2020-12-17 15:52:48 +00:00
my $ medev = ReadingsVal ( $ name , "currentMeterDev" , "" ) ; # aktuelles Meter device
my ( $ a , $ h ) = parseParams ( $ medev ) ;
$ medev = $ a - > [ 0 ] // "" ;
2020-12-15 13:41:10 +00:00
return if ( ! $ medev || ! $ defs { $ medev } ) ;
2020-12-13 17:29:15 +00:00
## aktuelle Consumption
#########################
2020-12-15 13:41:10 +00:00
my ( $ gc , $ gcunit ) = split ":" , $ h - > { gcon } ; # Readingname/Unit für aktuellen Netzbezug
2020-12-17 15:52:48 +00:00
Log3 ( $ name , 5 , "$name - collect Meter data: device=$medev, gcon=$gc ($gcunit)" ) ;
2020-12-13 17:29:15 +00:00
2020-12-15 13:41:10 +00:00
my $ gcuf = $ gcunit =~ /^kW$/xi ? 1000 : 1 ;
my $ co = ReadingsNum ( $ medev , $ gc , 0 ) * $ gcuf ; # aktueller Bezug (-) oder Einspeisung
push @$ daref , "Current_GridConsumption:" . $ co . " W" ;
2020-12-13 17:29:15 +00:00
return ;
}
################################################################
# FHEMWEB Fn
################################################################
sub FwFn {
my ( $ FW_wname , $ d , $ room , $ pageHash ) = @ _ ; # pageHash is set for summaryFn.
2020-12-17 15:52:48 +00:00
my $ hash = $ defs { $ d } ;
2020-12-13 17:29:15 +00:00
my $ height ;
2020-12-15 13:41:10 +00:00
RemoveInternalTimer ( $ hash , \ & pageRefresh ) ;
2020-12-13 17:29:15 +00:00
$ hash - > { HELPER } { FW } = $ FW_wname ;
my $ link = forecastGraphic ( $ d ) ;
my $ alias = AttrVal ( $ d , "alias" , $ d ) ; # Linktext als Aliasname oder Devicename setzen
my $ dlink = "<a href=\"/fhem?detail=$d\">$alias</a>" ;
my $ ret = "" ;
if ( IsDisabled ( $ d ) ) {
$ height = AttrNum ( $ d , 'beamHeight' , 200 ) ;
$ ret . = "<table class='roomoverview'>" ;
$ ret . = "<tr style='height:" . $ height . "px'>" ;
$ ret . = "<td>" ;
$ ret . = "Solar forecast graphic device <a href=\"/fhem?detail=$d\">$d</a> is disabled" ;
$ ret . = "</td>" ;
$ ret . = "</tr>" ;
$ ret . = "</table>" ;
}
else {
$ ret . = "<span>$dlink </span><br>" if ( AttrVal ( $ d , "showLink" , 0 ) ) ;
$ ret . = $ link ;
}
# Autorefresh nur des aufrufenden FHEMWEB-Devices
my $ al = AttrVal ( $ d , "autoRefresh" , 0 ) ;
if ( $ al ) {
InternalTimer ( gettimeofday ( ) + $ al , \ & pageRefresh , $ hash , 0 ) ;
Log3 ( $ d , 5 , "$d - next start of autoRefresh: " . FmtDateTime ( gettimeofday ( ) + $ al ) ) ;
}
return $ ret ;
}
################################################################
sub pageRefresh {
2020-12-17 15:52:48 +00:00
my $ hash = shift ;
my $ d = $ hash - > { NAME } ;
2020-12-13 17:29:15 +00:00
# Seitenrefresh festgelegt durch SolarForecast-Attribut "autoRefresh" und "autoRefreshFW"
my $ rd = AttrVal ( $ d , "autoRefreshFW" , $ hash - > { HELPER } { FW } ) ;
{ map { FW_directNotify ( "#FHEMWEB:$_" , "location.reload('true')" , "" ) } $ rd }
my $ al = AttrVal ( $ d , "autoRefresh" , 0 ) ;
2020-12-15 13:41:10 +00:00
2020-12-13 17:29:15 +00:00
if ( $ al ) {
InternalTimer ( gettimeofday ( ) + $ al , \ & pageRefresh , $ hash , 0 ) ;
Log3 ( $ d , 5 , "$d - next start of autoRefresh: " . FmtDateTime ( gettimeofday ( ) + $ al ) ) ;
}
else {
2020-12-15 13:41:10 +00:00
RemoveInternalTimer ( $ hash , \ & pageRefresh ) ;
2020-12-13 17:29:15 +00:00
}
return ;
}
#############################################################################################
# Versionierungen des Moduls setzen
# Die Verwendung von Meta.pm und Packages wird berücksichtigt
#############################################################################################
sub setVersionInfo {
my ( $ hash ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my $ v = ( sortTopicNum ( "desc" , keys % vNotesIntern ) ) [ 0 ] ;
my $ type = $ hash - > { TYPE } ;
$ hash - > { HELPER } { PACKAGE } = __PACKAGE__ ;
$ hash - > { HELPER } { VERSION } = $ v ;
if ( $ modules { $ type } { META } { x_prereqs_src } && ! $ hash - > { HELPER } { MODMETAABSENT } ) {
# META-Daten sind vorhanden
$ modules { $ type } { META } { version } = "v" . $ v ; # Version aus META.json überschreiben, Anzeige mit {Dumper $modules{SMAPortal}{META}}
if ( $ modules { $ type } { META } { x_version } ) { # {x_version} ( nur gesetzt wenn $Id: 76_SolarForecast.pm 21735 2020-04-20 20:53:24Z DS_Starter $ im Kopf komplett! vorhanden )
$ modules { $ type } { META } { x_version } =~ s/1\.1\.1/$v/g ;
}
else {
$ modules { $ type } { META } { x_version } = $ v ;
}
return $@ unless ( FHEM::Meta:: SetInternals ( $ hash ) ) ; # FVERSION wird gesetzt ( nur gesetzt wenn $Id: 76_SolarForecast.pm 21735 2020-04-20 20:53:24Z DS_Starter $ im Kopf komplett! vorhanden )
if ( __PACKAGE__ eq "FHEM::$type" || __PACKAGE__ eq $ type ) {
# es wird mit Packages gearbeitet -> Perl übliche Modulversion setzen
# mit {<Modul>->VERSION()} im FHEMWEB kann Modulversion abgefragt werden
use version 0.77 ; our $ VERSION = FHEM::Meta:: Get ( $ hash , 'version' ) ;
}
}
else { # herkömmliche Modulstruktur
$ hash - > { VERSION } = $ v ;
}
return ;
}
################################################################
# Grafik als HTML zurück liefern (z.B. für Widget)
################################################################
sub pageAsHtml {
2020-12-17 15:52:48 +00:00
my $ hash = shift ;
my $ ftui = shift ;
my $ name = $ hash - > { NAME } ;
2020-12-13 17:29:15 +00:00
my $ height ;
my $ link = forecastGraphic ( $ name , $ ftui ) ;
my $ alias = AttrVal ( $ name , "alias" , $ name ) ; # Linktext als Aliasname oder Devicename setzen
my $ dlink = "<a href=\"/fhem?detail=$name\">$alias</a>" ;
my $ ret = "<html>" ;
if ( IsDisabled ( $ name ) ) {
$ height = AttrNum ( $ name , 'beamHeight' , 200 ) ;
$ ret . = "<table class='roomoverview'>" ;
$ ret . = "<tr style='height:" . $ height . "px'>" ;
$ ret . = "<td>" ;
$ ret . = "SMA Portal graphic device <a href=\"/fhem?detail=$name\">$name</a> is disabled" ;
$ ret . = "</td>" ;
$ ret . = "</tr>" ;
$ ret . = "</table>" ;
}
else {
$ ret . = "<span>$dlink </span><br>" if ( AttrVal ( $ name , "showLink" , 0 ) ) ;
$ ret . = $ link ;
}
$ ret . = "</html>" ;
return $ ret ;
}
###############################################################################
# Subroutine für Vorhersagegrafik
###############################################################################
sub forecastGraphic { ## no critic 'complexity'
my $ name = shift ;
my $ ftui = shift // "" ;
my $ hash = $ defs { $ name } ;
my $ ret = "" ;
my ( $ val , $ height ) ;
my ( $ z2 , $ z3 , $ z4 ) ;
my $ he ; # Balkenhöhe
2021-01-01 14:36:19 +00:00
my ( % pv , % is , % t , % we , % we_txt , % di , % co ) ; # statt zusätzlich %we_txt , we verwenden und umbauen ?
2020-12-13 17:29:15 +00:00
my @ pgCDev ;
##########################################################
# Kontext des SolarForecast-Devices speichern für Refresh
$ hash - > { HELPER } { SPGDEV } = $ name ; # Name des aufrufenden SMAPortalSPG-Devices
$ hash - > { HELPER } { SPGROOM } = $ FW_room ? $ FW_room : "" ; # Raum aus dem das SMAPortalSPG-Device die Funktion aufrief
$ hash - > { HELPER } { SPGDETAIL } = $ FW_detail ? $ FW_detail : "" ; # Name des SMAPortalSPG-Devices (wenn Detailansicht)
2020-12-15 13:41:10 +00:00
my $ fcdev = ReadingsVal ( $ name , "currentForecastDev" , "" ) ; # aktuelles Forecast Device
my $ indev = ReadingsVal ( $ name , "currentInverterDev" , "" ) ; # aktuelles Inverter Device
my ( $ a , $ h ) = parseParams ( $ indev ) ;
$ indev = $ a - > [ 0 ] // "" ;
my $ cclv = "L05" ;
2020-12-13 17:29:15 +00:00
my $ pv0 = ReadingsNum ( $ name , "ThisHour_PVforecast" , undef ) ;
my $ ma = ReadingsNum ( $ name , "moduleArea" , 0 ) ; # Solar Modulfläche (qm)
if ( ! $ fcdev || ! $ ma || ! defined $ pv0 ) {
$ height = AttrNum ( $ name , 'beamHeight' , 200 ) ;
$ ret . = "<table class='roomoverview'>" ;
$ ret . = "<tr style='height:" . $ height . "px'>" ;
$ ret . = "<td>" ;
if ( ! $ fcdev ) {
2020-12-27 18:51:53 +00:00
$ ret . = qq{ Please select a Solar Forecast device of Type "DWD_OpenData" with "set $name currentForecastDev" } ;
2020-12-13 17:29:15 +00:00
}
elsif ( ! $ indev ) {
2020-12-27 18:51:53 +00:00
$ ret . = qq{ Please select an Inverter device with "set $name currentInverterDev" } ;
2020-12-13 17:29:15 +00:00
}
elsif ( ! $ ma ) {
$ ret . = qq{ Please specify the total module area with "set $name moduleArea" } ;
}
elsif ( ! defined $ pv0 ) {
$ ret . = qq{ Awaiting data from selected Solar Forecast device ... } ;
}
$ ret . = "</td>" ;
$ ret . = "</tr>" ;
$ ret . = "</table>" ;
return $ ret ;
}
@ pgCDev = split ( ',' , AttrVal ( $ name , "consumerList" , "" ) ) ; # definierte Verbraucher ermitteln
my ( $ legend_style , $ legend ) = split ( '_' , AttrVal ( $ name , 'consumerLegend' , 'icon_top' ) ) ;
$ legend = '' if ( ( $ legend_style eq 'none' ) || ( ! int ( @ pgCDev ) ) ) ;
###################################
# Verbraucherlegende und Steuerung
###################################
my $ legend_txt ;
if ( $ legend ) {
for ( @ pgCDev ) {
my ( $ txt , $ im ) = split ( ':' , $ _ ) ; # $txt ist der Verbrauchername
my $ cmdon = "\"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name $txt on')\"" ;
my $ cmdoff = "\"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name $txt off')\"" ;
my $ cmdauto = "\"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name $txt auto')\"" ;
if ( $ ftui eq "ftui" ) {
$ cmdon = "\"ftui.setFhemStatus('set $name $txt on')\"" ;
$ cmdoff = "\"ftui.setFhemStatus('set $name $txt off')\"" ;
$ cmdauto = "\"ftui.setFhemStatus('set $name $txt auto')\"" ;
}
my $ swstate = ReadingsVal ( $ name , "${cclv}_" . $ txt . "_Switch" , "undef" ) ;
my $ swicon = "<img src=\"$FW_ME/www/images/default/1px-spacer.png\">" ;
if ( $ swstate eq "off" ) {
$ swicon = "<a onClick=$cmdon><img src=\"$FW_ME/www/images/default/10px-kreis-rot.png\"></a>" ;
}
elsif ( $ swstate eq "on" ) {
$ swicon = "<a onClick=$cmdauto><img src=\"$FW_ME/www/images/default/10px-kreis-gruen.png\"></a>" ;
}
elsif ( $ swstate =~ /off.*automatic.*/ix ) {
$ swicon = "<a onClick=$cmdon><img src=\"$FW_ME/www/images/default/10px-kreis-gelb.png\"></a>" ;
}
if ( $ legend_style eq 'icon' ) { # mögliche Umbruchstellen mit normalen Blanks vorsehen !
$ legend_txt . = $ txt . ' ' . FW_makeImage ( $ im ) . ' ' . $ swicon . ' ' ;
}
else {
my ( undef , $ co ) = split ( '\@' , $ im ) ;
$ co = '#cccccc' if ( ! $ co ) ; # Farbe per default
$ legend_txt . = '<font color=\'' . $ co . '\'>' . $ txt . '</font> ' . $ swicon . ' ' ; # hier auch Umbruch erlauben
}
}
}
###################################
# Parameter f. Anzeige extrahieren
###################################
my $ maxhours = AttrNum ( $ name , 'hourCount' , 24 ) ;
my $ hourstyle = AttrVal ( $ name , 'hourStyle' , undef ) ;
my $ colorfc = AttrVal ( $ name , 'beamColor' , undef ) ;
my $ colorc = AttrVal ( $ name , 'beamColor2' , 'C4C4A7' ) ;
my $ icon = AttrVal ( $ name , 'consumerAdviceIcon' , undef ) ;
my $ html_start = AttrVal ( $ name , 'htmlStart' , undef ) ; # beliebige HTML Strings die vor der Grafik ausgegeben werden
my $ html_end = AttrVal ( $ name , 'htmlEnd' , undef ) ; # beliebige HTML Strings die nach der Grafik ausgegeben werden
my $ type = AttrVal ( $ name , 'layoutType' , 'pv' ) ;
my $ kw = AttrVal ( $ name , 'Wh/kWh' , 'Wh' ) ;
$ height = AttrNum ( $ name , 'beamHeight' , 200 ) ;
my $ width = AttrNum ( $ name , 'beamWidth' , 6 ) ; # zu klein ist nicht problematisch
my $ w = $ width * $ maxhours ; # gesammte Breite der Ausgabe , WetterIcon braucht ca. 34px
my $ fsize = AttrNum ( $ name , 'spaceSize' , 24 ) ;
my $ maxVal = AttrNum ( $ name , 'maxPV' , 0 ) ; # dyn. Anpassung der Balkenhöhe oder statisch ?
my $ show_night = AttrNum ( $ name , 'showNight' , 0 ) ; # alle Balken (Spalten) anzeigen ?
my $ show_diff = AttrVal ( $ name , 'showDiff' , 'no' ) ; # zusätzliche Anzeige $di{} in allen Typen
my $ weather = AttrNum ( $ name , 'showWeather' , 1 ) ;
2021-01-01 19:44:20 +00:00
my $ colorw = AttrVal ( $ name , 'weatherColor' , undef ) ; # Wetter Icon Farbe
my $ colorwn = AttrVal ( $ name , 'weatherColor_night' , $ colorw ) ; # Wetter Icon Farbe Nacht
2020-12-13 17:29:15 +00:00
my $ wlalias = AttrVal ( $ name , 'alias' , $ name ) ;
my $ header = AttrNum ( $ name , 'showHeader' , 1 ) ;
my $ hdrAlign = AttrVal ( $ name , 'headerAlignment' , 'center' ) ; # ermöglicht per attr die Ausrichtung der Tabelle zu setzen
my $ hdrDetail = AttrVal ( $ name , 'headerDetail' , 'all' ) ; # ermöglicht den Inhalt zu begrenzen, um bspw. passgenau in ftui einzubetten
# Icon Erstellung, mit @<Farbe> ergänzen falls einfärben
# Beispiel mit Farbe: $icon = FW_makeImage('light_light_dim_100.svg@green');
$ icon = FW_makeImage ( $ icon ) if ( defined ( $ icon ) ) ;
my $ co4h = ReadingsNum ( $ name , "Next04Hours_Consumption" , 0 ) ;
my $ coRe = ReadingsNum ( $ name , "RestOfDay_Consumption" , 0 ) ;
my $ coTo = ReadingsNum ( $ name , "Tomorrow_Consumption" , 0 ) ;
my $ coCu = ReadingsNum ( $ name , "Current_GridConsumption" , 0 ) ;
my $ pv4h = ReadingsNum ( $ name , "Next04Hours_PV" , 0 ) ;
my $ pvRe = ReadingsNum ( $ name , "RestOfDay_PV" , 0 ) ;
my $ pvTo = ReadingsNum ( $ name , "Tomorrow_PV" , 0 ) ;
my $ pvCu = ReadingsNum ( $ name , "Current_PV" , 0 ) ;
2020-12-25 14:14:14 +00:00
my $ pcfa = ReadingsVal ( $ name , "pvCorrectionFactor_Auto" , "off" ) ;
2020-12-13 17:29:15 +00:00
if ( $ kw eq 'kWh' ) {
$ co4h = sprintf ( "%.1f" , $ co4h / 1000 ) . " kWh" ;
$ coRe = sprintf ( "%.1f" , $ coRe / 1000 ) . " kWh" ;
$ coTo = sprintf ( "%.1f" , $ coTo / 1000 ) . " kWh" ;
$ coCu = sprintf ( "%.1f" , $ coCu / 1000 ) . " kW" ;
$ pv4h = sprintf ( "%.1f" , $ pv4h / 1000 ) . " kWh" ;
$ pvRe = sprintf ( "%.1f" , $ pvRe / 1000 ) . " kWh" ;
$ pvTo = sprintf ( "%.1f" , $ pvTo / 1000 ) . " kWh" ;
$ pvCu = sprintf ( "%.1f" , $ pvCu / 1000 ) . " kW" ;
}
else {
$ co4h . = " Wh" ;
$ coRe . = " Wh" ;
$ coTo . = " Wh" ;
$ coCu . = " W" ;
$ pv4h . = " Wh" ;
$ pvRe . = " Wh" ;
$ pvTo . = " Wh" ;
$ pvCu . = " W" ;
}
##########################
# Headerzeile generieren
##########################
if ( $ header ) {
my $ lang = AttrVal ( "global" , "language" , "EN" ) ;
my $ alias = AttrVal ( $ name , "alias" , $ name ) ; # Linktext als Aliasname
my $ dlink = "<a href=\"/fhem?detail=$name\">$alias</a>" ;
my $ lup = ReadingsTimestamp ( $ name , "ThisHour_PVforecast" , "0000-00-00 00:00:00" ) ; # letzter Forecast Update
2020-12-25 14:14:14 +00:00
my $ lupt = "last update:" ;
my $ autoct = "automatic correction:" ;
2020-12-13 17:29:15 +00:00
my $ lblPv4h = "next 4h:" ;
my $ lblPvRe = "remain today:" ;
my $ lblPvTo = "tomorrow:" ;
my $ lblPvCu = "actual" ;
if ( AttrVal ( "global" , "language" , "EN" ) eq "DE" ) { # Header globales Sprachschema Deutsch
2020-12-25 14:14:14 +00:00
$ lupt = "Stand:" ;
$ autoct = "automatische Korrektur:" ;
2020-12-13 17:29:15 +00:00
$ lblPv4h = encode ( "utf8" , "nächste 4h:" ) ;
2020-12-31 09:09:49 +00:00
$ lblPvRe = "Rest heute:" ;
2020-12-13 17:29:15 +00:00
$ lblPvTo = "morgen:" ;
$ lblPvCu = "aktuell" ;
}
$ header = "<table align=\"$hdrAlign\">" ;
#########################################
# Header Link + Status + Update Button
if ( $ hdrDetail eq "all" || $ hdrDetail eq "statusLink" ) {
my ( $ year , $ month , $ day , $ time ) = $ lup =~ /(\d{4})-(\d{2})-(\d{2})\s+(.*)/x ;
if ( AttrVal ( "global" , "language" , "EN" ) eq "DE" ) {
$ lup = "$day.$month.$year $time" ;
}
else {
$ lup = "$year-$month-$day $time" ;
}
2020-12-15 13:41:10 +00:00
my $ cmdupdate = "\"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=get $name data')\"" ; # Update Button generieren
2020-12-13 17:29:15 +00:00
if ( $ ftui eq "ftui" ) {
2020-12-15 13:41:10 +00:00
$ cmdupdate = "\"ftui.setFhemStatus('get $name data')\"" ;
2020-12-13 17:29:15 +00:00
}
my $ upstate = ReadingsVal ( $ name , "state" , "" ) ;
2020-12-25 14:14:14 +00:00
## Update-Icon
##############
my $ upicon ;
2020-12-13 17:29:15 +00:00
if ( $ upstate =~ /updated/ix ) {
$ upicon = "<a onClick=$cmdupdate><img src=\"$FW_ME/www/images/default/10px-kreis-gruen.png\"></a>" ;
}
2020-12-15 13:41:10 +00:00
elsif ( $ upstate =~ /running/ix ) {
2020-12-13 17:29:15 +00:00
$ upicon = "<img src=\"$FW_ME/www/images/default/10px-kreis-gelb.png\"></a>" ;
}
elsif ( $ upstate =~ /initialized/ix ) {
$ upicon = "<img src=\"$FW_ME/www/images/default/1px-spacer.png\"></a>" ;
}
else {
$ upicon = "<a onClick=$cmdupdate><img src=\"$FW_ME/www/images/default/10px-kreis-rot.png\"></a>" ;
}
2020-12-25 14:14:14 +00:00
## Autokorrektur-Icon
######################
my $ acicon ;
if ( $ pcfa eq "on" ) {
$ acicon = "<img src=\"$FW_ME/www/images/default/10px-kreis-gruen.png\">" ;
}
elsif ( $ pcfa eq "off" ) {
$ acicon = "off" ;
}
elsif ( $ pcfa =~ /standby/ix ) {
my ( $ rtime ) = $ pcfa =~ /for (.*?) hours/x ;
$ acicon = "<img src=\"$FW_ME/www/images/default/10px-kreis-gelb.png\"> (Start in " . $ rtime . " h)" ;
}
else {
$ acicon = "<img src=\"$FW_ME/www/images/default/10px-kreis-rot.png\">" ;
}
2020-12-13 17:29:15 +00:00
2020-12-25 14:14:14 +00:00
## erste Header-Zeilen
#######################
$ header . = "<tr><td colspan=\"3\" align=\"left\"><b>" . $ dlink . "</b></td><td colspan=\"3\" align=\"left\">" . $ lupt . " " . $ lup . " " . $ upicon . "</td></tr>" ;
$ header . = "<tr><td colspan=\"3\" align=\"left\"><b> </b></td><td colspan=\"3\" align=\"left\">" . $ autoct . " " . $ acicon . "</td></tr>" ;
2020-12-13 17:29:15 +00:00
}
########################
# Header Information pv
if ( $ hdrDetail eq "all" || $ hdrDetail eq "pv" || $ hdrDetail eq "pvco" ) {
$ header . = "<tr>" ;
$ header . = "<td><b>PV =></b></td>" ;
$ header . = "<td><b>$lblPvCu</b></td> <td align=right>$pvCu</td>" ;
$ header . = "<td><b>$lblPv4h</b></td> <td align=right>$pv4h</td>" ;
$ header . = "<td><b>$lblPvRe</b></td> <td align=right>$pvRe</td>" ;
$ header . = "<td><b>$lblPvTo</b></td> <td align=right>$pvTo</td>" ;
$ header . = "</tr>" ;
}
########################
# Header Information co
if ( $ hdrDetail eq "all" || $ hdrDetail eq "co" || $ hdrDetail eq "pvco" ) {
$ header . = "<tr>" ;
$ header . = "<td><b>CO =></b></td>" ;
$ header . = "<td><b>$lblPvCu</b></td> <td align=right>$coCu</td>" ;
$ header . = "<td><b>$lblPv4h</b></td> <td align=right>$co4h</td>" ;
$ header . = "<td><b>$lblPvRe</b></td> <td align=right>$coRe</td>" ;
$ header . = "<td><b>$lblPvTo</b></td> <td align=right>$coTo</td>" ;
$ header . = "</tr>" ;
}
$ header . = "</table>" ;
}
##########################
# Werte aktuelle Stunde
##########################
2021-01-01 19:44:20 +00:00
$ pv { 0 } = ReadingsNum ( $ name , "ThisHour_PVforecast" , 0 ) ;
$ co { 0 } = ReadingsNum ( $ name , "ThisHour_Consumption" , 0 ) ;
$ di { 0 } = $ pv { 0 } - $ co { 0 } ;
$ is { 0 } = ( ReadingsVal ( $ name , "ThisHour_IsConsumptionRecommended" , 'no' ) eq 'yes' ) ? $ icon : undef ;
$ we { 0 } = $ hash - > { HELPER } { "ThisHour_WeatherId" } if ( $ weather ) ; # für Wettericons
$ we { 0 } // = 99 ;
2020-12-13 17:29:15 +00:00
if ( AttrVal ( "global" , "language" , "EN" ) eq "DE" ) {
( undef , undef , undef , $ t { 0 } ) = ReadingsVal ( $ name , "ThisHour_Time" , '00.00.0000 24' ) =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x ;
2021-01-01 14:36:19 +00:00
$ we_txt { 0 } = $ hash - > { HELPER } { "ThisHour_WeatherTxt" } if ( $ weather ) ;
2020-12-13 17:29:15 +00:00
}
else {
( undef , undef , undef , $ t { 0 } ) = ReadingsVal ( $ name , "ThisHour_Time" , '0000-00-00 24' ) =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x ;
}
2021-01-01 14:36:19 +00:00
$ we_txt { 0 } // = '' ;
$ t { 0 } = int ( $ t { 0 } ) ; # zum Rechnen Integer ohne führende Null
2020-12-13 17:29:15 +00:00
###########################################################
# get consumer list and display it in portalGraphics
###########################################################
for ( @ pgCDev ) {
my ( $ itemName , undef ) = split ( ':' , $ _ ) ;
$ itemName =~ s/^\s+|\s+$//gx ; #trim it, if blanks were used
$ _ =~ s/^\s+|\s+$//gx ; #trim it, if blanks were used
##################################
#check if listed device is planned
if ( ReadingsVal ( $ name , $ itemName . "_Planned" , "no" ) eq "yes" ) {
#get start and end hour
my ( $ start , $ end ) ; # werden auf Balken Pos 0 - 23 umgerechnet, nicht auf Stunde !!, Pos = 24 -> ungültige Pos = keine Anzeige
if ( AttrVal ( "global" , "language" , "EN" ) eq "DE" ) {
( undef , undef , undef , $ start ) = ReadingsVal ( $ name , $ itemName . "_PlannedOpTimeBegin" , '00.00.0000 24' ) =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x ;
( undef , undef , undef , $ end ) = ReadingsVal ( $ name , $ itemName . "_PlannedOpTimeEnd" , '00.00.0000 24' ) =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x ;
}
else {
( undef , undef , undef , $ start ) = ReadingsVal ( $ name , $ itemName . "_PlannedOpTimeBegin" , '0000-00-00 24' ) =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x ;
( undef , undef , undef , $ end ) = ReadingsVal ( $ name , $ itemName . "_PlannedOpTimeEnd" , '0000-00-00 24' ) =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x ;
}
$ start = int ( $ start ) ;
$ end = int ( $ end ) ;
my $ flag = 0 ; # default kein Tagesverschieber
#######################################
#correct the hour for accurate display
if ( $ start < $ t { 0 } ) { # consumption seems to be tomorrow
$ start = 24 - $ t { 0 } + $ start ;
$ flag = 1 ;
}
else {
$ start -= $ t { 0 } ;
}
if ( $ flag ) { # consumption seems to be tomorrow
$ end = 24 - $ t { 0 } + $ end ;
}
else {
$ end -= $ t { 0 } ;
}
$ _ . = ":" . $ start . ":" . $ end ;
}
else {
$ _ . = ":24:24" ;
}
Log3 ( $ name , 4 , "$name - Consumer planned data: $_" ) ;
}
$ maxVal = ! $ maxVal ? $ pv { 0 } : $ maxVal ; # Startwert wenn kein Wert bereits via attr vorgegeben ist
my $ maxCon = $ co { 0 } ; # für Typ co
my $ maxDif = $ di { 0 } ; # für Typ diff
my $ minDif = $ di { 0 } ; # für Typ diff
for my $ i ( 1 .. $ maxhours - 1 ) {
2021-01-01 19:44:20 +00:00
$ pv { $ i } = ReadingsNum ( $ name , "NextHour" . sprintf ( "%02d" , $ i ) . "_PVforecast" , 0 ) ; # Erzeugung
$ co { $ i } = ReadingsNum ( $ name , "NextHour" . sprintf ( "%02d" , $ i ) . "_Consumption" , 0 ) ; # Verbrauch
$ di { $ i } = $ pv { $ i } - $ co { $ i } ;
2020-12-13 17:29:15 +00:00
2021-01-01 19:44:20 +00:00
$ maxVal = $ pv { $ i } if ( $ pv { $ i } > $ maxVal ) ;
$ maxCon = $ co { $ i } if ( $ co { $ i } > $ maxCon ) ;
$ maxDif = $ di { $ i } if ( $ di { $ i } > $ maxDif ) ;
$ minDif = $ di { $ i } if ( $ di { $ i } < $ minDif ) ;
2020-12-13 17:29:15 +00:00
2021-01-01 19:44:20 +00:00
$ is { $ i } = ( ReadingsVal ( $ name , "NextHour" . sprintf ( "%02d" , $ i ) . "_IsConsumptionRecommended" , 'no' ) eq 'yes' ) ? $ icon : undef ;
$ we { $ i } = $ hash - > { HELPER } { "NextHour" . sprintf ( "%02d" , $ i ) . "_WeatherId" } if ( $ weather ) ; # für Wettericons
$ we { $ i } // = 99 ;
2020-12-13 17:29:15 +00:00
if ( AttrVal ( "global" , "language" , "EN" ) eq "DE" ) {
( undef , undef , undef , $ t { $ i } ) = ReadingsVal ( $ name , "NextHour" . sprintf ( "%02d" , $ i ) . "_Time" , '00.00.0000 24' ) =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x ;
2021-01-01 14:36:19 +00:00
$ we_txt { $ i } = $ hash - > { HELPER } { "NextHour" . sprintf ( "%02d" , $ i ) . "_WeatherTxt" } if ( $ weather ) ; # für Wettericons
2020-12-13 17:29:15 +00:00
}
else {
( undef , undef , undef , $ t { $ i } ) = ReadingsVal ( $ name , "NextHour" . sprintf ( "%02d" , $ i ) . "_Time" , '0000-00-00 24' ) =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x ;
}
2021-01-01 14:36:19 +00:00
$ we_txt { $ i } // = '' ;
$ t { $ i } = int ( $ t { $ i } ) ; # keine führende 0
2020-12-13 17:29:15 +00:00
}
######################################
# Tabellen Ausgabe erzeugen
######################################
# Wenn Table class=block alleine steht, zieht es bei manchen Styles die Ausgabe auf 100% Seitenbreite
# lässt sich durch einbetten in eine zusätzliche Table roomoverview eindämmen
# Die Tabelle ist recht schmal angelegt, aber nur so lassen sich Umbrüche erzwingen
$ ret = "<html>" ;
$ ret . = $ html_start if ( defined ( $ html_start ) ) ;
$ ret . = "<style>TD.smaportal {text-align: center; padding-left:1px; padding-right:1px; margin:0px;}</style>" ;
$ ret . = "<table class='roomoverview' width='$w' style='width:" . $ w . "px'><tr class='devTypeTr'></tr>" ;
$ ret . = "<tr><td class='smaportal'>" ;
$ ret . = "\n<table class='block'>" ; # das \n erleichtert das Lesen der debug Quelltextausgabe
if ( $ header ) { # Header ausgeben
$ ret . = "<tr class='odd'>" ;
# mit einem extra <td></td> ein wenig mehr Platz machen, ergibt i.d.R. weniger als ein Zeichen
$ ret . = "<td colspan='" . ( $ maxhours + 2 ) . "' align='center' style='word-break: normal'>$header</td></tr>" ;
}
if ( $ legend_txt && ( $ legend eq 'top' ) ) {
$ ret . = "<tr class='odd'>" ;
$ ret . = "<td colspan='" . ( $ maxhours + 2 ) . "' align='center' style='word-break: normal'>$legend_txt</td></tr>" ;
}
if ( $ weather ) {
$ ret . = "<tr class='even'><td class='smaportal'></td>" ; # freier Platz am Anfang
for my $ i ( 0 .. $ maxhours - 1 ) { # keine Anzeige bei Null Ertrag bzw. in der Nacht , Typ pcvo & diff haben aber immer Daten in der Nacht
if ( $ pv { $ i } || $ show_night || ( $ type eq 'pvco' ) || ( $ type eq 'diff' ) ) { # FHEM Wetter Icons (weather_xxx) , Skalierung und Farbe durch FHEM Bordmittel
2021-01-04 20:25:02 +00:00
my $ night = ( $ we { $ i } > 99 ) ? 1 : 0 ;
$ we { $ i } -= 100 if ( $ night ) ;
my ( $ icon_name , $ title ) = weather_icon ( $ we { $ i } ) ; # unknown -> FHEM Icon Fragezeichen im Kreis wird als Ersatz Icon ausgegeben
2021-01-01 19:44:20 +00:00
Log3 ( $ name , 3 , "$name - unknown weather id: " . $ we { $ i } . ", please inform the maintainer" ) if ( $ icon_name eq 'unknown' ) ;
2020-12-13 17:29:15 +00:00
2021-01-01 19:44:20 +00:00
$ icon_name . = '@' . $ colorw if ( defined ( $ colorw ) && ! $ night ) ;
$ icon_name . = '@' . $ colorwn if ( defined ( $ colorwn ) && $ night ) ;
2020-12-13 17:29:15 +00:00
$ val = FW_makeImage ( $ icon_name ) ;
2021-01-01 14:36:19 +00:00
if ( $ val eq $ icon_name ) { # passendes Icon beim User nicht vorhanden ! ( attr web iconPath falsch/prüfen/update ? )
$ val = '<b>???<b/>' ;
2021-01-01 19:44:20 +00:00
Log3 ( $ name , 3 , qq{ $name - the icon $we { $i } not found. Please check attribute "iconPath" of your FHEMWEB instance and/or update your FHEM software } ) ;
2021-01-01 14:36:19 +00:00
}
2021-01-04 20:25:02 +00:00
$ ret . = "<td title='$title' class='smaportal' width='$width' style='margin:1px; vertical-align:middle align:center; padding-bottom:1px;'>$val</td>" ; # title -> Mouse Over Text
2020-12-13 17:29:15 +00:00
}
else { # Kein Ertrag oder show_night = 0
$ ret . = "<td></td>" ; $ we { $ i } = undef ;
}
# mit $we{$i} = undef kann man unten leicht feststellen ob für diese Spalte bereits ein Icon ausgegeben wurde oder nicht
}
$ ret . = "<td class='smaportal'></td></tr>" ; # freier Platz am Ende der Icon Zeile
}
if ( $ show_diff eq 'top' ) { # Zusätzliche Zeile Ertrag - Verbrauch
$ ret . = "<tr class='even'><td class='smaportal'></td>" ; # freier Platz am Anfang
for my $ i ( 0 .. $ maxhours - 1 ) {
$ val = formatVal6 ( $ di { $ i } , $ kw , $ we { $ i } ) ;
$ val = ( $ di { $ i } < 0 ) ? '<b>' . $ val . '<b/>' : '+' . $ val ; # negative Zahlen in Fettschrift
$ ret . = "<td class='smaportal' style='vertical-align:middle; text-align:center;'>$val</td>" ;
}
$ ret . = "<td class='smaportal'></td></tr>" ; # freier Platz am Ende
}
$ ret . = "<tr class='even'><td class='smaportal'></td>" ; # Neue Zeile mit freiem Platz am Anfang
for my $ i ( 0 .. $ maxhours - 1 ) {
# Achtung Falle, Division by Zero möglich,
# maxVal kann gerade bei kleineren maxhours Ausgaben in der Nacht leicht auf 0 fallen
$ height = 200 if ( ! $ height ) ; # Fallback, sollte eigentlich nicht vorkommen, außer der User setzt es auf 0
$ maxVal = 1 if ( ! int $ maxVal ) ;
$ maxCon = 1 if ( ! $ maxCon ) ;
# Der zusätzliche Offset durch $fsize verhindert bei den meisten Skins
# dass die Grundlinie der Balken nach unten durchbrochen wird
if ( $ type eq 'co' ) {
$ he = int ( ( $ maxCon - $ co { $ i } ) / $ maxCon * $ height ) + $ fsize ; # he - freier der Raum über den Balken.
$ z3 = int ( $ height + $ fsize - $ he ) ; # Resthöhe
}
elsif ( $ type eq 'pv' ) {
$ he = int ( ( $ maxVal - $ pv { $ i } ) / $ maxVal * $ height ) + $ fsize ;
$ z3 = int ( $ height + $ fsize - $ he ) ;
}
elsif ( $ type eq 'pvco' ) {
# Berechnung der Zonen
# he - freier der Raum über den Balken. fsize wird nicht verwendet, da bei diesem Typ keine Zahlen über den Balken stehen
# z2 - der Ertrag ggf mit Icon
# z3 - der Verbrauch , bei zu kleinem Wert wird der Platz komplett Zone 2 zugeschlagen und nicht angezeigt
# z2 und z3 nach Bedarf tauschen, wenn der Verbrauch größer als der Ertrag ist
$ maxVal = $ maxCon if ( $ maxCon > $ maxVal ) ; # wer hat den größten Wert ?
if ( $ pv { $ i } > $ co { $ i } ) { # pv oben , co unten
$ z2 = $ pv { $ i } ; $ z3 = $ co { $ i } ;
}
else { # tauschen, Verbrauch ist größer als Ertrag
$ z3 = $ pv { $ i } ; $ z2 = $ co { $ i } ;
}
$ he = int ( ( $ maxVal - $ z2 ) / $ maxVal * $ height ) ;
$ z2 = int ( ( $ z2 - $ z3 ) / $ maxVal * $ height ) ;
$ z3 = int ( $ height - $ he - $ z2 ) ; # was von maxVal noch übrig ist
if ( $ z3 < int ( $ fsize /2)) { # dünnen Strichbalken vermeiden / ca . halbe Zeichenhöhe
$ z2 += $ z3 ; $ z3 = 0 ;
}
}
else { # Typ diff
# Berechnung der Zonen
# he - freier der Raum über den Balken , Zahl positiver Wert + fsize
# z2 - positiver Balken inkl Icon
# z3 - negativer Balken
# z4 - Zahl negativer Wert + fsize
my ( $ px_pos , $ px_neg ) ;
my $ maxPV = 0 ; # ToDo: maxPV noch aus Attribut maxPV ableiten
if ( $ maxPV ) { # Feste Aufteilung +/- , jeder 50 % bei maxPV = 0
$ px_pos = int ( $ height / 2 ) ;
$ px_neg = $ height - $ px_pos ; # Rundungsfehler vermeiden
}
else { # Dynamische hoch/runter Verschiebung der Null-Linie
if ( $ minDif >= 0 ) { # keine negativen Balken vorhanden, die Positiven bekommen den gesammten Raum
$ px_neg = 0 ;
$ px_pos = $ height ;
}
else {
if ( $ maxDif > 0 ) {
$ px_neg = int ( $ height * abs ( $ minDif ) / ( $ maxDif + abs ( $ minDif ) ) ) ; # Wieviel % entfallen auf unten ?
$ px_pos = $ height - $ px_neg ; # der Rest ist oben
}
else { # keine positiven Balken vorhanden, die Negativen bekommen den gesammten Raum
$ px_neg = $ height ;
$ px_pos = 0 ;
}
}
}
if ( $ di { $ i } >= 0 ) { # Zone 2 & 3 mit ihren direkten Werten vorbesetzen
$ z2 = $ di { $ i } ;
$ z3 = abs ( $ minDif ) ;
}
else {
$ z2 = $ maxDif ;
$ z3 = abs ( $ di { $ i } ) ; # Nur Betrag ohne Vorzeichen
}
# Alle vorbesetzen Werte umrechnen auf echte Ausgabe px
$ he = ( ! $ px_pos ) ? 0 : int ( ( $ maxDif - $ z2 ) / $ maxDif * $ px_pos ) ; # Teilung durch 0 vermeiden
$ z2 = ( $ px_pos - $ he ) ;
$ z4 = ( ! $ px_neg ) ? 0 : int ( ( abs ( $ minDif ) - $ z3 ) / abs ( $ minDif ) * $ px_neg ) ; # Teilung durch 0 unbedingt vermeiden
$ z3 = ( $ px_neg - $ z4 ) ;
# Beiden Zonen die Werte ausgeben könnten muß fsize als zusätzlicher Raum zugeschlagen werden !
$ he += $ fsize ;
$ z4 += $ fsize if ( $ z3 ) ; # komplette Grafik ohne negativ Balken, keine Ausgabe von z3 & z4
}
# das style des nächsten TD bestimmt ganz wesentlich das gesammte Design
# das \n erleichtert das lesen des Seitenquelltext beim debugging
# vertical-align:bottom damit alle Balken und Ausgaben wirklich auf der gleichen Grundlinie sitzen
$ ret . = "<td style='text-align: center; padding-left:1px; padding-right:1px; margin:0px; vertical-align:bottom; padding-top:0px'>\n" ;
my $ v ;
if ( ( $ type eq 'pv' ) || ( $ type eq 'co' ) ) {
$ v = ( $ type eq 'co' ) ? $ co { $ i } : $ pv { $ i } ;
$ v = 0 if ( ( $ type eq 'co' ) && ! $ pv { $ i } && ! $ show_night ) ; # auch bei type co die Nacht ggf. unterdrücken
$ val = formatVal6 ( $ v , $ kw , $ we { $ i } ) ;
$ ret . = "<table width='100%' height='100%'>" ; # mit width=100% etwas bessere Füllung der Balken
$ ret . = "<tr class='even' style='height:" . $ he . "px'>" ;
$ ret . = "<td class='smaportal' style='vertical-align:bottom'>" . $ val . "</td></tr>" ;
if ( $ v || $ show_night ) { # Balken nur einfärben wenn der User via Attr eine Farbe vorgibt, sonst bestimmt class odd von TR alleine die Farbe
my $ style = "style=\"padding-bottom:0px; vertical-align:top; margin-left:auto; margin-right:auto;" ;
$ style . = defined $ colorfc ? " background-color:#$colorfc\"" : '"' ; # Syntaxhilight
$ ret . = "<tr class='odd' style='height:" . $ z3 . "px;'>" ;
$ ret . = "<td align='center' class='smaportal' " . $ style . ">" ;
my $ sicon = 1 ;
$ ret . = $ is { $ i } if ( defined ( $ is { $ i } ) && $ sicon ) ;
##################################
# inject the new icon if defined
$ ret . = consinject ( $ hash , $ i , @ pgCDev ) if ( $ ret ) ;
$ ret . = "</td></tr>" ;
}
}
elsif ( $ type eq 'pvco' ) {
my ( $ color1 , $ color2 , $ style1 , $ style2 ) ;
$ ret . = "<table width='100%' height='100%'>\n" ; # mit width=100% etwas bessere Füllung der Balken
if ( $ he ) { # der Freiraum oben kann beim größten Balken ganz entfallen
$ ret . = "<tr class='even' style='height:" . $ he . "px'><td class='smaportal'></td></tr>" ;
}
if ( $ pv { $ i } > $ co { $ i } ) { # wer ist oben, co pder pv ? Wert und Farbe für Zone 2 & 3 vorbesetzen
$ val = formatVal6 ( $ pv { $ i } , $ kw , $ we { $ i } ) ;
$ color1 = $ colorfc ;
$ style1 = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;" ;
$ style1 . = ( defined ( $ color1 ) ) ? " background-color:#$color1\"" : '"' ;
if ( $ z3 ) { # die Zuweisung können wir uns sparen wenn Zone 3 nachher eh nicht ausgegeben wird
$ v = formatVal6 ( $ co { $ i } , $ kw , $ we { $ i } ) ;
$ color2 = $ colorc ;
$ style2 = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;" ;
$ style2 . = ( defined ( $ color2 ) ) ? " background-color:#$color2\"" : '"' ;
}
}
else {
$ val = formatVal6 ( $ co { $ i } , $ kw , $ we { $ i } ) ;
$ color1 = $ colorc ;
$ style1 = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;" ;
$ style1 . = ( defined ( $ color1 ) ) ? " background-color:#$color1\"" : '"' ;
if ( $ z3 ) {
$ v = formatVal6 ( $ pv { $ i } , $ kw , $ we { $ i } ) ;
$ color2 = $ colorfc ;
$ style2 = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;" ;
$ style2 . = ( defined ( $ color2 ) ) ? " background-color:#$color2\"" : '"' ;
}
}
$ ret . = "<tr class='odd' style='height:" . $ z2 . "px'>" ;
$ ret . = "<td align='center' class='smaportal' " . $ style1 . ">$val" ;
$ ret . = $ is { $ i } if ( defined $ is { $ i } ) ;
##################################
# inject the new icon if defined
$ ret . = consinject ( $ hash , $ i , @ pgCDev ) if ( $ ret ) ;
$ ret . = "</td></tr>" ;
if ( $ z3 ) { # die Zone 3 lassen wir bei zu kleinen Werten auch ganz weg
$ ret . = "<tr class='odd' style='height:" . $ z3 . "px'>" ;
$ ret . = "<td align='center' class='smaportal' " . $ style2 . ">$v</td></tr>" ;
}
}
else { # Type diff
my $ style = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;" ;
$ ret . = "<table width='100%' border='0'>\n" ; # Tipp : das nachfolgende border=0 auf 1 setzen hilft sehr Ausgabefehler zu endecken
$ val = ( $ di { $ i } >= 0 ) ? formatVal6 ( $ di { $ i } , $ kw , $ we { $ i } ) : '' ;
$ val = ' 0 ' if ( $ di { $ i } == 0 ) ; # Sonderfall , hier wird die 0 gebraucht !
if ( $ val ) {
$ ret . = "<tr class='even' style='height:" . $ he . "px'>" ;
$ ret . = "<td class='smaportal' style='vertical-align:bottom'>" . $ val . "</td></tr>" ;
}
if ( $ di { $ i } >= 0 ) { # mit Farbe 1 colorfc füllen
$ style . = defined $ colorfc ? " background-color:#$colorfc\"" : '"' ;
$ z2 = 1 if ( $ di { $ i } == 0 ) ; # Sonderfall , 1px dünnen Strich ausgeben
$ ret . = "<tr class='odd' style='height:" . $ z2 . "px'>" ;
$ ret . = "<td align='center' class='smaportal' " . $ style . ">" ;
$ ret . = $ is { $ i } if ( defined $ is { $ i } ) ;
$ ret . = "</td></tr>" ;
}
else { # ohne Farbe
$ z2 = 2 if ( $ di { $ i } == 0 ) ; # Sonderfall, hier wird die 0 gebraucht !
if ( $ z2 && $ val ) { # z2 weglassen wenn nicht unbedigt nötig bzw. wenn zuvor he mit val keinen Wert hatte
$ ret . = "<tr class='even' style='height:" . $ z2 . "px'>" ;
$ ret . = "<td class='smaportal'></td></tr>" ;
}
}
if ( $ di { $ i } < 0 ) { # Negativ Balken anzeigen ?
$ style . = ( defined ( $ colorc ) ) ? " background-color:#$colorc\"" : '"' ; # mit Farbe 2 colorc füllen
$ ret . = "<tr class='odd' style='height:" . $ z3 . "px'>" ;
$ ret . = "<td align='center' class='smaportal' " . $ style . "></td></tr>" ;
}
elsif ( $ z3 ) { # ohne Farbe
$ ret . = "<tr class='even' style='height:" . $ z3 . "px'>" ;
$ ret . = "<td class='smaportal'></td></tr>" ;
}
if ( $ z4 ) { # kann entfallen wenn auch z3 0 ist
$ val = ( $ di { $ i } < 0 ) ? formatVal6 ( $ di { $ i } , $ kw , $ we { $ i } ) : ' ' ;
$ ret . = "<tr class='even' style='height:" . $ z4 . "px'>" ;
$ ret . = "<td class='smaportal' style='vertical-align:top'>" . $ val . "</td></tr>" ;
}
}
if ( $ show_diff eq 'bottom' ) { # zusätzliche diff Anzeige
$ val = formatVal6 ( $ di { $ i } , $ kw , $ we { $ i } ) ;
$ val = ( $ di { $ i } < 0 ) ? '<b>' . $ val . '<b/>' : '+' . $ val ; # Kommentar siehe oben bei show_diff eq top
$ ret . = "<tr class='even'><td class='smaportal' style='vertical-align:middle; text-align:center;'>$val</td></tr>" ;
}
$ ret . = "<tr class='even'><td class='smaportal' style='vertical-align:bottom; text-align:center;'>" ;
$ t { $ i } = $ t { $ i } . $ hourstyle if ( defined ( $ hourstyle ) ) ; # z.B. 10:00 statt 10
$ ret . = $ t { $ i } . "</td></tr></table></td>" ; # Stundenwerte ohne führende 0
}
$ ret . = "<td class='smaportal'></td></tr>" ;
###################
# Legende unten
if ( $ legend_txt && ( $ legend eq 'bottom' ) ) {
$ ret . = "<tr class='odd'>" ;
$ ret . = "<td colspan='" . ( $ maxhours + 2 ) . "' align='center' style='word-break: normal'>" ;
$ ret . = "$legend_txt</td></tr>" ;
}
$ ret . = "</table></td></tr></table>" ;
$ ret . = $ html_end if ( defined ( $ html_end ) ) ;
$ ret . = "</html>" ;
return $ ret ;
}
################################################################
# Inject consumer icon
################################################################
sub consinject {
my ( $ hash , $ i , @ pgCDev ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my $ ret = "" ;
for ( @ pgCDev ) {
if ( $ _ ) {
my ( $ cons , $ im , $ start , $ end ) = split ( ':' , $ _ ) ;
Log3 ( $ name , 4 , "$name - Consumer to show -> $cons, relative to current time -> start: $start, end: $end" ) if ( $ i < 1 ) ;
if ( $ im && ( $ i >= $ start ) && ( $ i <= $ end ) ) {
$ ret . = FW_makeImage ( $ im ) ;
}
}
}
return $ ret ;
}
###############################################################################
# Balkenbreite normieren
#
# Die Balkenbreite wird bestimmt durch den Wert.
# Damit alle Balken die gleiche Breite bekommen, müssen die Werte auf
# 6 Ausgabezeichen angeglichen werden.
# "align=center" gleicht gleicht es aus, alternativ könnte man sie auch
# komplett rechtsbündig ausgeben.
# Es ergibt bei fast allen Styles gute Ergebnisse, Ausnahme IOS12 & 6, da diese
# beiden Styles einen recht großen Font benutzen.
# Wird Wetter benutzt, wird die Balkenbreite durch das Icon bestimmt
#
###############################################################################
sub formatVal6 {
2020-12-13 21:48:45 +00:00
my ( $ v , $ kw , $ w ) = @ _ ;
my $ n = ' ' ; # positive Zahl
2020-12-13 17:29:15 +00:00
2020-12-13 21:48:45 +00:00
if ( $ v < 0 ) {
2020-12-13 17:29:15 +00:00
$ n = '-' ; # negatives Vorzeichen merken
$ v = abs ( $ v ) ;
}
2020-12-13 21:48:45 +00:00
if ( $ kw eq 'kWh' ) { # bei Anzeige in kWh muss weniger aufgefüllt werden
2020-12-13 17:29:15 +00:00
$ v = sprintf ( '%.1f' , ( $ v / 1000 ) ) ;
$ v += 0 ; # keine 0.0 oder 6.0 etc
return ( $ n eq '-' ) ? ( $ v * - 1 ) : $ v if defined ( $ w ) ;
my $ t = $ v - int ( $ v ) ; # Nachkommstelle ?
2020-12-13 21:48:45 +00:00
if ( ! $ t ) { # glatte Zahl ohne Nachkommastelle
2020-12-13 17:29:15 +00:00
if ( ! $ v ) {
return ' ' ; # 0 nicht anzeigen, passt eigentlich immer bis auf einen Fall im Typ diff
}
elsif ( $ v < 10 ) {
return ' ' . $ n . $ v . ' ' ;
}
else {
return ' ' . $ n . $ v . ' ' ;
}
}
else { # mit Nachkommastelle -> zwei Zeichen mehr .X
if ( $ v < 10 ) {
return ' ' . $ n . $ v . ' ' ;
}
else {
return $ n . $ v . ' ' ;
}
}
}
2020-12-13 21:48:45 +00:00
return ( $ n eq '-' ) ? ( $ v * - 1 ) : $ v if defined ( $ w ) ;
2020-12-13 17:29:15 +00:00
# Werte bleiben in Watt
if ( ! $ v ) { return ' ' ; } ## no critic "Cascading" # keine Anzeige bei Null
elsif ( $ v < 10 ) { return ' ' . $ n . $ v . ' ' ; } # z.B. 0
elsif ( $ v < 100 ) { return ' ' . $ n . $ v . ' ' ; }
elsif ( $ v < 1000 ) { return ' ' . $ n . $ v . ' ' ; }
elsif ( $ v < 10000 ) { return $ n . $ v . ' ' ; }
else { return $ n . $ v ; } # mehr als 10.000 W :)
}
###############################################################################
# Zuordungstabelle "WeatherId" angepasst auf FHEM Icons
###############################################################################
sub weather_icon {
my $ id = shift ;
2021-01-17 19:13:02 +00:00
$ id = int $ id ;
2021-01-04 20:25:02 +00:00
if ( defined $ weather_ids { $ id } ) {
return $ weather_ids { $ id } { icon } , encode ( "utf8" , $ weather_ids { $ id } { txtd } ) ;
}
2020-12-13 17:29:15 +00:00
2021-01-04 20:25:02 +00:00
return 'unknown' , '' ;
2020-12-13 17:29:15 +00:00
}
################################################################
# Timestamp berechnen
################################################################
sub TimeAdjust {
my $ epoch = shift ;
my ( $ lyear , $ lmonth , $ lday , $ lhour ) = ( localtime ( $ epoch ) ) [ 5 , 4 , 3 , 2 ] ;
$ lyear += 1900 ; # year is 1900 based
$ lmonth + + ; # month number is zero based
if ( AttrVal ( "global" , "language" , "EN" ) eq "DE" ) {
return ( sprintf ( "%02d.%02d.%04d %02d:%s" , $ lday , $ lmonth , $ lyear , $ lhour , "00:00" ) ) ;
}
else {
return ( sprintf ( "%04d-%02d-%02d %02d:%s" , $ lyear , $ lmonth , $ lday , $ lhour , "00:00" ) ) ;
}
}
2021-01-23 11:46:54 +00:00
################################################################
# benötigte Attribute im DWD Device checken
################################################################
sub checkdwdattr {
my $ dwddev = shift ;
my @ fcprop = map { trim ( $ _ ) } split "," , AttrVal ( $ dwddev , "forecastProperties" , "pattern" ) ;
# my @trimed = map { trim($_) } @fcprop;
my @ aneeded ;
for my $ am ( @ dwdattrmust ) {
next if ( $ am ~ ~ @ fcprop ) ;
push @ aneeded , $ am ;
}
return @ aneeded ;
}
2020-12-13 17:29:15 +00:00
##################################################################################################
# PV Forecast Rad1h in kWh / Wh
# Für die Umrechnung in einen kWh/Wh-Wert benötigt man einen entsprechenden Faktorwert:
#
# * Faktor für Umwandlung kJ in kWh: 0.00027778
# * Eigene Modulfläche in qm z.B.: 31,04
# * Wirkungsgrad der Module in % z.B.: 16,52
# * Wirkungsgrad WR in % z.B.: 98,3
# * Korrekturwerte wegen Ausrichtung/Verschattung: 83% wegen Ost/West und Schatten (Iteration)
#
# Die Formel wäre dann:
# Ertrag in kWh = Rad1h * 0.00027778 * 31,04 qm * 16,52% * 98,3% * 100%
#
# Damit ergibt sich ein Umrechnungsfaktor von: 0,00140019 für kWh / 1,40019 für Wh
#
# Bei einem Rad1h-Wert von 500 ergibt dies bei mir also 0,700095 kWh / 700,095 Wh
2021-01-22 09:51:04 +00:00
#
# Die Abhängigkeit der Strahlungsleistung der Sonnenenergie nach Wetterlage und Jahreszeit ist
# hier beschrieben:
# https://www.energie-experten.org/erneuerbare-energien/photovoltaik/planung/sonnenstunden
#
2020-12-13 17:29:15 +00:00
##################################################################################################
sub calcPVforecast {
my $ name = shift ;
my $ rad = shift ; # Nominale Strahlung aus DWD Device
my $ fh = shift ; # Stunde des Tages
2021-01-21 22:07:18 +00:00
my $ hash = $ defs { $ name } ;
2020-12-13 17:29:15 +00:00
my $ ma = ReadingsNum ( $ name , "moduleArea" , 0 ) ; # Solar Modulfläche (gesamt)
2020-12-16 19:15:48 +00:00
my $ ta = ReadingsNum ( $ name , "moduleTiltAngle" , 45 ) ; # Neigungswinkel Solarmodule
2020-12-13 17:29:15 +00:00
my $ me = ReadingsNum ( $ name , "moduleEfficiency" , $ defpvme ) ; # Solar Modul Wirkungsgrad (%)
my $ ie = ReadingsNum ( $ name , "inverterEfficiency" , $ definve ) ; # Solar Inverter Wirkungsgrad (%)
my $ hc = ReadingsNum ( $ name , "pvCorrectionFactor_" . sprintf ( "%02d" , $ fh ) , 1 ) ; # Korrekturfaktor für die Stunde des Tages
2021-01-21 22:07:18 +00:00
my $ cloudcover = $ hash - > { HELPER } { "NextHour" . sprintf ( "%02d" , $ fh ) . "_CloudCover" } // 0 ; # effektive Wolkendecke
2021-01-22 14:18:33 +00:00
my $ ccf = 1 - ( ( $ cloudcover - $ cloud_base ) * $ cloudslope / 100 ) ; # Cloud Correction Faktor mit Steilheit und Fußpunkt
2021-01-21 22:07:18 +00:00
2021-01-22 20:16:33 +00:00
my $ rainprob = $ hash - > { HELPER } { "NextHour" . sprintf ( "%02d" , $ fh ) . "_RainProb" } // 0 ; # Niederschlagswahrscheinlichkeit> 0,1 mm während der letzten Stunde
my $ rcf = 1 - ( ( $ rainprob - $ rain_base ) * $ rainslope / 100 ) ; # Rain Correction Faktor mit Steilheit
2020-12-16 19:15:48 +00:00
$ hc = 1 if ( 1 * $ hc == 0 ) ;
2021-01-22 20:16:33 +00:00
my $ pv = sprintf "%.1f" , ( $ rad * $ kJtokWh * $ ma * $ htilt { "$ta" } * $ me /100 * $ie/ 100 * $ hc * $ ccf * $ rcf * 1000 ) ;
2020-12-31 09:09:49 +00:00
my $ kw = AttrVal ( $ name , 'Wh/kWh' , 'Wh' ) ;
if ( $ kw eq "Wh" ) {
$ pv = int $ pv ;
}
2020-12-16 19:15:48 +00:00
2021-01-22 20:16:33 +00:00
Log3 ( $ name , 5 , "$name - calcPVforecast -> Hour: " . sprintf ( "%02d" , $ fh ) . " ,moduleTiltAngle factor: " . $ htilt { "$ta" } . ", Cloudfactor: $ccf, Rainfactor: $rcf, pvCorrectionFactor: $hc" ) ;
2020-12-13 17:29:15 +00:00
2020-12-16 19:15:48 +00:00
return $ pv ;
2020-12-13 17:29:15 +00:00
}
2020-12-13 21:48:45 +00:00
################################################################
# Abweichung PVreal / PVforecast berechnen
2021-01-22 09:51:04 +00:00
# bei eingeschalteter automat. Korrektur
2020-12-13 21:48:45 +00:00
################################################################
sub calcVariance {
2020-12-27 18:51:53 +00:00
my $ paref = shift ;
2020-12-17 15:52:48 +00:00
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
2020-12-27 18:51:53 +00:00
my $ chour = $ paref - > { chour } ;
2021-01-19 09:10:20 +00:00
2020-12-17 15:52:48 +00:00
my $ dcauto = ReadingsVal ( $ name , "pvCorrectionFactor_Auto" , "off" ) ; # nur bei "on" automatische Varianzkalkulation
2020-12-16 09:33:20 +00:00
if ( $ dcauto =~ /^off/x ) {
2020-12-17 15:52:48 +00:00
Log3 ( $ name , 4 , "$name - automatic Variance calculation is switched off." ) ;
2020-12-15 13:41:10 +00:00
return ;
}
2020-12-27 18:51:53 +00:00
my $ tlim = "00" ; # Stunde 00 -> löschen aller Autocalc Statusreadings des Tages
if ( $ chour =~ /^($tlim)$/x ) {
deleteReadingspec ( $ hash , "pvCorrectionFactor_.*_autocalc" ) ;
}
my $ idts = ReadingsTimestamp ( $ name , "currentInverterDev" , "" ) ; # Definitionstimestamp des Inverterdevice
2020-12-13 21:48:45 +00:00
return if ( ! $ idts ) ;
2020-12-27 18:51:53 +00:00
$ idts = timestringToTimestamp ( $ hash , $ idts ) ;
2020-12-13 21:48:45 +00:00
2020-12-25 14:14:14 +00:00
my $ t = time ; # aktuelle Unix-Zeit
2020-12-13 21:48:45 +00:00
if ( $ t - $ idts < 86400 ) {
2020-12-15 13:41:10 +00:00
my $ rmh = sprintf "%.1f" , ( ( 86400 - ( $ t - $ idts ) ) / 3600 ) ;
2020-12-17 15:52:48 +00:00
Log3 ( $ name , 4 , "$name - Variance calculation in standby. It starts in $rmh hours." ) ;
readingsSingleUpdate ( $ hash , "pvCorrectionFactor_Auto" , "on (remains in standby for $rmh hours)" , 0 ) ;
2020-12-13 21:48:45 +00:00
return ;
2020-12-16 15:48:38 +00:00
}
else {
2020-12-17 15:52:48 +00:00
readingsSingleUpdate ( $ hash , "pvCorrectionFactor_Auto" , "on" , 0 ) ;
2020-12-27 20:04:17 +00:00
}
my $ maxvar = AttrVal ( $ name , "maxVariancePerDay" , $ defmaxvar ) ; # max. Korrekturvarianz
2020-12-13 21:48:45 +00:00
my @ da ;
for my $ h ( 1 .. 23 ) {
2020-12-17 13:35:52 +00:00
next if ( ! $ chour || $ h >= $ chour ) ;
2021-01-21 18:23:21 +00:00
my $ fcval = ReadingsNum ( $ name , "Today_Hour" . sprintf ( "%02d" , $ h ) . "_PVforecast" , 0 ) ;
2021-01-20 21:00:34 +00:00
next if ( ! $ fcval ) ;
2020-12-17 13:35:52 +00:00
2021-01-21 18:23:21 +00:00
my $ pvval = ReadingsNum ( $ name , "Today_Hour" . sprintf ( "%02d" , $ h ) . "_PVreal" , 0 ) ;
2020-12-16 19:15:48 +00:00
next if ( ! $ pvval ) ;
2020-12-27 18:51:53 +00:00
my $ cdone = ReadingsVal ( $ name , "pvCorrectionFactor_" . sprintf ( "%02d" , $ h ) . "_autocalc" , "" ) ;
if ( $ cdone eq "done" ) {
Log3 ( $ name , 5 , "$name - pvCorrectionFactor Hour: " . sprintf ( "%02d" , $ h ) ) ;
next ;
}
2020-12-17 13:35:52 +00:00
2021-01-21 18:23:21 +00:00
my $ oldfac = ReadingsNum ( $ name , "pvCorrectionFactor_" . sprintf ( "%02d" , $ h ) , 1 ) ; # bisher definierter Korrekturfaktor
2020-12-16 19:15:48 +00:00
$ oldfac = 1 if ( 1 * $ oldfac == 0 ) ;
2021-01-20 21:00:34 +00:00
Log3 ( $ name , 5 , "$name - Hour: " . sprintf ( "%02d" , $ h ) . ", Today PVreal: $pvval, PVforecast: $fcval" ) ;
2020-12-15 13:41:10 +00:00
2021-01-21 18:23:21 +00:00
$ paref - > { hour } = $ h ;
my ( $ pvavg , $ fcavg ) = calcFromHistory ( $ paref ) ; # historische PV / Forecast Vergleichswerte ermitteln
2021-01-22 09:51:04 +00:00
$ pvval = $ pvavg ? ( $ pvval + $ pvavg ) / 2 : $ pvval ; # Ertrag aktuelle Stunde berücksichtigen
$ fcval = $ fcavg ? ( $ fcval + $ fcavg ) / 2 : $ fcval ; # Vorhersage aktuelle Stunde berücksichtigen
2021-01-21 18:23:21 +00:00
my $ factor = sprintf "%.2f" , ( $ pvval / $fcval); # Faktorberechnung: reale PV / Prognose
2020-12-27 20:04:17 +00:00
if ( abs ( $ factor - $ oldfac ) > $ maxvar ) {
$ factor = sprintf "%.2f" , ( $ factor > $ oldfac ? $ oldfac + $ maxvar : $ oldfac - $ maxvar ) ;
2021-01-22 09:51:04 +00:00
Log3 ( $ name , 3 , "$name - new limited Variance factor: $factor (old: $oldfac) for hour: $h" ) ;
2020-12-15 13:41:10 +00:00
}
else {
2021-01-22 09:51:04 +00:00
Log3 ( $ name , 3 , "$name - new Variance factor: $factor (old: $oldfac) for hour: $h calculated" ) if ( $ factor != $ oldfac ) ;
2020-12-15 13:41:10 +00:00
}
2020-12-17 08:44:29 +00:00
push @ da , "pvCorrectionFactor_" . sprintf ( "%02d" , $ h ) . ":" . $ factor . " (automatic)" ;
2020-12-27 18:51:53 +00:00
push @ da , "pvCorrectionFactor_" . sprintf ( "%02d" , $ h ) . "_autocalc:done" ;
2020-12-13 21:48:45 +00:00
}
2020-12-27 18:51:53 +00:00
createReadingsFromArray ( $ hash , \ @ da , 1 ) ;
2020-12-13 21:48:45 +00:00
return ;
}
2021-01-19 09:10:20 +00:00
################################################################
# Berechne Durchschnitte aus Werten der PV History
################################################################
sub calcFromHistory {
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ t = $ paref - > { t } ; # aktuelle Unix-Zeit
2021-01-21 18:23:21 +00:00
my $ hour = $ paref - > { hour } ; # Stunde für die der Durchschnitt bestimmt werden soll
2021-01-19 09:10:20 +00:00
2021-01-22 09:51:04 +00:00
$ hour = sprintf ( "%02d" , $ hour ) ;
2021-01-20 21:00:34 +00:00
my $ name = $ hash - > { NAME } ;
my $ type = $ hash - > { TYPE } ;
my $ day = strftime "%d" , localtime ( $ t ) ; # aktueller Tag
my $ pvhh = $ data { $ type } { $ name } { pvhist } ;
2021-01-19 09:10:20 +00:00
2021-01-20 21:00:34 +00:00
my $ calcd = AttrVal ( $ name , "numHistDays" , $ calcmaxd ) ;
2021-01-19 09:10:20 +00:00
my @ k = sort { $ a <=> $ b } keys % { $ pvhh } ;
my $ ile = $# k ; # Index letztes Arrayelement
my ( $ idx ) = grep { $ k [ $ _ ] eq "$day" } ( 0 .. @ k - 1 ) ; # Index des aktuellen Tages
2021-01-20 08:49:39 +00:00
if ( defined $ idx ) {
my $ ei = $ idx - 1 ;
$ ei = $ ei < 0 ? $ ile : $ ei ;
my @ efa ;
2021-01-20 21:00:34 +00:00
for my $ e ( 0 .. $ calcd ) {
last if ( $ e == $ calcd || $ k [ $ ei ] == $ day ) ;
unshift @ efa , $ k [ $ ei ] ;
2021-01-20 08:49:39 +00:00
$ ei - - ;
}
2021-01-20 21:00:34 +00:00
Log3 ( $ name , 4 , "$name - PV History -> Day $day has index $idx. Days ($calcd) for calc: " . join " " , @ efa ) ;
my $ pvhh = $ data { $ type } { $ name } { pvhist } ;
my $ anzavg = scalar ( @ efa ) ;
my ( $ pvrl , $ pvfc ) = ( 0 , 0 ) ;
2021-01-22 09:51:04 +00:00
2021-01-20 21:00:34 +00:00
for my $ dayfa ( @ efa ) {
2021-01-22 09:51:04 +00:00
$ pvrl += $ pvhh - > { $ dayfa } { $ hour } { pvrl } // 0 ;
$ pvfc += $ pvhh - > { $ dayfa } { $ hour } { pvfc } // 0 ;
2021-01-20 21:00:34 +00:00
}
2021-01-22 10:06:56 +00:00
my $ pvavg = sprintf "%.2f" , $ pvrl / $ anzavg ;
my $ fcavg = sprintf "%.2f" , $ pvfc / $ anzavg ;
2021-01-20 21:00:34 +00:00
2021-01-21 18:23:21 +00:00
Log3 ( $ name , 4 , "$name - PV History -> average hour ($hour) -> real: $pvavg, forecast: $fcavg" ) ;
2021-01-20 21:00:34 +00:00
return ( $ pvavg , $ fcavg ) ;
2021-01-20 08:49:39 +00:00
}
2021-01-19 09:10:20 +00:00
return ;
}
2021-01-17 19:13:02 +00:00
################################################################
# PV und PV Forecast in History-Hash speichern zur
# Berechnung des Korrekturfaktors über mehrere Tage
################################################################
sub setPVhistory {
my $ paref = shift ;
my $ hash = $ paref - > { hash } ;
my $ name = $ paref - > { name } ;
2021-01-23 08:01:53 +00:00
my $ t = $ paref - > { t } ; # aktuelle Unix-Zeit
2021-01-17 19:13:02 +00:00
my $ chour = $ paref - > { chour } ;
2021-01-19 12:12:28 +00:00
my $ ethishour = $ paref - > { ethishour } // 0 ;
my $ calcpv = $ paref - > { calcpv } // 0 ;
2021-01-17 19:13:02 +00:00
my $ type = $ hash - > { TYPE } ;
2021-01-23 08:01:53 +00:00
my $ day = strftime "%d" , localtime ( $ t ) ; # aktueller Tag
2021-01-17 19:13:02 +00:00
2021-01-23 08:01:53 +00:00
$ data { $ type } { $ name } { pvhist } { $ day } { $ chour } { pvrl } = $ ethishour if ( defined $ ethishour ) ; # realer Energieertrag
$ data { $ type } { $ name } { pvhist } { $ day } { $ chour } { pvfc } = $ calcpv if ( defined $ calcpv ) ; # prognostizierter Energieertrag
2021-01-22 20:16:33 +00:00
Log3 ( $ name , 5 , "$name - set PV History hour $chour -> real: $ethishour, forecast: $calcpv" ) ;
2021-01-17 19:13:02 +00:00
return ;
}
################################################################
# liefert aktuelle Einträge des PV Cache
################################################################
sub listPVHistory {
my $ hash = shift ;
my $ name = $ hash - > { NAME } ;
my $ type = $ hash - > { TYPE } ;
2021-01-19 09:10:20 +00:00
my $ pvhh = $ data { $ type } { $ name } { pvhist } ;
2021-01-17 19:13:02 +00:00
2021-01-22 20:57:14 +00:00
Log3 ( $ name , 5 , "$name - PV History content: " . Dumper $ pvhh ) ;
2021-01-17 19:13:02 +00:00
my $ sub = sub {
my $ day = shift ;
my $ ret ;
2021-01-19 09:10:20 +00:00
for my $ key ( sort { $ a <=> $ b } keys % { $ pvhh - > { $ day } } ) {
2021-01-19 12:12:28 +00:00
my $ pvrl = $ pvhh - > { $ day } { $ key } { pvrl } // 0 ;
my $ pvfc = $ pvhh - > { $ day } { $ key } { pvfc } // 0 ;
$ ret . = "\n " if ( $ ret ) ;
$ ret . = $ key . " => pvreal: $pvrl, pvforecast: $pvfc" ;
2021-01-17 19:13:02 +00:00
}
return $ ret ;
} ;
2021-01-19 09:10:20 +00:00
if ( ! keys % { $ pvhh } ) {
2021-01-17 19:13:02 +00:00
return qq{ PV cache is empty. } ;
}
my $ sq ;
2021-01-19 09:10:20 +00:00
for my $ idx ( sort { $ a <=> $ b } keys % { $ pvhh } ) {
2021-01-17 19:13:02 +00:00
$ sq . = $ idx . " => " . $ sub - > ( $ idx ) . "\n" ;
}
return $ sq ;
}
2020-12-13 17:29:15 +00:00
################################################################
# Zusammenfassungen erstellen
################################################################
sub sumNextHours {
my $ hash = shift ;
my $ chour = shift ; # aktuelle Stunde
my $ daref = shift ;
my $ name = $ hash - > { NAME } ;
my $ next4HoursSum = { "PV" = > 0 , "Consumption" = > 0 , "Total" = > 0 , "ConsumpRcmd" = > 0 } ;
my $ restOfDaySum = { "PV" = > 0 , "Consumption" = > 0 , "Total" = > 0 , "ConsumpRcmd" = > 0 } ;
my $ tomorrowSum = { "PV" = > 0 , "Consumption" = > 0 , "Total" = > 0 , "ConsumpRcmd" = > 0 } ;
my $ rdh = 24 - $ chour - 1 ; # verbleibende Anzahl Stunden am Tag beginnend mit 00 (abzüglich aktuelle Stunde)
my $ thforecast = ReadingsNum ( $ name , "ThisHour_PVforecast" , 0 ) ;
$ next4HoursSum - > { PV } = $ thforecast ;
$ restOfDaySum - > { PV } = $ thforecast ;
2020-12-15 21:27:21 +00:00
for my $ h ( 1 .. 47 ) {
2020-12-13 17:29:15 +00:00
$ next4HoursSum - > { PV } += ReadingsNum ( $ name , "NextHour" . ( sprintf "%02d" , $ h ) . "_PVforecast" , 0 ) if ( $ h <= 3 ) ;
$ restOfDaySum - > { PV } += ReadingsNum ( $ name , "NextHour" . ( sprintf "%02d" , $ h ) . "_PVforecast" , 0 ) if ( $ h <= $ rdh ) ;
$ tomorrowSum - > { PV } += ReadingsNum ( $ name , "NextHour" . ( sprintf "%02d" , $ h ) . "_PVforecast" , 0 ) if ( $ h > $ rdh ) ;
}
push @$ daref , "Next04Hours_PV:" . ( int $ next4HoursSum - > { PV } ) . " Wh" ;
push @$ daref , "RestOfDay_PV:" . ( int $ restOfDaySum - > { PV } ) . " Wh" ;
push @$ daref , "Tomorrow_PV:" . ( int $ tomorrowSum - > { PV } ) . " Wh" ;
2020-12-27 18:51:53 +00:00
createReadingsFromArray ( $ hash , $ daref , 1 ) ;
2020-12-13 17:29:15 +00:00
return ;
}
################################################################
2020-12-27 18:51:53 +00:00
# einen Zeitstring YYYY-MM-TT hh:mm:ss in einen Unix
# Timestamp umwandeln
################################################################
sub timestringToTimestamp {
my $ hash = shift ;
my $ tstring = shift ;
my $ name = $ hash - > { NAME } ;
my ( $ y , $ mo , $ d , $ h , $ m , $ s ) = $ tstring =~ /([0-9]{4})-([0-9]{2})-([0-9]{2})\s([0-9]{2}):([0-9]{2}):([0-9]{2})/xs ;
return if ( ! $ mo || ! $ y ) ;
my $ timestamp = fhemTimeLocal ( $ s , $ m , $ h , $ d , $ mo - 1 , $ y - 1900 ) ;
return $ timestamp ;
}
################################################################
# Readings aus Array erstellen
# $daref: Referenz zum Array der zu erstellenden Readings
# muß Paare <Readingname>:<Wert> enthalten
# $doevt: 1-Events erstellen, 0-keine Events erstellen
2020-12-13 17:29:15 +00:00
################################################################
2020-12-27 18:51:53 +00:00
sub createReadingsFromArray {
2020-12-13 17:29:15 +00:00
my $ hash = shift ;
my $ daref = shift ;
2020-12-27 18:51:53 +00:00
my $ doevt = shift // 0 ;
2020-12-13 17:29:15 +00:00
readingsBeginUpdate ( $ hash ) ;
for my $ elem ( @$ daref ) {
my ( $ rn , $ rval ) = split ":" , $ elem , 2 ;
readingsBulkUpdate ( $ hash , $ rn , $ rval ) ;
}
2020-12-27 18:51:53 +00:00
readingsEndUpdate ( $ hash , $ doevt ) ;
2020-12-13 17:29:15 +00:00
return ;
}
2020-12-16 09:15:37 +00:00
################################################################
# alle Readings eines Devices oder nur Reaging-Regex
# löschen
################################################################
sub deleteReadingspec {
my $ hash = shift ;
my $ spec = shift // ".*" ;
my $ readingspec = '^' . $ spec . '$' ;
for my $ reading ( grep { /$readingspec/ } keys % { $ hash - > { READINGS } } ) {
readingsDelete ( $ hash , $ reading ) ;
}
return ;
}
2020-12-13 17:29:15 +00:00
######################################################################################
# NOTIFYDEV erstellen
######################################################################################
sub createNotifyDev {
my $ hash = shift ;
my $ name = $ hash - > { NAME } ;
RemoveInternalTimer ( $ hash , "FHEM::SolarForecast::createNotifyDev" ) ;
if ( $ init_done == 1 ) {
my @ nd ;
2020-12-15 13:41:10 +00:00
my ( $ a , $ h ) ;
2020-12-27 18:51:53 +00:00
my $ fcdev = ReadingsVal ( $ name , "currentForecastDev" , "" ) ; # Forecast Device
( $ a , $ h ) = parseParams ( $ fcdev ) ;
$ fcdev = $ a - > [ 0 ] // "" ;
2020-12-13 17:29:15 +00:00
2020-12-27 18:51:53 +00:00
my $ indev = ReadingsVal ( $ name , "currentInverterDev" , "" ) ; # Inverter Device
2020-12-15 13:41:10 +00:00
( $ a , $ h ) = parseParams ( $ indev ) ;
$ indev = $ a - > [ 0 ] // "" ;
2020-12-27 18:51:53 +00:00
my $ medev = ReadingsVal ( $ name , "currentMeterDev" , "" ) ; # Meter Device
2020-12-15 13:41:10 +00:00
( $ a , $ h ) = parseParams ( $ medev ) ;
$ medev = $ a - > [ 0 ] // "" ;
2020-12-13 17:29:15 +00:00
push @ nd , $ fcdev ;
push @ nd , $ indev ;
push @ nd , $ medev ;
2020-12-27 18:51:53 +00:00
if ( @ nd ) {
$ hash - > { NOTIFYDEV } = join "," , @ nd ;
readingsSingleUpdate ( $ hash , ".associatedWith" , join ( " " , @ nd ) , 0 ) ;
}
2020-12-13 17:29:15 +00:00
}
else {
InternalTimer ( gettimeofday ( ) + 3 , "FHEM::SolarForecast::createNotifyDev" , $ hash , 0 ) ;
}
return ;
}
1 ;
= pod
= item summary Visualization of solar predictions for PV systems
= item summary_DE Visualisierung von solaren Vorhersagen für PV Anlagen
= begin html
= end html
= begin html_DE
< a name = "SolarForecast" > </a>
<h3> SolarForecast </h3>
<br>
2020-12-27 18:51:53 +00:00
2021-01-19 09:10:20 +00:00
Das Modul SolarForecast erstellt auf Grundlage der Werte aus generischen Quellendevices eine
2020-12-15 13:41:10 +00:00
Vorhersage für den solaren Ertrag und weitere Informationen als Grundlage für abhängige Steuerungen . <br>
2020-12-13 17:29:15 +00:00
Die Solargrafik kann ebenfalls in FHEM Tablet UI mit dem
< a href = "https://wiki.fhem.de/wiki/FTUI_Widget_SolarForecast" > "SolarForecast Widget" </a> integriert werden . <br> <br>
Die solare Vorhersage basiert auf der durch den Deutschen Wetterdienst ( DWD ) prognostizierten Globalstrahlung am
Anlagenstandort . Im zugeordneten DWD_OpenData Device ist die passende Wetterstation mit dem Attribut "forecastStation"
2021-01-19 09:10:20 +00:00
festzulegen um eine Prognose für diesen Standort zu erhalten . <br>
2020-12-13 17:29:15 +00:00
Abhängig von der physikalischen Anlagengestaltung ( Ausrichtung , Winkel , Aufteilung in mehrere Strings , u . a . ) wird die
verfügbare Globalstrahlung ganz spezifisch in elektrische Energie umgewandelt .
<ul>
< a name = "SolarForecastdefine" > </a>
<b> Define </b>
<br> <br>
<ul>
Ein SolarForecast Device wird einfach erstellt mit: <br> <br>
<ul>
<b> define & lt ; name & gt ; SolarForecast </b>
</ul>
<br>
Nach der Definition des Devices ist zwingend ein Vorhersage - Device des Typs DWD_OpenData zuzuordnen sowie weitere
2021-01-19 09:10:20 +00:00
anlagenspezifische Angaben mit dem entsprechenden set - Kommando vorzunehmen . <br>
Mit nachfolgenden set - Kommandos werden die Quellendevices und Quellenreadings für maßgebliche Informationen
hinterlegt: <br> <br>
<ul>
<table>
<colgroup> < col width = 35 % > < col width = 65 % > < / colgroup >
<tr> <td> <b> currentForecastDev </b> </td> <td> Device welches Strahlungsdaten liefert </td> </tr>
<tr> <td> <b> currentInverterDev </b> </td> <td> Device welches PV Leistungsdaten liefert </td> </tr>
<tr> <td> <b> currentMeterDev </b> </td> <td> Device welches aktuelle Netzbezugsdaten liefert </td> </tr>
</table>
</ul>
<br>
Um eine Anpassung an die persönliche Anlage zu ermöglichen , können Korrekturfaktoren manuell
( set & lt ; name & gt ; pvCorrectionFactor_XX ) bzw . automatisiert ( set & lt ; name & gt ; pvCorrectionFactor_Auto ) eingefügt
werden .
2020-12-13 17:29:15 +00:00
<br> <br>
</ul>
< a name = "SolarForecastset" > </a>
<b> Set </b>
<ul>
<ul>
2020-12-27 18:51:53 +00:00
< a name = "currentForecastDev" > </a>
<li> <b> currentForecastDev </b> <br>
2020-12-13 17:29:15 +00:00
Legt das Device ( Typ DWD_OpenData ) fest , welches die Daten der solaren Vorhersage liefert . Ist noch kein Device dieses Typs
vorhanden , muß es manuell definiert werden ( siehe < a href = "http://fhem.de/commandref.html#DWD_OpenData" > DWD_OpenData Commandref </a> ) . <br>
Im ausgewählten DWD_OpenData Device müssen mindestens diese Attribute gesetzt sein: <br> <br>
<ul>
<table>
<colgroup> < col width = 35 % > < col width = 65 % > < / colgroup >
2020-12-27 18:51:53 +00:00
<tr> <td> <b> forecastDays </b> </td> <td> 1 </td> </tr>
2021-01-23 08:01:53 +00:00
<tr> <td> <b> forecastProperties </b> </td> <td> Rad1h , TTT , Neff , R101 , ww , SunUp , SunRise , SunSet </td> </tr>
2020-12-27 18:51:53 +00:00
<tr> <td> <b> forecastResolution </b> </td> <td> 1 </td> </tr>
<tr> <td> <b> forecastStation </b> </td> <td> & lt ; Stationscode der ausgewerteten DWD Station & gt ; </td> </tr>
<tr> <td> </td> <td> <b> Hinweis: </b> Die ausgewählte forecastStation muß Strahlungswerte ( Rad1h Readings ) liefern . </td> </tr>
2020-12-13 17:29:15 +00:00
</table>
2020-12-27 18:51:53 +00:00
</ul>
2020-12-13 17:29:15 +00:00
</li>
</ul>
<br>
<ul>
2020-12-27 18:51:53 +00:00
< a name = "currentInverterDev" > </a>
<li> <b> currentInverterDev & lt ; Inverter Device Name & gt ; pv = & lt ; Reading aktuelle PV - Leistung & gt ; : & lt ; Einheit & gt ; etoday = & lt ; Reading Energieerzeugung aktueller Tag & gt ; : & lt ; Einheit & gt ; </b> <br>
2020-12-15 13:41:10 +00:00
Legt ein beliebiges Device zur Lieferung der aktuellen PV Erzeugungswerte fest .
Es ist anzugeben , welche Readings die aktuelle PV - Leistung und die erzeugte Energie des aktuellen Tages liefern sowie deren Einheit ( W , kW , Wh , kWh ) .
2021-01-23 12:15:30 +00:00
<br>
Das Reading im Schlüssel etoday muß die tägliche Erzeugung beginnend mit 0 um 00 : 00 Uhr aufsteigend bis zum maximalen
täglichen Ertrag am Ende des Tages enthalten .
2020-12-13 17:29:15 +00:00
<br> <br>
2020-12-15 13:41:10 +00:00
<ul>
<b> Beispiel: </b> <br>
2020-12-27 18:51:53 +00:00
set & lt ; name & gt ; currentInverterDev STP5000 pv = total_pac:kW etoday = etoday:kWh <br>
2021-01-23 12:15:30 +00:00
# Device STP5000 liefert PV-Werte. Die aktuell erzeugte Leistung im Reading "total_pac" (kW) und die tägliche Erzeugung im
2020-12-15 13:41:10 +00:00
Reading "etoday" ( kWh )
</ul>
2020-12-13 17:29:15 +00:00
</li>
</ul>
<br>
<ul>
2020-12-27 18:51:53 +00:00
< a name = "currentMeterDev" > </a>
<li> <b> currentMeterDev & lt ; Meter Device Name & gt ; gcon = & lt ; Reading aktueller Netzbezug & gt ; : & lt ; Einheit & gt ; </b> <br>
2020-12-15 13:41:10 +00:00
Legt ein beliebiges Device zur Messung des aktuellen Energiebezugs fest .
Es ist das Reading anzugeben welches die aktuell aus dem Netz bezogene Leistung liefert sowie dessen Einheit ( W , kW ) .
<br> <br>
<ul>
<b> Beispiel: </b> <br>
2020-12-27 18:51:53 +00:00
set & lt ; name & gt ; currentMeterDev SMA_Energymeter gcon = Bezug_Wirkleistung:W <br>
2020-12-15 13:41:10 +00:00
# Device SMA_Energymeter liefert den aktuellen Netzbezug im Reading "Bezug_Wirkleistung" (W)
</ul>
2020-12-13 17:29:15 +00:00
</li>
</ul>
<br>
2020-12-27 18:51:53 +00:00
<ul>
< a name = "inverterEfficiency" > </a>
<li> <b> inverterEfficiency & lt ; Zahl & gt ; </b> <br>
2021-01-21 22:07:18 +00:00
Wirkungsgrad des Wechselrichters ( currentInverterDev ) in % laut Herstellerangabe . <br>
2020-12-27 18:51:53 +00:00
( default: 98.3 )
</li>
</ul>
<br>
2020-12-13 17:29:15 +00:00
<ul>
< a name = "moduleArea" > </a>
<li> <b> moduleArea & lt ; Zahl & gt ; </b> <br>
Gesamte installierte Solarmodulfläche in qm .
</li>
</ul>
<br>
<ul>
< a name = "moduleEfficiency" > </a>
<li> <b> moduleEfficiency & lt ; Zahl & gt ; </b> <br>
2021-01-21 22:07:18 +00:00
Wirkungsgrad der Solarmodule in % laut Herstellerangabe . <br>
2020-12-13 17:29:15 +00:00
( default: 16.52 )
</li>
</ul>
<br>
2020-12-16 19:15:48 +00:00
<ul>
< a name = "moduleTiltAngle" > </a>
<li> <b> moduleTiltAngle </b> <br>
Neigungswinkel der Solarmodule ( 0 = waagerecht , 90 = senkrecht ) . <br>
( default: 45 )
</li>
</ul>
<br>
2020-12-15 13:41:10 +00:00
<ul>
< a name = "pvCorrectionFactor_Auto" > </a>
<li> <b> pvCorrectionFactor_Auto & lt ; on | off & gt ; </b> <br>
Schaltet die automatische Vorhersagekorrektur ein / aus . <br>
2020-12-15 21:27:21 +00:00
Ist die Automatik eingeschaltet , wird nach einer Mindestlaufzeit von FHEM bzw . des Moduls von 24 Stunden für jede Stunde
ein Korrekturfaktor der Solarvorhersage berechnet und auf die Erwartung des kommenden Tages angewendet .
2020-12-15 13:41:10 +00:00
Dazu wird die tatsächliche Energierzeugung mit dem vorhergesagten Wert des aktuellen Tages und Stunde vergleichen und
daraus eine Korrektur abgeleitet . <br>
( default: off )
</li>
</ul>
<br>
2020-12-13 17:29:15 +00:00
<ul>
< a name = "pvCorrectionFactor_XX" > </a>
<li> <b> pvCorrectionFactor_XX & lt ; Zahl & gt ; </b> <br>
Manueller Korrekturfaktor für die Stunde XX des Tages zur Anpassung der Vorhersage an die individuelle Anlage . <br>
( default: 1.0 )
</li>
</ul>
<br>
2021-01-19 17:23:24 +00:00
<ul>
< a name = "reset" > </a>
<li> <b> reset </b> <br>
Löscht die aus der Drop - Down Liste gewählte Datenquelle . <br>
</li>
</ul>
<br>
2020-12-13 17:29:15 +00:00
</ul>
<br>
< a name = "SolarForecastget" > </a>
<b> Get </b>
<ul>
<ul>
< a name = "html" > </a>
<li> <b> html </b> <br>
Die Solar Grafik wird als HTML - Code abgerufen und wiedergegeben .
</li>
</ul>
<br>
2020-12-15 13:54:31 +00:00
<ul>
< a name = "data" > </a>
<li> <b> data </b> <br>
Startet die Datensammlung zur Bestimmung der solaren Vorhersage und anderer Werte .
</li>
</ul>
<br>
2021-01-17 19:13:02 +00:00
<ul>
< a name = "pvHistory" > </a>
<li> <b> pvHistory </b> <br>
Listet die PV Werte der letzten Tage sortiert nach dem Tagesdatum und der Stunde des jeweiligen Tages auf .
Dabei sind <b> pvreal </b> der reale und <b> pvforecast </b> der prognostizierte PV Ertrag .
</li>
</ul>
<br>
2020-12-13 17:29:15 +00:00
</ul>
<br>
< a name = "SolarForecastattr" > </a>
<b> Attribute </b>
<br> <br>
<ul>
<ul>
< a name = "alias" > </a>
<li> <b> alias </b> <br>
2020-12-27 18:51:53 +00:00
In Verbindung mit "showLink" ein beliebiger Anzeigename .
2020-12-13 17:29:15 +00:00
</li>
<br>
< a name = "autoRefresh" > </a>
<li> <b> autoRefresh </b> <br>
Wenn gesetzt , werden aktive Browserseiten des FHEMWEB - Devices welches das SolarForecast - Device aufgerufen hat , nach der
eingestellten Zeit ( Sekunden ) neu geladen . Sollen statt dessen Browserseiten eines bestimmten FHEMWEB - Devices neu
geladen werden , kann dieses Device mit dem Attribut "autoRefreshFW" festgelegt werden .
</li>
<br>
< a name = "autoRefreshFW" > </a>
<li> <b> autoRefreshFW </b> <br>
Ist "autoRefresh" aktiviert , kann mit diesem Attribut das FHEMWEB - Device bestimmt werden dessen aktive Browserseiten
regelmäßig neu geladen werden sollen .
</li>
<br>
< a name = "beamColor" > </a>
<li> <b> beamColor </b> <br>
Farbauswahl der primären Balken .
</li>
<br>
< a name = "beamColor2" > </a>
<li> <b> beamColor2 </b> <br>
Farbauswahl der sekundären Balken . Die zweite Farbe ist nur sinnvoll für den Anzeigedevice - Typ "Generation_Consumption"
( pvco ) und "Differential" ( diff ) .
</li>
<br>
< a name = "beamHeight" > </a>
<li> <b> beamHeight & lt ; value & gt ; </b> <br>
Höhe der Balken in px und damit Bestimmung der gesammten Höhe .
In Verbindung mit "hourCount" lassen sich damit auch recht kleine Grafikausgaben erzeugen . <br>
( default: 200 )
</li>
<br>
< a name = "beamWidth" > </a>
<li> <b> beamWidth & lt ; value & gt ; </b> <br>
Breite der Balken in px . <br>
( default: 6 ( auto ) )
</li>
<br>
< a name = "consumerList" > </a>
<li> <b> consumerList & lt ; Verbraucher1 & gt ; : & lt ; Icon & gt ; @ & lt ; Farbe & gt ; , & lt ; Verbraucher2 & gt ; : & lt ; Icon & gt ; @ & lt ; Farbe & gt ; , ... </b> <br>
Komma getrennte Liste der am SMA Sunny Home Manager angeschlossenen Geräte . <br>
Sobald die Aktivierung einer der angegebenen Verbraucher geplant ist , wird der geplante Zeitraum in der Grafik
angezeigt .
Der Name des Verbrauchers muss dabei dem Namen im Reading "L3_<Verbrauchername>_Planned" entsprechen . <br> <br>
<b> Beispiel: </b> <br>
attr & lt ; name & gt ; consumerList Trockner:scene_clothes_dryer @ yellow , Waschmaschine:scene_washing_machine @ lightgreen , Geschirrspueler:scene_dishwasher @ orange
<br>
</li>
<br>
< a name = "consumerLegend" > </a>
<li> <b> consumerLegend & ltnone | icon_top | icon_bottom | text_top | text_bottom & gt ; </b> <br>
Lage bzw . Art und Weise der angezeigten Verbraucherlegende .
</li>
<br>
< a name = "disable" > </a>
<li> <b> disable </b> <br>
Aktiviert / deaktiviert das Device .
</li>
<br>
< a name = "forcePageRefresh" > </a>
<li> <b> forcePageRefresh </b> <br>
Das Attribut wird durch das SMAPortal - Device ausgewertet . <br>
Wenn gesetzt , wird ein Reload aller Browserseiten mit aktiven FHEMWEB - Verbindungen nach dem Update des
Eltern - SMAPortal - Devices erzwungen .
</li>
<br>
< a name = "headerAlignment" > </a>
<li> <b> headerAlignment & lt ; center | left | right & gt ; </b> <br>
Ausrichtung der Kopfzeilen . <br>
( default: center )
</li>
<br>
2021-01-20 21:00:34 +00:00
< a name = "hourCount" > </a>
<li> <b> hourCount & lt ; 4 ... 24 & gt ; </b> <br>
Anzahl der Balken / Stunden . <br>
( default: 24 )
</li>
<br>
2020-12-13 17:29:15 +00:00
< a name = "headerDetail" > </a>
<li> <b> headerDetail & lt ; all | co | pv | pvco | statusLink & gt ; </b> <br>
Detailiierungsgrad der Kopfzeilen . <br>
( default: all )
<ul>
<table>
<colgroup> < col width = 15 % > < col width = 85 % > < / colgroup >
<tr> <td> <b> all </b> </td> <td> Anzeige Erzeugung ( PV ) , Verbrauch ( CO ) , Link zur Device Detailanzeige + Aktualisierungszeit ( default ) </td> </tr>
<tr> <td> <b> co </b> </td> <td> nur Verbrauch ( CO ) </td> </tr>
<tr> <td> <b> pv </b> </td> <td> nur Erzeugung ( PV ) </td> </tr>
<tr> <td> <b> pvco </b> </td> <td> Erzeugung ( PV ) und Verbrauch ( CO ) </td> </tr>
<tr> <td> <b> statusLink </b> </td> <td> Link zur Device Detailanzeige + Aktualisierungszeit </td> </tr>
</table>
</ul>
</li>
<br>
< a name = "hourStyle" > </a>
<li> <b> hourStyle </b> <br>
Format der Zeitangabe . <br> <br>
2020-12-15 13:41:10 +00:00
<ul>
2020-12-13 17:29:15 +00:00
<table>
2020-12-15 13:41:10 +00:00
<colgroup> < col width = 10 % > < col width = 90 % > < / colgroup >
<tr> <td> <b> nicht gesetzt </b> </td> <td> - nur Stundenangabe ohne Minuten ( default ) </td> </tr>
<tr> <td> <b> : 00 </b> </td> <td> - Stunden sowie Minuten zweistellig , z . B . 10 : 00 </td> </tr>
<tr> <td> <b> : 0 </b> </td> <td> - Stunden sowie Minuten einstellig , z . B . 8 : 0 </td> </tr>
2020-12-13 17:29:15 +00:00
</table>
2020-12-15 13:41:10 +00:00
</ul>
2020-12-13 17:29:15 +00:00
</li>
<br>
2020-12-15 13:41:10 +00:00
2021-01-20 21:00:34 +00:00
< a name = "htmlStart" > </a>
<li> <b> htmlStart & lt ; HTML - String & gt ; </b> <br>
Angabe eines beliebigen HTML - Strings der vor dem Grafik - Code ausgeführt wird .
</li>
<br>
< a name = "htmlEnd" > </a>
<li> <b> htmlEnd & lt ; HTML - String & gt ; </b> <br>
Angabe eines beliebigen HTML - Strings der nach dem Grafik - Code ausgeführt wird .
</li>
<br>
2020-12-15 13:41:10 +00:00
< a name = "interval" > </a>
<li> <b> interval & lt ; Sekunden & gt ; </b> <br>
Zeitintervall der Datensammlung . <br>
Ist interval explizit auf "0" gesetzt , erfolgt keine automatische Datensammlung und muss mit "get <name> data"
manuell erfolgen . <br>
( default: 70 )
</li> <br>
2020-12-13 17:29:15 +00:00
< a name = "maxPV" > </a>
<li> <b> maxPV & lt ; 0 ... val & gt ; </b> <br>
Maximaler Ertrag in einer Stunde zur Berechnung der Balkenhöhe . <br>
( default: 0 - > dynamisch )
</li>
<br>
2020-12-27 20:04:17 +00:00
< a name = "maxVariancePerDay" > </a>
<li> <b> maxVariancePerDay & lt ; Zahl & gt ; </b> <br>
Maximale Änderungsgröße des PV Vorhersagefaktors ( Reading pvCorrectionFactor_XX ) pro Tag . <br>
( default: 0.5 )
</li>
<br>
2021-01-20 21:00:34 +00:00
< a name = "numHistDays" > </a>
<li> <b> numHistDays </b> <br>
Anzahl der vergangenen Tage die zur Durchschnittsermittlung von PV Erzeugung und PV Vorhersage aus den historischen
Daten verwendet werden . Diese Werte dienen zur automatischen Vorhersageanpassung sofern verwendet . <br>
( default: 30 )
2020-12-13 17:29:15 +00:00
</li>
<br>
< a name = "showDiff" > </a>
<li> <b> showDiff & lt ; no | top | bottom & gt ; </b> <br>
Zusätzliche Anzeige der Differenz "Ertrag - Verbrauch" wie beim Anzeigetyp Differential ( diff ) . <br>
( default: no )
</li>
<br>
< a name = "showHeader" > </a>
<li> <b> showHeader </b> <br>
Anzeige der Kopfzeile mit Prognosedaten , Rest des aktuellen Tages und des nächsten Tages <br>
( default: 1 )
</li>
<br>
< a name = "showLink" > </a>
<li> <b> showLink </b> <br>
Anzeige des Detail - Links über dem Grafik - Device <br>
( default: 1 )
</li>
<br>
< a name = "showNight" > </a>
<li> <b> showNight </b> <br>
Die Nachtstunden ( ohne Ertragsprognose ) werden mit angezeigt . <br>
( default: 0 )
</li>
<br>
< a name = "showWeather" > </a>
<li> <b> showWeather </b> <br>
Wettericons anzeigen . <br>
( default: 1 )
</li>
<br>
< a name = "spaceSize" > </a>
<li> <b> spaceSize & lt ; value & gt ; </b> <br>
Legt fest wieviel Platz in px über oder unter den Balken ( bei Anzeigetyp Differential ( diff ) ) zur Anzeige der
Werte freigehalten wird . Bei Styles mit große Fonts kann der default - Wert zu klein sein bzw . rutscht ein
Balken u . U . über die Grundlinie . In diesen Fällen bitte den Wert erhöhen . <br>
( default: 24 )
</li>
<br>
< a name = "consumerAdviceIcon" > </a>
<li> <b> consumerAdviceIcon </b> <br>
Setzt das Icon zur Darstellung der Zeiten mit Verbraucherempfehlung .
Dazu kann ein beliebiges Icon mit Hilfe der Standard "Select Icon" - Funktion ( links unten im FHEMWEB ) direkt ausgewählt
werden .
</li>
<br>
< a name = "layoutType" > </a>
<li> <b> layoutType & lt ; pv | co | pvco | diff & gt ; </b> <br>
Layout der Portalgrafik . <br>
( default: pv )
<br> <br>
<ul>
<table>
<colgroup> < col width = 15 % > < col width = 85 % > < / colgroup >
<tr> <td> <b> pv </b> </td> <td> - Erzeugung </td> </tr>
<tr> <td> <b> co </b> </td> <td> - Verbrauch </td> </tr>
<tr> <td> <b> pvco </b> </td> <td> - Erzeugung und Verbrauch </td> </tr>
<tr> <td> <b> diff </b> </td> <td> - Differenz von Erzeugung und Verbrauch </td> </tr>
</table>
</ul>
</li>
<br>
< a name = "Wh/kWh" > </a>
<li> <b> Wh /kWh <Wh | kWh> </ b > <br>
Definiert die Anzeigeeinheit in Wh oder in kWh auf eine Nachkommastelle gerundet . <br>
( default: W )
</li>
<br>
< a name = "weatherColor" > </a>
<li> <b> weatherColor </b> <br>
Farbe der Wetter - Icons .
</li>
2021-01-01 19:44:20 +00:00
<br>
< a name = "weatherColor_night" > </a>
<li> <b> weatherColor_night </b> <br>
Farbe der Wetter - Icons für die Nachtstunden .
</li>
2020-12-13 17:29:15 +00:00
<br>
</ul>
</ul>
</ul>
= end html_DE
= for : application / json ; q = META . json 76 _SolarForecast . pm
{
"abstract" : "Visualization of solar predictions for PV systems" ,
"x_lang" : {
"de" : {
"abstract" : "Visualisierung von solaren Vorhersagen für PV Anlagen"
}
} ,
"keywords" : [
"sma" ,
"photovoltaik" ,
"electricity" ,
"portal" ,
"smaportal" ,
"graphics" ,
"longpoll" ,
"refresh"
] ,
"version" : "v1.1.1" ,
"release_status" : "testing" ,
"author" : [
"Heiko Maaz <heiko.maaz@t-online.de>"
] ,
"x_fhem_maintainer" : [
"DS_Starter"
] ,
"x_fhem_maintainer_github" : [
"nasseeder1"
] ,
"prereqs" : {
"runtime" : {
"requires" : {
"FHEM" : 5.00918799 ,
"perl" : 5.014 ,
"Time::HiRes" : 0
} ,
"recommends" : {
"FHEM::Meta" : 0
} ,
"suggests" : {
}
}
} ,
"resources" : {
"repository" : {
"x_dev" : {
"type" : "svn" ,
"url" : "https://svn.fhem.de/trac/browser/trunk/fhem/contrib/DS_Starter" ,
"web" : "https://svn.fhem.de/trac/browser/trunk/fhem/contrib/DS_Starter/76_SolarForecast.pm" ,
"x_branch" : "dev" ,
"x_filepath" : "fhem/contrib/" ,
"x_raw" : "https://svn.fhem.de/fhem/trunk/fhem/contrib/DS_Starter/76_SolarForecast.pm"
}
}
}
}
= end : application / json ; q = META . json
= cut