From bfbc99516e3e1c27d37fb465485cc7e26d8c438a Mon Sep 17 00:00:00 2001 From: DS_Starter Date: Thu, 3 Oct 2024 19:00:06 +0000 Subject: [PATCH] 76_SolarForecast: features and changes, see https://forum.fhem.de/index.php?msg=1321556 git-svn-id: https://svn.fhem.de/fhem/trunk@29192 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 2 + fhem/FHEM/76_SolarForecast.pm | 1275 +++++++++++++++++++++------------ 2 files changed, 815 insertions(+), 462 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index 480ccaafa..73e39eb07 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,7 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it + - feature: 76_SolarForecast: features and changes, see + https://forum.fhem.de/index.php?msg=1321556 - change: 74_AutomowerConnect: convert model to upper case due to changed spelling - bugfix: 76_SolarForecast: internal code change diff --git a/fhem/FHEM/76_SolarForecast.pm b/fhem/FHEM/76_SolarForecast.pm index a742051a2..c8c73a049 100644 --- a/fhem/FHEM/76_SolarForecast.pm +++ b/fhem/FHEM/76_SolarForecast.pm @@ -4,6 +4,7 @@ # 76_SolarForecast.pm # # (c) 2020-2024 by Heiko Maaz e-mail: Heiko dot Maaz at t-online dot de +# with credits to: kask, Prof. Dr. Peter Henning, Wzut (and much more FHEM users) # # This script is part of fhem. # @@ -37,6 +38,7 @@ 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 tv_interval); use Math::Trig; +use List::Util qw(max); eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; ## no critic 'eval' eval "use FHEM::Utility::CTZ qw(:all);1;" or my $ctzAbsent = 1; ## no critic 'eval' @@ -48,14 +50,13 @@ use HttpUtils; eval "use JSON;1;" or my $jsonabs = 'JSON'; ## no critic 'eval' # Debian: sudo apt-get install libjson-perl eval "use AI::DecisionTree;1;" or my $aidtabs = 'AI::DecisionTree'; ## no critic 'eval' -use FHEM::SynoModules::SMUtils qw( - checkModVer +use FHEM::SynoModules::SMUtils qw (checkModVer evaljson getClHash delClHash moduleVersion trim - ); # Hilfsroutinen Modul + ); # Hilfsroutinen Modul use Data::Dumper; use Blocking; @@ -66,8 +67,7 @@ use MIME::Base64; BEGIN { # Import from main:: GP_Import( - qw( - attr + qw (attr asyncOutput AnalyzePerlCommand AnalyzeCommandChain @@ -135,7 +135,7 @@ BEGIN { FW_widgetOverride FW_wname readyfnlist - ) + ) ); # Export to main context with different name @@ -156,9 +156,18 @@ BEGIN { # Versions History intern my %vNotesIntern = ( + "1.34.0" => "03.10.2024 implement ___areaFactorTrack for calculation of direct area factor and share of direct radiation ". + "note in Reading pvCorrectionFactor_XX if AI prediction was used in relevant hour ". + "AI usage depending either of available number of rules or difference to api forecast ". + "minor fix in ___readCandQ, new experimental attribute ctrlAreaFactorUsage ". + "optional icon in attr setupOtherProducerXX, integrate Producer to _flowGraphic (kask) ". + "don't show Consumer or Producer if it isn't defined any kind of it ". + "Optimization of space in the flow chart above generators and below consumers ". + "_beamGraphic: implement barcount to Limit the number of bars in level 2 if the number of bars in ". + "level 1 is less than graphicHourCount (fall/winter) ", "1.33.1" => "27.09.2024 bugfix of 1.33.0, add aiRulesNumber to pvCircular, limits of AI trained datasets for ". - "AI use (aiAccTRNLim, aiSpreadTRNLim)", - "1.33.0" => "26.09.2024 substitute area factor hash by ___areaFactorPV function ", + "AI use (aiAccTRNMin, aiSpreadTRNMin)", + "1.33.0" => "26.09.2024 substitute area factor hash by ___areaFactorFix function ", "1.32.0" => "02.09.2024 new attr setupOtherProducerXX, report calculation and storage of negative consumption values ". "Forum: https://forum.fhem.de/index.php?msg=1319083 ". "bugfix in _estConsumptionForecast, new ctrlDebug consumption_long ", @@ -240,7 +249,7 @@ my %vNotesIntern = ( "delete CircularCloudkorrVal, show sun position in beamgrafic weather mouse over ". "split pvCorrection into pvCorrectionRead and pvCorrectionWrite ". "_checkSetupNotComplete: improve setup Wizzard for ForecastSolar-API ", - "1.16.2" => "22.02.2024 minor changes, R101 -> RR1c, totalrain instead of weatherrainprob, delete wrp r101 ". + "1.16.2" => "22.02.2024 minor changes, R101 -> RR1c, rr1c instead of weatherrainprob, delete wrp r101 ". "delete wrp from circular & airaw, remove rain2bin, __getDWDSolarData: change \$runh, ". "fix Illegal division by zero Forum: https://forum.fhem.de/index.php?msg=1304009 ". "DWD API: check age of Rad1h data, store pvcorrf of sunalt with value 200+x in pvCircular ", @@ -347,7 +356,7 @@ my %vNotesIntern = ( "0.80.16"=> "26.07.2023 new consumer type noSchedule, expand maxconsumer to 16, minor changes/fixes ", "0.80.15"=> "24.07.2023 new sub getDebug, new key switchdev in consumer attributes, change Debug consumtion ". "reorg data in pvHistory when a hour of day was deleted ", - "0.80.14"=> "21.07.2023 __substConsumerIcon: use isConsumerLogOn instead of isConsumerPhysOn ", + "0.80.14"=> "21.07.2023 __substituteIcon: use isConsumerLogOn instead of isConsumerPhysOn ", "0.80.13"=> "18.07.2023 include parameter DoN in nextHours hash, new KPI's todayConForecastTillSunset, currentRunMtsConsumer_XX ". "minor fixes and improvements ", "0.80.12"=> "16.07.2023 preparation for alternative switch device in consumer attribute, revise CommandRef ". @@ -418,13 +427,13 @@ my $pvhexprtcsv = $root."/FHEM/FhemUtils/PVH_Export_SolarForecast_"; my $aitrblto = 7200; # KI Training BlockingCall Timeout my $aibcthhld = 0.2; # Schwelle der KI Trainigszeit ab der BlockingCall benutzt wird my $aitrstartdef = 2; # default Stunde f. Start AI-Training -my $aistdudef = 1095; # default Haltezeit KI Raw Daten (Tage) +my $aistdudef = 1825; # default Haltezeit KI Raw Daten (Tage) my $aiSpreadUpLim = 120; # obere Abweichungsgrenze (%) AI 'Spread' von API Prognose my $aiSpreadLowLim = 80; # untere Abweichungsgrenze (%) AI 'Spread' von API Prognose my $aiAccUpLim = 130; # obere Abweichungsgrenze (%) AI 'Accurate' von API Prognose my $aiAccLowLim = 70; # untere Abweichungsgrenze (%) AI 'Accurate' von API Prognose -my $aiAccTRNLim = 1500; # Mindestanzahl KI Trainingssätze für Verwendung "KI Accurate" -my $aiSpreadTRNLim = 3500; # Mindestanzahl KI Trainingssätze für Verwendung "KI Spreaded" +my $aiAccTRNMin = 5500; # Mindestanzahl KI Trainingssätze für Verwendung "KI Accurate" +my $aiSpreadTRNMin = 7000; # Mindestanzahl KI Trainingssätze für Verwendung "KI Spreaded" my $calcmaxd = 30; # Anzahl Tage die zur Berechnung Vorhersagekorrektur verwendet werden my @dweattrmust = qw(TTT Neff RR1c ww SunUp SunRise SunSet); # Werte die im Attr forecastProperties des Weather-DWD_Opendata Devices mindestens gesetzt sein müssen @@ -471,6 +480,9 @@ my $b4coldef = 'DBDBD0'; my $b4fontcoldef = '000000'; # default Schriftfarbe Beam 4 my $fgCDdef = 130; # Abstand Verbrauchericons zueinander +my $prodicondef = 'sani_garden_pump'; # default Producer-Icon +my $consicondef = 'light_light_dim_100'; # default Consumer-Icon + my $bPath = 'https://svn.fhem.de/trac/browser/trunk/fhem/contrib/SolarForecast/'; # Basispfad Abruf contrib SolarForecast Files my $pPath = '?format=txt'; # Download Format my $cfile = 'controls_solarforecast.txt'; # Name des Controlfiles @@ -492,8 +504,8 @@ my $cssdef = qq{.flowg.text { stroke: none; fill: gray; font-size: 60p qq{.flowg.active_bat_in { stroke: darkorange; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.8; animation: dash 0.5s linear; animation-iteration-count: infinite; } \n}. qq{.flowg.active_bat_out { stroke: green; stroke-dashoffset: 20; stroke-dasharray: 10; opacity: 0.8; animation: dash 0.5s linear; animation-iteration-count: infinite; } \n} ; - -# initiale Hashes für Stunden Consumption Forecast inkl. und exkl. Verbraucher + +# initiale Hashes für Stunden Consumption Forecast inkl. und exkl. Verbraucher my $conhfc = { "01" => 0, "02" => 0, "03" => 0, "04" => 0, "05" => 0, "06" => 0, "07" => 0, "08" => 0, "09" => 0, "10" => 0, "11" => 0, "12" => 0, "13" => 0, "14" => 0, "15" => 0, "16" => 0, "17" => 0, "18" => 0, "19" => 0, "20" => 0, "21" => 0, "22" => 0, "23" => 0, "24" => 0, @@ -1103,7 +1115,7 @@ my %hfspvh = ( batouttotal => { fn => \&_storeVal, storname => 'batouttotal', validkey => undef, fpar => undef }, # totale Batterieentladung weatherid => { fn => \&_storeVal, storname => 'weatherid', validkey => undef, fpar => undef }, # Wetter ID weathercloudcover => { fn => \&_storeVal, storname => 'wcc', validkey => undef, fpar => undef }, # Wolkenbedeckung - totalrain => { fn => \&_storeVal, storname => 'rr1c', validkey => undef, fpar => undef }, # Gesamtniederschlag (1-stündig) letzte 1 Stunde + rr1c => { fn => \&_storeVal, storname => 'rr1c', validkey => undef, fpar => undef }, # Gesamtniederschlag (1-stündig) letzte 1 Stunde pvcorrfactor => { fn => \&_storeVal, storname => 'pvcorrf', validkey => undef, fpar => undef }, # pvCorrectionFactor temperature => { fn => \&_storeVal, storname => 'temp', validkey => undef, fpar => undef }, # Außentemperatur conprice => { fn => \&_storeVal, storname => 'conprice', validkey => undef, fpar => undef }, # Bezugspreis pro kWh der Stunde @@ -1159,7 +1171,7 @@ sub Initialize { $consumer .= "consumer${c}:textField-long "; push @allc, $c; } - + for my $prn (1..$maxproducer) { $prn = sprintf "%02d", $prn; $setupprod .= "setupOtherProducer${prn}:textField-long "; @@ -1196,6 +1208,7 @@ sub Initialize { "ctrlBatSocManagement:textField-long ". "ctrlConsRecommendReadings:multiple-strict,$allcs ". "ctrlDebug:multiple-strict,$dm,#14 ". + "ctrlAreaFactorUsage:fix,trackFull,trackShared,trackFlex ". "ctrlGenPVdeviation:daily,continuously ". "ctrlInterval ". "ctrlLanguage:DE,EN ". @@ -1303,10 +1316,10 @@ sub Define { useErrCodes => 0, useCTZ => 1, }; - + use version 0.77; our $VERSION = moduleVersion ($params); # Versionsinformationen setzen delete $params->{hash}; - + createAssociatedWith ($hash); $params->{file} = $pvhcache.$name; # Cache File PV History einlesen wenn vorhanden @@ -1357,7 +1370,7 @@ sub _readCacheFile { my $file = $paref->{file}; my $cachename = $paref->{cachename}; my $title = $paref->{title}; - + my $hash = $defs{$name}; if ($cachename eq 'aitrained') { @@ -1593,7 +1606,7 @@ sub _setconsumerImmediatePlanning { ## no critic "not used" my $c = $paref->{prop}; my $evt = $paref->{prop1} // 0; # geändert V 1.1.0 - 1 -> 0 my $hash = $defs{$name}; - + return qq{no consumer number specified} if(!$c); return qq{no valid consumer id "$c"} if(!ConsumerVal ($hash, $c, "name", "")); @@ -1652,7 +1665,7 @@ sub _setconsumerNewPlanning { ## no critic "not used" my $c = $paref->{prop}; my $evt = $paref->{prop1} // 0; # geändert V 1.1.0 - 1 -> 0 my $hash = $defs{$name}; - + return qq{no consumer number specified} if(!$c); return qq{no valid consumer id "$c"} if(!ConsumerVal ($hash, $c, 'name', '')); @@ -1675,9 +1688,9 @@ sub _setroofIdentPair { ## no critic "not used" my $type = $paref->{type}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; - + my $hash = $defs{$name}; - + if (!$arg) { return qq{The command "$opt" needs an argument !}; } @@ -1716,7 +1729,7 @@ sub _setVictronCredentials { ## no critic "not used" my $type = $paref->{type}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; - + my $hash = $defs{$name}; my $msg; @@ -1755,7 +1768,7 @@ sub _setoperationMode { ## no critic "not used" my $paref = shift; my $name = $paref->{name}; my $prop = $paref->{prop} // return qq{no mode specified}; - + my $hash = $defs{$name}; singleUpdateState ( {hash => $hash, state => $prop, evt => 1} ); @@ -1787,7 +1800,7 @@ sub _setTrigger { ## no critic "not used" return qq{The key "$key" is invalid. Please consider the commandref.}; } } - + my $hash = $defs{$name}; if ($opt eq 'powerTrigger') { @@ -1830,7 +1843,7 @@ sub _setstringDeclination { ## no critic "not used" return qq{The inclination angle of "$key" is incorrect}; } } - + my $hash = $defs{$name}; readingsSingleUpdate ($hash, 'setupStringDeclination', $arg, 1); @@ -1869,7 +1882,7 @@ sub _setstringAzimuth { ## no critic "not used" return qq{The module direction of "$key" is wrong: $value}; } } - + my $hash = $defs{$name}; readingsSingleUpdate ($hash, 'setupStringAzimuth', $arg, 1); @@ -1891,7 +1904,7 @@ sub _setplantConfiguration { ## no critic "not used" my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; - + my $hash = $defs{$name}; my ($err,$nr,$na,@pvconf); @@ -1951,9 +1964,9 @@ sub __plantCfgAsynchOut { my $paref = shift; my $name = $paref->{name}; my $out = $paref->{out}; - + my $hash = $defs{$name}; - + asyncOutput($hash->{HELPER}{CL}{1}, $out); delClHash ($name); @@ -1968,7 +1981,7 @@ sub _setpvCorrectionFactor { ## no critic "not used" my $name = $paref->{name}; my $opt = $paref->{opt}; my $prop = $paref->{prop} // return qq{no correction value specified}; - + my $hash = $defs{$name}; if ($prop !~ /[0-9,.]/x) { @@ -1976,7 +1989,7 @@ sub _setpvCorrectionFactor { ## no critic "not used" } $prop =~ s/,/./x; - + my ($acu, $aln) = isAutoCorrUsed ($name); my $mode = $acu =~ /on/xs ? 'manual flex' : 'manual fix'; @@ -2000,7 +2013,7 @@ sub _setpvCorrectionFactorAuto { ## no critic "not used" my $prop = $paref->{prop} // return qq{no correction value specified}; my $hash = $defs{$name}; - + if ($prop eq 'noLearning') { my $pfa = ReadingsVal ($name, 'pvCorrectionFactor_Auto', 'off'); # aktuelle Autokorrektureinstellung $prop = $pfa.' '.$prop; @@ -2012,7 +2025,7 @@ sub _setpvCorrectionFactorAuto { ## no critic "not used" for my $n (1..24) { $n = sprintf "%02d", $n; my $rv = ReadingsVal ($name, "pvCorrectionFactor_${n}", ""); - + if ($rv !~ /manual/xs) { deleteReadingspec ($hash, "pvCorrectionFactor_${n}.*"); } @@ -2028,14 +2041,14 @@ sub _setpvCorrectionFactorAuto { ## no critic "not used" for my $n (1..24) { $n = sprintf "%02d", $n; my $rv = ReadingsVal ($name, "pvCorrectionFactor_${n}", ""); - + if ($rv =~ /manual/xs) { $rv =~ s/fix/flex/xs; - readingsSingleUpdate ($hash, "pvCorrectionFactor_${n}", $rv, 0); + readingsSingleUpdate ($hash, "pvCorrectionFactor_${n}", $rv, 0); } - } + } } - + writeCacheToFile ($hash, 'plantconfig', $plantcfg.$name); # Anlagenkonfiguration sichern return; @@ -2248,7 +2261,7 @@ sub _setreset { ## no critic "not used" my $c = $paref->{prop1} // ''; # bestimmten Verbraucher setzen falls angegeben if ($c) { - $paref->{c} = $c; + $paref->{c} = $c; delConsumerFromMem ($paref); # spezifischen Consumer aus History löschen } else { @@ -2257,10 +2270,10 @@ sub _setreset { ## no critic "not used" delConsumerFromMem ($paref); # alle Consumer aus History löschen } } - + delete $paref->{c}; $data{$type}{$name}{current}{consumerCollected} = 0; # Consumer neu sammeln - + writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben centralTask ($hash, 0); } @@ -2278,7 +2291,7 @@ sub _setoperatingMemory { ## no critic "not used" my $paref = shift; my $name = $paref->{name}; my $prop = $paref->{prop} // return qq{no operation specified for command}; - + my $hash = $defs{$name}; if ($prop eq 'save') { @@ -2325,7 +2338,7 @@ sub _setclientAction { ## no critic "not used" my $opt = $paref->{opt}; my $arg = $paref->{arg}; my $argsref = $paref->{argsref}; - + my $hash = $defs{$name}; if (!$arg) { @@ -2460,6 +2473,7 @@ sub Get { t => $t, chour => (strftime "%H", localtime($t)), # aktuelle Stunde in 24h format (00-23) date => (strftime "%Y-%m-%d", localtime($t)), + day => (strftime "%d", localtime($t)), # aktueller Tag (range 01 .. 31) debug => getDebug ($hash), lang => getLang ($hash) }; @@ -2488,10 +2502,10 @@ sub _getRoofTopData { my $name = $paref->{name}; my $type = $paref->{type}; my $hash = $defs{$name}; - + delete $data{$type}{$name}{current}{dwdRad1hAge}; delete $data{$type}{$name}{current}{dwdRad1hAgeTS}; - + my $ret = "$name is not a valid SolarForeCast Model: ".$hash->{MODEL}; if ($hash->{MODEL} eq 'SolCastAPI') { @@ -2523,7 +2537,7 @@ sub __getSolCastData { my $t = $paref->{t} // time; my $debug = $paref->{debug}; my $lang = $paref->{lang}; - + my $hash = $defs{$name}; my $msg; @@ -2623,7 +2637,7 @@ sub __solCast_ApiRequest { my $debug = $paref->{debug}; my $hash = $defs{$name}; - + if (!$allstrings) { # alle Strings wurden abgerufen return; } @@ -2980,7 +2994,7 @@ sub __getForecastSolarData { my $force = $paref->{force} // 0; my $t = $paref->{t} // time; my $lang = $paref->{lang}; - + my $hash = $defs{$name}; if (!$force) { # regulärer API Abruf @@ -3038,7 +3052,7 @@ sub __forecastSolar_ApiRequest { my $type = $paref->{type}; my $allstrings = $paref->{allstrings}; # alle Strings my $debug = $paref->{debug}; - + my $hash = $defs{$name}; if (!$allstrings) { # alle Strings wurden abgerufen @@ -3267,7 +3281,7 @@ sub ___setForeCastAPIcallKeyData { my $lang = $paref->{lang}; my $debug = $paref->{debug}; my $t = $paref->{t} // time; - + my $hash = $defs{$name}; $data{$type}{$name}{solcastapi}{'?All'}{'?All'}{todayDoneAPIrequests} += 1; @@ -3303,14 +3317,10 @@ return; } ################################################################################################## -# Abruf DWD Strahlungsdaten und Rohdaten ohne Korrektur -# speichern in solcastapi Hash +# Abruf DWD Strahlungsdaten und Rohdaten ohne Korrektur # -# !!!! NACHFOLGENDE INFO GILT NUR BEI DWD RAD1H VERWENDUNG !!!! -# ############################################################# -# -# PV Forecast Rad1h in kWh / Wh -# Berechnung nach Formel 1 aus http://www.ing-büro-junge.de/html/photovoltaik.html: +# Berechnung nach Formel 1 aus http://www.ing-büro-junge.de/html/photovoltaik.html +# als Jahreserträge: # # * Faktor für Umwandlung kJ in kWh: 0.00027778 # * Eigene Modulfläche in qm z.B.: 31,04 @@ -3318,8 +3328,8 @@ return; # * Wirkungsgrad WR in % z.B.: 98,3 # * Korrekturwerte wegen Ausrichtung/Verschattung etc. # -# Die Formel wäre dann: -# Ertrag in Wh = Rad1h * 0.00027778 * 31,04 qm * 16,52% * 98,3% * 100% * 1000 +# Die Formel wäre dann: +# Ertrag in Wh = Rad1h * 0.00027778 * 31,04 qm * 16,52% * 98,3% * 100% * 1000 # # Berechnung nach Formel 2 aus http://www.ing-büro-junge.de/html/photovoltaik.html: # @@ -3337,7 +3347,7 @@ return; # hier beschrieben: # https://www.energie-experten.org/erneuerbare-energien/photovoltaik/planung/sonnenstunden # -# !!! PV Berechnungsgrundlagen !!! +# PV Berechnungsgrundlagen # https://www.energie-experten.org/erneuerbare-energien/photovoltaik/planung/ertrag # http://www.ing-büro-junge.de/html/photovoltaik.html # @@ -3346,15 +3356,17 @@ sub __getDWDSolarData { my $paref = shift; my $name = $paref->{name}; my $type = $paref->{type}; - my $date = $paref->{date}; # aktueller Tag "YYYY-MM-DD" + my $date = $paref->{date}; # aktuelles Datum "YYYY-MM-DD" + my $day = $paref->{day}; # aktuelles Tagesdatum 01 .. 31 my $t = $paref->{t} // time; my $lang = $paref->{lang}; - + my $hash = $defs{$name}; - + my $raname = AttrVal ($name, 'setupRadiationAPI', ''); # Radiation Forecast API return if(!$raname || !$defs{$raname}); + my $cafd = AttrVal ($name, 'ctrlAreaFactorUsage', 'fix'); # Art der Flächenfaktor Berechnung my $stime = $date.' 00:00:00'; # Startzeit Soll Übernahmedaten my $sts = timestringToTimestamp ($stime); my @strings = sort keys %{$data{$type}{$name}{strings}}; @@ -3372,13 +3384,12 @@ sub __getDWDSolarData { debugLog ($paref, "apiCall", "DWD API - collect DWD Radiation data with start >$stime<- device: $raname =>"); for my $num (0..47) { - my $dateTime = strftime "%Y-%m-%d %H:%M:00", localtime($sts + (3600 * $num)); # laufendes Datum ' ' Zeit - my $runh = int strftime "%H", localtime($sts + (3600 * $num) + 3600); # Stunde in 24h format (00-23), Rad1h = Absolute Globalstrahlung letzte 1 Stunde my ($fd,$fh) = calcDayHourMove (0, $num); - next if($fh == 24); - my $rad = ReadingsVal ($raname, "fc${fd}_${runh}_Rad1h", '0.00'); # kJ/m2 + my $dateTime = strftime "%Y-%m-%d %H:%M:00", localtime($sts + (3600 * $num)); # abzurufendes Datum ' ' Zeit + my $runh = int strftime "%H", localtime($sts + (3600 * $num) + 3600); # Stunde in 24h format (00-23), Rad1h = Absolute Globalstrahlung letzte 1 Stunde + my $rad = ReadingsVal ($raname, "fc${fd}_${runh}_Rad1h", '0.00'); # kJ/m2 if ($runh == 12 && !$rad) { $ret = "The reading 'fc${fd}_${runh}_Rad1h' does not appear to be present or has an unusual value.\nRun 'set $name plantConfiguration check' for further information."; @@ -3392,20 +3403,57 @@ sub __getDWDSolarData { $data{$type}{$name}{solcastapi}{'?All'}{$dateTime}{Rad1h} = sprintf "%.0f", $rad; + my ($ddate, $dtime) = split ' ', $dateTime; # abzurufendes Datum + Zeit + my $hod = sprintf "%02d", ((split ':', $dtime)[0] + 1); # abzurufende Zeit + my $dday = (split '-', $ddate)[2]; # abzurufender Tag: 01, 02 ... 31 + for my $string (@strings) { # für jeden String der Config .. my $peak = $data{$type}{$name}{strings}{$string}{peak}; # String Peak (kWp) $peak *= 1000; # kWp in Wp umrechnen my $ti = $data{$type}{$name}{strings}{$string}{tilt}; # Neigungswinkel Solarmodule my $az = $data{$type}{$name}{strings}{$string}{azimut}; # Ausrichtung der Solarmodule $az += 180; # Umsetzung -180 - 180 in 0 - 360 - - my $af = ___areaFactorPV ($ti, $az); # Flächenfaktor: https://wiki.fhem.de/wiki/Ertragsprognose_PV - - my $pv = sprintf "%.1f", ($rad * $af * $kJtokWh * $peak * $prdef); # Rad wird in kW/m2 erwartet - debugLog ($paref, "apiProcess", "DWD API - PV estimate String >$string< => $pv Wh, Area factor: $af"); + my ($af, $pv, $sdr, $wcc); - $data{$type}{$name}{solcastapi}{$string}{$dateTime}{pv_estimate50} = $pv; # Startzeit wird verwendet, nicht laufende Stunde + if ($cafd =~ /track/xs) { # Flächenfaktor Sonnenstand geführt + ($af, $sdr, $wcc) = ___areaFactorTrack ( { name => $name, + day => $day, + dday => $dday, + chour => $paref->{chour}, + hod => $hod, + tilt => $ti, + azimut => $az + } + ); + + $wcc = 0 if(!isNumeric($wcc)); + $wcc = cloud2bin($wcc); + + debugLog ($paref, "apiProcess", "DWD API - Value of sunaz/sunalt not stored in pvHistory, workaround using 1.00/0.75") + if(!isNumeric($af)); + + $af = 1.00 if(!isNumeric($af)); + $sdr = 0.75 if(!isNumeric($sdr)); + + if ($cafd eq 'trackShared'|| ($cafd eq 'trackFlex' && $wcc >= 80)) { # Direktstrahlung + Diffusstrahlung + my $dirrad = $rad * $sdr; # Anteil Direktstrahlung an Globalstrahlung + my $difrad = $rad - $dirrad; # Anteil Diffusstrahlung an Globalstrahlung + + $pv = sprintf "%.1f", ((($dirrad * $af) + $difrad) * $kJtokWh * $peak * $prdef); # Rad wird in kW/m2 erwartet + } + else { # Flächenfaktor auf volle Rad1h anwenden + $pv = sprintf "%.1f", ($rad * $af * $kJtokWh * $peak * $prdef); + } + } + else { # Flächenfaktor Fix + $af = ___areaFactorFix ($ti, $az); # Flächenfaktor: https://wiki.fhem.de/wiki/Ertragsprognose_PV + $pv = sprintf "%.1f", ($rad * $af * $kJtokWh * $peak * $prdef); # Rad wird in kW/m2 erwartet + } + + $data{$type}{$name}{solcastapi}{$string}{$dateTime}{pv_estimate50} = $pv; # Startzeit wird verwendet, nicht laufende Stunde + + debugLog ($paref, "apiProcess", "DWD API - PV estimate String >$string< => $dateTime, $pv Wh, Afactor: $af ($cafd)"); } } @@ -3420,27 +3468,104 @@ return; # ersetzt die Tabelle auf Basis http://www.ing-büro-junge.de/html/photovoltaik.html # siehe Wiki: https://wiki.fhem.de/wiki/Ertragsprognose_PV ################################################################################################## -sub ___areaFactorPV { +sub ___areaFactorFix { my $tilt = shift; my $azimut = shift; - + my $pi180 = 0.0174532918889; # Grad in Radiant Umrechnungsfaktor - + my $x = $tilt * sin ($azimut * $pi180); my $y = $tilt * cos ($azimut * $pi180); my $x2 = $x**2; my $x4 = $x2**2; - + my $af = 3.808301895960147E-7 - 8.650170178954599E-11 * $x2 + 5.50016483344622E-15 * $x4; $af = $af * $y + 0.00007319316326291892 - 3.604294916743569E-9 * $x2 - 2.343747951073022E-13 * $x4; $af = $af * $y - 0.00785953342909065 + 1.1197340251684106E-6 * $x2 - 8.99915952119488E-11 * $x4; $af = $af * $y - 0.8432627150525525 + 0.00010392051567819936 * $x2 - 3.979206287671085E-9 * $x4; $af = $af * $y + 99.49627151067648 - 0.006340200119196879 * $x2 + 2.052575360270524E-7 * $x4; $af = sprintf "%.2f", ($af / 100); # Prozenz in Faktor - + return $af; } +########################################################################################################## +# Flächenfaktor Photovoltaik und Direktstrahlungsanteilsfaktor in Abhängigkeit des Sonnenstandes +# +# Die Globalstrahlung (Summe aus diffuser und direkter Sonnenstrahlung) +# ---------------------------------------------------------------------- +# Die Globalstrahlung ist die am Boden von einer horizontalen Ebene empfangene Sonnenstrahlung +# und setzt sich aus der direkten Strahlung (der Schatten werfenden Strahlung) und der +# gestreuten Sonnenstrahlung (diffuse Himmelsstrahlung) aus der Himmelshalbkugel zusammen. +# Bei Sonnenhöhen von mehr als 50° und wolkenlosem Himmel besteht die Globalstrahlung zu ca. 3/4 +# aus direkter Sonnenstrahlung, bei tiefen Sonnenständen (bis etwa 10°) nur noch zu ca. 1/3. +# +# Direktstrahlung = Globalstrahlung * 0.75 (bei > 50° sunalt) +# Direktstrahlung = Globalstrahlung * 0.33 (bei <= 10° sunalt) +# +# Quelle: https://www.dwd.de/DE/leistungen/solarenergie/globalstrahlung.html?nn=16102&lsbId=416798 +# +# Return: +# $daf - direct Area Faktor für den Anteil Direktstrahlung der Globalstrahlung +# $sdr - Share of direct radiation = Faktor Anteil Direktstrahlung an Globalstrahlung (0.33 .. 0.75) +# +########################################################################################################## +sub ___areaFactorTrack { + my $paref = shift; + my $name = $paref->{name}; + my $day = $paref->{day}; # aktueller Tag 01 .. 31 + my $dday = $paref->{dday}; # abzufragender Tag: 01 .. 31 + my $chour = $paref->{chour}; # aktuelle Stunde (00 .. 23) + my $hod = $paref->{hod}; # abzufragende Stunde des Tages 01, 02 ... 24 + my $tilt = $paref->{tilt}; # String Anstellwinkel / Neigung + my $azimut = $paref->{azimut}; # String Ausrichtung / Azimut + + my $hash = $defs{$name}; + + my ($sunalt, $sunaz, $wcc); + + if ($dday eq $day) { + $sunalt = HistoryVal ($hash, $dday, $hod, 'sunalt', undef); # Sonne Höhe (Altitude) + $sunaz = HistoryVal ($hash, $dday, $hod, 'sunaz', undef); # Sonne Azimuth + $wcc = HistoryVal ($hash, $dday, $hod, 'wcc', 0); # Bewölkung + } + else { + my $nhtstr = 'NextHour'.sprintf "%02d", (23 - (int $chour) + $hod); + $sunalt = NexthoursVal ($hash, $nhtstr, 'sunalt', undef); + $sunaz = NexthoursVal ($hash, $nhtstr, 'sunaz', undef); + $wcc = NexthoursVal ($hash, $nhtstr, 'wcc', 0); + } + + return ('-', '-', '-') if(!defined $sunalt || !defined $sunaz); + + my $pi180 = 0.0174532918889; # PI/180 + + #-- Normale der Anlage (Nordrichtung = y-Achse, Ostrichtung = x-Achse) + my $nz = cos ($tilt * $pi180); + my $ny = sin ($tilt * $pi180) * cos ($azimut * $pi180); + my $nx = sin ($tilt * $pi180) * sin ($azimut * $pi180); + + #-- Vektor zur Sonne + my $sz = sin ($sunalt * $pi180); + my $sy = cos ($sunalt * $pi180) * cos ($sunaz * $pi180); + my $sx = cos ($sunalt * $pi180) * sin ($sunaz * $pi180); + + #-- Normale N = ($nx,$ny,$nz) Richtung Sonne S = ($sx,$sy,$sz) + my $daf = $nx * $sx + $ny * $sy + $nz * $sz; + $daf = max ($daf, 0); + $daf += 1 if($daf); + $daf = sprintf "%.2f", $daf; + + ## Schätzung Anteil Direktstrahlung an Globalstrahlung + ######################################################## + my $drif = 0.0105; # Faktor Zunahme Direktstrahlung pro Grad sunalt von 10° bis 50° + my $sdr = $sunalt <= 10 ? 0.33 : + $sunalt > 10 && $sunalt <= 50 ? (($sunalt - 10) * 0.0105) + 0.33 : + 0.75; + +return ($daf, $sdr, $wcc); +} + #################################################################################################### # Abruf Victron VRM API Forecast # @@ -3453,7 +3578,7 @@ sub __getVictronSolarData { my $force = $paref->{force} // 0; my $t = $paref->{t}; my $lang = $paref->{lang}; - + my $hash = $defs{$name}; my $lrt = SolCastAPIVal ($hash, '?All', '?All', 'lastretrieval_timestamp', 0); @@ -3482,7 +3607,7 @@ sub __VictronVRM_ApiRequestLogin { my $name = $paref->{name}; my $debug = $paref->{debug}; my $type = $paref->{type}; - + my $hash = $defs{$name}; my $url = 'https://vrmapi.victronenergy.com/v2/auth/login'; @@ -3814,7 +3939,7 @@ sub __VictronVRM_ApiRequestLogout { my $name = $paref->{name}; my $token = $paref->{token}; my $debug = $paref->{debug}; - + my $hash = $defs{$name}; my $url = 'https://vrmapi.victronenergy.com/v2/auth/logout'; @@ -3893,7 +4018,7 @@ sub __getopenMeteoData { my $t = $paref->{t}; my $lang = $paref->{lang}; my $debug = $paref->{debug}; - + my $hash = $defs{$name}; my $donearq = SolCastAPIVal ($hash, '?All', '?All', 'todayDoneAPIrequests', 0); @@ -3963,9 +4088,9 @@ sub __openMeteoDWD_ApiRequest { my $lang = $paref->{lang}; my $t = $paref->{t} // int time; my $submodel = $paref->{submodel}; # abzufragendes Wettermodell - + my $hash = $defs{$name}; - + if (!$allstrings) { # alle Strings wurden abgerufen my $apiitv = SolCastAPIVal ($hash, '?All', '?All', 'currentAPIinterval', $ometeorepdef); readingsSingleUpdate ($hash, 'nextRadiationAPICall', $hqtxt{after}{$lang}.' '.(timestampToTimestring ($t + $apiitv, $lang))[0], 1); @@ -4711,7 +4836,7 @@ sub __dwdStatCatalog_Request { my $paref = shift; my $name = $paref->{name}; my $debug = $paref->{debug}; - + my $hash = $defs{$name}; my $url = "https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_stationskatalog.cfg?view=nasPublication&nn=16102"; @@ -4861,7 +4986,7 @@ sub __getaiRuleStrings { ## no critic "not used" my $paref = shift; my $name = $paref->{name}; my $lang = $paref->{lang}; - + my $hash = $defs{$name}; return 'the AI usage is not prepared' if(!isPrepared4AI ($hash)); @@ -5259,7 +5384,7 @@ sub _attrconsumer { ## no critic "not used" return if(!$init_done); # Forum: https://forum.fhem.de/index.php/topic,117864.msg1159959.html#msg1159959 my $hash = $defs{$name}; - + if ($cmd eq "set") { my ($err, $codev, $h) = isDeviceValid ( { name => $name, obj => $aVal, method => 'string' } ); return $err if($err); @@ -5356,7 +5481,7 @@ sub _attrconsumer { ## no critic "not used" else { my $day = strftime "%d", localtime(time); # aktueller Tag (range 01 to 31) my ($c) = $aName =~ /consumer([0-9]+)/xs; - + $paref->{c} = $c; delConsumerFromMem ($paref); # Consumerdaten aus History löschen @@ -5380,7 +5505,7 @@ sub _attrcreateConsRecRdgs { ## no critic "not used" my $paref = shift; my $name = $paref->{name}; my $aName = $paref->{aName}; - + my $hash = $defs{$name}; if ($aName eq 'ctrlConsRecommendReadings') { @@ -5456,7 +5581,7 @@ sub _attrMeterDev { ## no critic "not used" my $type = $paref->{type}; return if(!$init_done); - + my $hash = $defs{$name}; if ($paref->{cmd} eq 'set') { @@ -5519,7 +5644,7 @@ sub _attrOtherProducer { ## no critic "not used" my $type = $paref->{type}; return if(!$init_done); - + my $hash = $defs{$name}; my $prn = (split 'Producer', $aName)[1]; @@ -5537,16 +5662,19 @@ sub _attrOtherProducer { ## no critic "not used" delete $paref->{prn}; } + delete $data{$type}{$name}{current}{'iconp'.$prn}; + + InternalTimer (gettimeofday()+0.5, 'FHEM::SolarForecast::centralTask', [$name, 0], 0); InternalTimer (gettimeofday() + 2, 'FHEM::SolarForecast::createAssociatedWith', $hash, 0); InternalTimer (gettimeofday() + 3, 'FHEM::SolarForecast::writeCacheToFile', [$name, 'plantconfig', $plantcfg.$name], 0); # Anlagenkonfiguration File schreiben return; } -################################################################ -# löschen Werte eines Producers aus Speicherhashes ################################################################ -sub __delProducerValues { +# löschen Werte eines Producers aus Speicherhashes +################################################################ +sub __delProducerValues { my $paref = shift; my $name = $paref->{name}; my $prn = $paref->{prn} // return 'The producer number is empty'; # Producernummer (01, 02, 03) @@ -5556,15 +5684,18 @@ sub __delProducerValues { deleteReadingspec ($hash, ".*_PPreal".$prn); readingsDelete ($hash, 'Current_PP'.$prn); delete $data{$type}{$name}{current}{'generationp'.$prn}; - delete $data{$type}{$name}{current}{'etotalp'.$prn}; - + delete $data{$type}{$name}{current}{'etotalp' .$prn}; + delete $data{$type}{$name}{current}{'iconp' .$prn}; + delete $data{$type}{$name}{current}{'namep' .$prn}; + delete $data{$type}{$name}{current}{'aliasp' .$prn}; + for my $hod (keys %{$data{$type}{$name}{circular}}) { delete $data{$type}{$name}{circular}{$hod}{'pprl'.$prn}; } - + for my $dy (sort keys %{$data{$type}{$name}{pvhist}}) { for my $hr (sort keys %{$data{$type}{$name}{pvhist}{$dy}}) { - delete $data{$type}{$name}{pvhist}{$dy}{$hr}{'pprl'.$prn}; + delete $data{$type}{$name}{pvhist}{$dy}{$hr}{'pprl' .$prn}; delete $data{$type}{$name}{pvhist}{$dy}{$hr}{'etotalp'.$prn}; } } @@ -5585,7 +5716,7 @@ sub _attrInverterDev { ## no critic "not used" my $type = $paref->{type}; return if(!$init_done); - + my $hash = $defs{$name}; if ($paref->{cmd} eq 'set') { @@ -5654,7 +5785,7 @@ sub _attrStringPeak { ## no critic "not used" my $aVal = $paref->{aVal}; return if(!$init_done); - + my $hash = $defs{$name}; if ($paref->{cmd} eq 'set') { @@ -5699,11 +5830,11 @@ sub _attrRoofTops { ## no critic "not used" my $paref = shift; my $name = $paref->{name}; my $aVal = $paref->{aVal}; - + return if(!$init_done); - + my $hash = $defs{$name}; - + if ($paref->{cmd} eq 'set') { my ($a,$h) = parseParams ($aVal); @@ -5733,7 +5864,7 @@ sub _attrRoofTops { ## no critic "not used" } } } - + InternalTimer (gettimeofday() + 3, 'FHEM::SolarForecast::writeCacheToFile', [$name, 'plantconfig', $plantcfg.$name], 0); # Anlagenkonfiguration File schreiben return; @@ -5750,7 +5881,7 @@ sub _attrBatteryDev { ## no critic "not used" my $type = $paref->{type}; return if(!$init_done); - + my $hash = $defs{$name}; if ($paref->{cmd} eq 'set') { @@ -5804,7 +5935,7 @@ sub _attrWeatherDev { ## no critic "not used" my $aName = $paref->{aName}; return if(!$init_done); - + my $hash = $defs{$name}; if ($paref->{cmd} eq 'set') { @@ -5841,7 +5972,7 @@ sub _attrRadiationAPI { ## no critic "not used" my $type = $paref->{type}; return if(!$init_done); - + my $hash = $defs{$name}; if ($paref->{cmd} eq 'set') { @@ -6233,34 +6364,34 @@ return; } ################################################################ -# Consumer Daten aus History löschen +# Consumer Daten aus History löschen ################################################################ -sub delConsumerFromMem { +sub delConsumerFromMem { my $paref = shift; my $name = $paref->{name}; my $type = $paref->{type}; my $c = $paref->{c}; - + my $hash = $defs{$name}; my $calias = ConsumerVal ($hash, $c, 'alias', ''); - + for my $d (1..31) { $d = sprintf("%02d", $d); delete $data{$type}{$name}{pvhist}{$d}{99}{"csme${c}"}; delete $data{$type}{$name}{pvhist}{$d}{99}{"cyclescsm${c}"}; delete $data{$type}{$name}{pvhist}{$d}{99}{"hourscsme${c}"}; delete $data{$type}{$name}{pvhist}{$d}{99}{"avgcycmntscsm${c}"}; - - for my $i (1..24) { + + for my $i (1..24) { $i = sprintf("%02d", $i); delete $data{$type}{$name}{pvhist}{$d}{$i}{"csmt${c}"}; delete $data{$type}{$name}{pvhist}{$d}{$i}{"csme${c}"}; - delete $data{$type}{$name}{pvhist}{$d}{$i}{"minutescsm${c}"}; + delete $data{$type}{$name}{pvhist}{$d}{$i}{"minutescsm${c}"}; } } - + delete $data{$type}{$name}{consumers}{$c}; - + Log3 ($name, 3, qq{$name - Consumer "$c - $calias" deleted from memory}); return; @@ -6312,7 +6443,7 @@ sub writeCacheToFile { if ($data) { $error = fileStore ($data, $file); - + if ($error) { $err = qq{ERROR while writing AI data to file "$file": $error}; Log3 ($name, 1, "$name - $err"); @@ -6330,7 +6461,7 @@ sub writeCacheToFile { if ($cachename eq 'dwdcatalog') { if (scalar keys %{$data{$type}{$name}{dwdcatalog}}) { $error = fileStore ($data{$type}{$name}{dwdcatalog}, $file); - + if ($error) { $err = qq{ERROR while writing DWD Station Catalog to file "$file": $error}; Log3 ($name, 1, "$name - $err"); @@ -6349,7 +6480,7 @@ sub writeCacheToFile { if (scalar keys %{$plantcfg}) { $error = fileStore ($plantcfg, $file); - + if ($error) { $err = qq{ERROR writing cache file "$file": $error}; Log3 ($name, 1, "$name - $err"); @@ -6365,10 +6496,10 @@ sub writeCacheToFile { } return if(!$data{$type}{$name}{$cachename}); - + my @arr; push @arr, encode_json ($data{$type}{$name}{$cachename}); - + $error = FileWrite ($file, @arr); if ($error) { @@ -6576,7 +6707,7 @@ sub _addDynAttr { for my $step (1..$weatherDevMax) { if ($step == 1) { - push @deva, ($adwds ? "setupWeatherDev1:OpenMeteoDWD-API,OpenMeteoDWDEnsemble-API,OpenMeteoWorld-API,$adwds" : + push @deva, ($adwds ? "setupWeatherDev1:OpenMeteoDWD-API,OpenMeteoDWDEnsemble-API,OpenMeteoWorld-API,$adwds" : "setupWeatherDev1:OpenMeteoDWD-API,OpenMeteoDWDEnsemble-API,OpenMeteoWorld-API"); next; } @@ -6622,24 +6753,8 @@ sub centralTask { return if(!$init_done); ### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !! - ########################################################################################################################## - my $dir = ReadingsVal ($name, 'moduleAzimuth', ''); # 16.06.2024 - if ($dir) { - readingsSingleUpdate ($hash, 'setupStringAzimuth', $dir, 0); - readingsDelete ($hash, 'moduleAzimuth'); - } - - my $dec = ReadingsVal ($name, 'moduleDeclination', ''); # 16.06.2024 - if ($dec) { - readingsSingleUpdate ($hash, 'setupStringDeclination', $dec, 0); - readingsDelete ($hash, 'moduleDeclination'); - } - - my $val6 = ReadingsVal ($name, 'moduleRoofTops', ''); # 16.06.2024 - if ($val6) { - CommandAttr (undef, "$name setupRoofTops $val6"); - readingsDelete ($hash, 'moduleRoofTops'); - } + ########################################################################################################################## + ########################################################################################################################## setModel ($hash); # Model setzen @@ -6666,7 +6781,7 @@ sub centralTask { my $date = strftime "%Y-%m-%d", localtime($t); # aktuelles Datum my $chour = strftime "%H", localtime($t); # aktuelle Stunde in 24h format (00-23) my $minute = strftime "%M", localtime($t); # aktuelle Minute (00-59) - my $day = strftime "%d", localtime($t); # aktueller Tag (range 01 to 31) + my $day = strftime "%d", localtime($t); # aktueller Tag (range 01 .. 31) my $dayname = strftime "%a", localtime($t); # aktueller Wochentagsname my $debug = getDebug ($hash); # Debug Module @@ -6982,7 +7097,7 @@ sub _collectAllRegConsumers { my $paref = shift; my $name = $paref->{name}; my $type = $paref->{type}; - + my $hash = $defs{$name}; return if(CurrentVal ($hash, 'consumerCollected', 0)); # Abbruch wenn Consumer bereits gesammelt @@ -7064,8 +7179,6 @@ sub _collectAllRegConsumers { ($interruptable,$hyst) = $interruptable =~ /(.*):(.*)$/xs if($interruptable ne '1'); } - delete $data{$type}{$name}{consumers}{$c}{sunriseshift}; - delete $data{$type}{$name}{consumers}{$c}{sunsetshift}; my ($riseshift, $setshift); if (exists $hc->{mintime}) { # Check Regex @@ -7083,6 +7196,10 @@ sub _collectAllRegConsumers { $clt = $hc->{locktime}; } + delete $data{$type}{$name}{consumers}{$c}{sunriseshift}; + delete $data{$type}{$name}{consumers}{$c}{sunsetshift}; + delete $data{$type}{$name}{consumers}{$c}{icon}; + my $rauto = $hc->{auto} // q{}; my $ctype = $hc->{type} // $defctype; @@ -7093,7 +7210,6 @@ sub _collectAllRegConsumers { $data{$type}{$name}{consumers}{$c}{avgenergy} = q{}; # Initialwert Energieverbrauch (evtl. Überschreiben in manageConsumerData) $data{$type}{$name}{consumers}{$c}{mintime} = $hc->{mintime} // $hef{$ctype}{mt}; # Initialwert min. Einplanungsdauer (evtl. Überschreiben in manageConsumerData) $data{$type}{$name}{consumers}{$c}{mode} = $hc->{mode} // $defcmode; # Planungsmode des Verbrauchers - $data{$type}{$name}{consumers}{$c}{icon} = $hc->{icon} // q{}; # Icon für den Verbraucher $data{$type}{$name}{consumers}{$c}{oncom} = $hc->{on} // q{}; # Setter Einschaltkommando $data{$type}{$name}{consumers}{$c}{offcom} = $hc->{off} // q{}; # Setter Ausschaltkommando $data{$type}{$name}{consumers}{$c}{dswitch} = $dswitch; # Switchdevice zur Kommandoausführung @@ -7124,8 +7240,9 @@ sub _collectAllRegConsumers { $data{$type}{$name}{consumers}{$c}{spignorecondregex} = $spignorecondregex // q{}; # Regex der Ignore Bedingung $data{$type}{$name}{consumers}{$c}{interruptable} = $interruptable; # Ein-Zustand des Verbrauchers ist unterbrechbar $data{$type}{$name}{consumers}{$c}{hysteresis} = $hyst // $defhyst; # Hysterese - $data{$type}{$name}{consumers}{$c}{sunriseshift} = $riseshift if(defined $riseshift); # Verschiebung (Sekunden) Sonnenaufgang bei SunPath Verwendung - $data{$type}{$name}{consumers}{$c}{sunsetshift} = $setshift if(defined $setshift); # Verschiebung (Sekunden) Sonnenuntergang bei SunPath Verwendung + $data{$type}{$name}{consumers}{$c}{sunriseshift} = $riseshift if(defined $riseshift); # Verschiebung (Sekunden) Sonnenaufgang bei SunPath Verwendung + $data{$type}{$name}{consumers}{$c}{sunsetshift} = $setshift if(defined $setshift); # Verschiebung (Sekunden) Sonnenuntergang bei SunPath Verwendung + $data{$type}{$name}{consumers}{$c}{icon} = $hc->{icon} if(defined $hc->{icon}); # Icon für den Verbraucher } $data{$type}{$name}{current}{consumerCollected} = 1; @@ -7208,8 +7325,8 @@ sub _specialActivities { if (scalar(@widgetreadings)) { # vermeide Schleife falls FHEMWEB geöfffnet my @acopy = @widgetreadings; - @widgetreadings = (); - + @widgetreadings = (); + for my $wdr (@acopy) { # Array der Hilfsreadings (Attributspeicher) löschen readingsDelete ($hash, $wdr); } @@ -7350,12 +7467,12 @@ sub __deletePvCorffReadings { if (ReadingsVal ($name, 'pvCorrectionFactor_Auto', 'off') =~ /on/xs) { my $pcf = ReadingsVal ($name, "pvCorrectionFactor_${n}", ''); ($pcf) = split " / ", $pcf if($pcf =~ /\s\/\s/xs); - - if ($pcf !~ /manual/xs) { # manuell gesetzte pcf-Readings nicht löschen + + if ($pcf !~ /manual/xs) { # manuell gesetzte pcf-Readings nicht löschen deleteReadingspec ($hash, "pvCorrectionFactor_${n}.*"); } else { - readingsSingleUpdate ($hash, "pvCorrectionFactor_${n}", $pcf, 0); + readingsSingleUpdate ($hash, "pvCorrectionFactor_${n}", $pcf, 0); deleteReadingspec ($hash, "pvCorrectionFactor_${n}_autocalc"); } } @@ -7394,7 +7511,7 @@ sub __delObsoleteAPIData { my $type = $paref->{type}; my $date = $paref->{date}; # aktuelles Datum my $hash = $defs{$name}; - + if (!keys %{$data{$type}{$name}{solcastapi}}) { return; } @@ -7478,25 +7595,25 @@ sub _transferWeatherValues { my $wid = $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{ww}; # signifikantes Wetter = Wetter ID my $wwd = $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{wwd}; # Wetter Beschreibung - my $neff = $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{neff}; # Effektive Wolkendecke + my $wcc = $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{neff}; # Effektive Wolkendecke my $rr1c = $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{rr1c}; # Gesamtniederschlag (1-stündig) letzte 1 Stunde my $temp = $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{ttt}; # Außentemperatur my $don = $data{$type}{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{don}; # Tag/Nacht-Grenze - my $nhtstr = "NextHour".sprintf "%02d", $num; - $data{$type}{$name}{nexthours}{$nhtstr}{weatherid} = $wid; - $data{$type}{$name}{nexthours}{$nhtstr}{cloudcover} = $neff; - $data{$type}{$name}{nexthours}{$nhtstr}{totalrain} = $rr1c; - $data{$type}{$name}{nexthours}{$nhtstr}{rainrange} = $rr1c; - $data{$type}{$name}{nexthours}{$nhtstr}{temp} = $temp; - $data{$type}{$name}{nexthours}{$nhtstr}{DoN} = $don; + my $nhtstr = "NextHour".sprintf "%02d", $num; + $data{$type}{$name}{nexthours}{$nhtstr}{weatherid} = $wid; + $data{$type}{$name}{nexthours}{$nhtstr}{wcc} = $wcc; + $data{$type}{$name}{nexthours}{$nhtstr}{rr1c} = $rr1c; + $data{$type}{$name}{nexthours}{$nhtstr}{rainrange} = $rr1c; + $data{$type}{$name}{nexthours}{$nhtstr}{temp} = $temp; + $data{$type}{$name}{nexthours}{$nhtstr}{DoN} = $don; my $fh1 = $fh + 1; # = hod if ($num < 23 && $fh < 24) { # Ringspeicher Weather Forum: https://forum.fhem.de/index.php/topic,117864.msg1139251.html#msg1139251 $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{weatherid} = $wid; $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{weathertxt} = $wwd; - $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{wcc} = $neff; + $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{wcc} = $wcc; $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{rr1c} = $rr1c; $data{$type}{$name}{circular}{sprintf("%02d",$fh1)}{temp} = $temp; @@ -7506,11 +7623,11 @@ sub _transferWeatherValues { } if ($fd == 0 && $fh1) { # Weather in pvHistory speichern - writeToHistory ( { paref => $paref, key => 'weatherid', val => $wid, hour => $fh1 } ); - writeToHistory ( { paref => $paref, key => 'weathercloudcover', val => $neff // 0, hour => $fh1 } ); - writeToHistory ( { paref => $paref, key => 'totalrain', val => $rr1c, hour => $fh1 } ); - writeToHistory ( { paref => $paref, key => 'temperature', val => $temp, hour => $fh1 } ); - writeToHistory ( { paref => $paref, key => 'DoN', val => $don, hour => $fh1 } ); + writeToHistory ( { paref => $paref, key => 'weatherid', val => $wid, hour => $fh1 } ); + writeToHistory ( { paref => $paref, key => 'weathercloudcover', val => $wcc // 0, hour => $fh1 } ); + writeToHistory ( { paref => $paref, key => 'rr1c', val => $rr1c, hour => $fh1 } ); + writeToHistory ( { paref => $paref, key => 'temperature', val => $temp, hour => $fh1 } ); + writeToHistory ( { paref => $paref, key => 'DoN', val => $don, hour => $fh1 } ); } } @@ -7776,18 +7893,18 @@ sub _transferAPIRadiationValues { for my $num (0..47) { my ($fd,$fh) = calcDayHourMove ($chour, $num); - if ($fd > 1) { # überhängende Werte löschen + if ($fd > 1) { # überhängende Werte löschen delete $data{$type}{$name}{nexthours}{"NextHour".sprintf "%02d", $num}; next; } - my $fh1 = $fh + 1; - my $wantts = (timestringToTimestamp ($date.' '.$chour.':00:00')) + ($num * 3600); - my $wantdt = (timestampToTimestring ($wantts, $lang))[1]; - my $nhtstr = 'NextHour'.sprintf "%02d", $num; - my ($wtday, $hod) = $wantdt =~ /(\d{2})\s(\d{2}):/xs; - $hod = sprintf "%02d", int $hod + 1; # Stunde des Tages - my $rad1h = SolCastAPIVal ($hash, '?All', $wantdt, 'Rad1h', undef); + my $fh1 = $fh + 1; + my $wantts = (timestringToTimestamp ($date.' '.$chour.':00:00')) + ($num * 3600); + my $wantdt = (timestampToTimestring ($wantts, $lang))[1]; + my $nhtstr = 'NextHour'.sprintf "%02d", $num; + my ($wtday, $wthour) = $wantdt =~ /(\d{2})\s(\d{2}):/xs; + my $hod = sprintf "%02d", int $wthour + 1; # Stunde des Tages + my $rad1h = SolCastAPIVal ($hash, '?All', $wantdt, 'Rad1h', undef); $paref->{wantdt} = $wantdt; $paref->{wantts} = $wantts; @@ -7848,10 +7965,10 @@ sub _transferAPIRadiationValues { if ($msg eq 'accurate' || $msg eq 'spreaded') { my $airn = CircularVal ($hash, 99, 'aiRulesNumber', 0); my $aivar = 100; - $aivar = sprintf "%.0f", (100 * $pvaifc / $est) if($est); # Übereinstimmungsgrad KI Forecast zu API Forecast in % + $aivar = sprintf "%.0f", (100 * $pvaifc / $est) if($est); # Übereinstimmungsgrad KI Forecast zu API Forecast in % if ($msg eq 'accurate') { # KI liefert 'accurate' Treffer -> verwenden - if ($airn >= $aiAccTRNLim || ($aivar >= $aiAccLowLim && $aivar <= $aiAccUpLim)) { + if ($airn >= $aiAccTRNMin || ($aivar >= $aiAccLowLim && $aivar <= $aiAccUpLim)) { $data{$type}{$name}{nexthours}{$nhtstr}{aihit} = 1; $pvfc = $pvaifc; $useai = 1; @@ -7860,7 +7977,7 @@ sub _transferAPIRadiationValues { } } elsif ($msg eq 'spreaded') { # Abweichung AI von Standardvorhersage begrenzen - if ($airn >= $aiSpreadTRNLim || ($aivar >= $aiSpreadLowLim && $aivar <= $aiSpreadUpLim)) { + if ($airn >= $aiSpreadTRNMin || ($aivar >= $aiSpreadLowLim && $aivar <= $aiSpreadUpLim)) { $data{$type}{$name}{nexthours}{$nhtstr}{aihit} = 1; $pvfc = $pvaifc; $useai = 1; @@ -7973,14 +8090,14 @@ sub __calcPVestimates { my $hash = $defs{$name}; my $reld = $fd == 0 ? "today" : $fd == 1 ? "tomorrow" : "unknown"; - my $totalrain = NexthoursVal ($hash, "NextHour".sprintf ("%02d",$num), "totalrain", 0); # Gesamtniederschlag während der letzten Stunde kg/m2 - my $cloudcover = NexthoursVal ($hash, "NextHour".sprintf ("%02d",$num), "cloudcover", 0); # effektive Wolkendecke nächste Stunde X + my $rr1c = NexthoursVal ($hash, "NextHour".sprintf ("%02d",$num), "rr1c", 0); # Gesamtniederschlag während der letzten Stunde kg/m2 + my $wcc = NexthoursVal ($hash, "NextHour".sprintf ("%02d",$num), "wcc", 0); # effektive Wolkendecke nächste Stunde X my $temp = NexthoursVal ($hash, "NextHour".sprintf ("%02d",$num), "temp", $tempbasedef); # vorhergesagte Temperatur Stunde X my ($acu, $aln) = isAutoCorrUsed ($name); - $paref->{cloudcover} = $cloudcover; - my ($hc, $hq) = ___readCandQ ($paref); # liest den anzuwendenden Korrekturfaktor - delete $paref->{cloudcover}; + $paref->{wcc} = $wcc; + my ($hc, $hq) = ___readCandQ ($paref); # liest den anzuwendenden Korrekturfaktor + delete $paref->{wcc}; my ($lh,$sq,$peakloss, $modtemp); my $pvsum = 0; @@ -7990,15 +8107,15 @@ sub __calcPVestimates { my $peak = StringVal ($hash, $string, 'peak', 0); # String Peak (kWp) if ($acu =~ /on_complex/xs) { - $paref->{peak} = $peak; - $paref->{cloudcover} = $cloudcover; - $paref->{temp} = $temp; + $paref->{peak} = $peak; + $paref->{wcc} = $wcc; + $paref->{temp} = $temp; ($peakloss, $modtemp) = ___calcPeaklossByTemp ($paref); # Reduktion Peakleistung durch Temperaturkoeffizienten der Module (vorzeichengehaftet) $peak += $peakloss; delete $paref->{peak}; - delete $paref->{cloudcover}; + delete $paref->{wcc}; delete $paref->{temp}; } @@ -8053,8 +8170,8 @@ sub __calcPVestimates { $lh = { # Log-Hash zur Ausgabe "Starttime" => $wantdt, "Forecasted temperature" => $temp." °C", - "Cloudcover" => $cloudcover, - "Total Rain last hour" => $totalrain." kg/m2", + "Cloudcover" => $wcc, + "Total Rain last hour" => $rr1c." kg/m2", "PV Correction mode" => ($acu ? $acu : 'no'), "PV generation forecast" => $pvsum." Wh ".$logao, }; @@ -8086,7 +8203,7 @@ sub ___readCandQ { my $num = $paref->{num}; my $fh1 = $paref->{fh1}; my $fd = $paref->{fd}; - my $cc = $paref->{cloudcover}; + my $wcc = $paref->{wcc}; my $sabin = $paref->{sabin}; my $hash = $defs{$name}; @@ -8097,13 +8214,13 @@ sub ___readCandQ { my $hq = '-'; # keine Qualität definiert my $crang = 'simple'; my $hc; - + delete $data{$type}{$name}{nexthours}{"NextHour".sprintf("%02d",$num)}{cloudrange}; if ($acu =~ /on_complex/xs) { # Autokorrektur complex soll genutzt werden - $crang = cloud2bin ($cc); # Range errechnen - ($hc, $hq) = CircularSunCloudkorrVal ($hash, sprintf("%02d",$fh1), $sabin, $crang, undef); # Korrekturfaktor/Qualität der Stunde des Tages (complex) - + $crang = cloud2bin ($wcc); # Range errechnen + ($hc, $hq) = CircularSunCloudkorrVal ($hash, sprintf("%02d",$fh1), $sabin, $crang, undef); # Korrekturfaktor/Qualität der Stunde des Tages (complex) + $data{$type}{$name}{nexthours}{"NextHour".sprintf("%02d",$num)}{cloudrange} = $crang; } elsif ($acu =~ /on_simple/xs) { @@ -8111,20 +8228,22 @@ sub ___readCandQ { } else { # keine Autokorrektur ($hc, $hq) = CircularSunCloudkorrVal ($hash, sprintf("%02d",$fh1), $sabin, 'simple', undef); # Korrekturfaktor/Qualität der Stunde des Tages (simple) + $hc = 1; } $hq //= '-'; # keine Qualität definiert + $hq = sprintf "%.2f", $hq if(isNumeric ($hq)); $hc //= $hcraw; # Korrekturfaktor Voreinstellung $hc = 1 if(1 * $hc == 0); # 0.0-Werte ignorieren (Schleifengefahr) $hc = sprintf "%.2f", $hc; - + if ($cpcf =~ /manual\sfix/xs) { # Voreinstellung pcf-Reading verwenden wenn 'manual fix' $hc = $hcraw; - debugLog ($paref, 'pvCorrectionRead', "use 'manual fix' - fd: $fd, hod: ".sprintf("%02d",$fh1).", corrf: $hc, quality: $hq"); + debugLog ($paref, 'pvCorrectionRead', "use 'manual fix' - fd: $fd, hod: ".sprintf("%02d",$fh1).", corrf: $hc, quality: $hq"); } else { my $flex = $cpcf =~ /manual\sflex/xs ? "use 'manual flex'" : 'read parameters'; - debugLog ($paref, 'pvCorrectionRead', "$flex - fd: $fd, hod: ".sprintf("%02d",$fh1).", Sun Altitude Bin: $sabin, Cloud range: $crang, corrf: $hc, quality: $hq"); + debugLog ($paref, 'pvCorrectionRead', "$flex - fd: $fd, hod: ".sprintf("%02d",$fh1).", Sun Altitude Bin: $sabin, Cloud range: $crang, corrf: $hc, quality: $hq"); } $data{$type}{$name}{nexthours}{"NextHour".sprintf("%02d",$num)}{pvcorrf} = $hc."/".$hq; @@ -8156,13 +8275,13 @@ return ($hc, $hq); # ################################################################### sub ___calcPeaklossByTemp { - my $paref = shift; - my $name = $paref->{name}; - my $peak = $paref->{peak} // return (0,0); - my $cloudcover = $paref->{cloudcover} // return (0,0); # vorhergesagte Wolkendecke Stunde X - my $temp = $paref->{temp} // return (0,0); # vorhergesagte Temperatur Stunde X - - my $modtemp = $temp + ($tempmodinc * (1 - ($cloudcover/100))); # kalkulierte Modultemperatur + my $paref = shift; + my $name = $paref->{name}; + my $peak = $paref->{peak} // return (0,0); + my $wcc = $paref->{wcc} // return (0,0); # vorhergesagte Wolkendecke Stunde X + my $temp = $paref->{temp} // return (0,0); # vorhergesagte Temperatur Stunde X + + my $modtemp = $temp + ($tempmodinc * (1 - ($wcc/100))); # kalkulierte Modultemperatur my $peakloss = sprintf "%.2f", $tempcoeffdef * ($modtemp - $tempbasedef) * $peak / 100; return ($peakloss, $modtemp); @@ -8177,7 +8296,7 @@ sub ___70percentRule { my $pvsum = $paref->{pvsum}; my $peaksum = $paref->{peaksum}; my $num = $paref->{num}; # Nexthour - + my $hash = $defs{$name}; my $logao = qq{}; my $confc = NexthoursVal ($hash, "NextHour".sprintf("%02d",$num), "confc", 0); @@ -8235,7 +8354,7 @@ sub _transferInverterValues { my $t = $paref->{t}; # aktuelle Unix-Zeit my $chour = $paref->{chour}; my $day = $paref->{day}; - + my $hash = $defs{$name}; my ($err, $indev, $h) = isDeviceValid ( { name => $name, obj => 'setupInverterDev', method => 'attr' } ); return if($err); @@ -8324,9 +8443,9 @@ sub _transferProducerValues { my $t = $paref->{t}; # aktuelle Unix-Zeit my $chour = $paref->{chour}; my $day = $paref->{day}; - + my $hash = $defs{$name}; - + for my $prn (1..$maxproducer) { $prn = sprintf "%02d", $prn; my ($err, $prdev, $h) = isDeviceValid ( { name => $name, obj => 'setupOtherProducer'.$prn, method => 'attr' } ); @@ -8339,12 +8458,14 @@ sub _transferProducerValues { next if(!$pcread || !$edread); + $data{$type}{$name}{current}{'iconp'.$prn} = $h->{icon} if($h->{icon}); # Icon des Producers + my $pu = $pcunit =~ /^kW$/xi ? 1000 : 1; my $p = ReadingsNum ($prdev, $pcread, 0) * $pu; # aktuelle Erzeugung (W) - $p = $p < 0 ? 0 : sprintf("%.0f", $p); + $p = $p < 0 ? 0 : $p; - storeReading ('Current_PP'.$prn, $p.' W'); - $data{$type}{$name}{current}{'generationp'.$prn} = $p; + storeReading ('Current_PP'.$prn, sprintf("%.1f", $p).' W'); + $data{$type}{$name}{current}{'generationp'.$prn} = $p; my $etu = $etunit =~ /^kWh$/xi ? 1000 : 1; my $etotal = ReadingsNum ($prdev, $edread, 0) * $etu; # Erzeugung total (Wh) @@ -8368,13 +8489,15 @@ sub _transferProducerValues { } $data{$type}{$name}{current}{'etotalp'.$prn} = $etotal; # aktuellen etotal des WR speichern + $data{$type}{$name}{current}{'namep' .$prn} = $prdev; # Name des Producerdevices + $data{$type}{$name}{current}{'aliasp' .$prn} = AttrVal ($prdev, 'alias', $prdev); # Alias Producer if ($ethishour < 0) { $ethishour = 0; my $vl = 3; my $pre = '- WARNING -'; - if ($paref->{debug} =~ /collectData/xs) { + if ($paref->{debug} =~ /collectData/xs) { $vl = 1; $pre = 'DEBUG> - WARNING -'; } @@ -8384,11 +8507,11 @@ sub _transferProducerValues { } storeReading ('Today_Hour'.sprintf("%02d",$nhour).'_PPreal'.$prn, $ethishour.' Wh'.$warn); - $data{$type}{$name}{circular}{sprintf("%02d",$nhour)}{'pprl'.$prn} = $ethishour; # Ringspeicher P real + $data{$type}{$name}{circular}{sprintf("%02d",$nhour)}{'pprl'.$prn} = $ethishour; # Ringspeicher P real - writeToHistory ( { paref => $paref, key => 'pprl'.$prn, val => $ethishour, hour => $nhour } ); + writeToHistory ( { paref => $paref, key => 'pprl'.$prn, val => $ethishour, hour => $nhour } ); } - + return; } @@ -8612,7 +8735,7 @@ sub _transferBatteryValues { my $name = $paref->{name}; my $chour = $paref->{chour}; my $day = $paref->{day}; - + my $hash = $defs{$name}; my ($err, $badev, $h) = isDeviceValid ( { name => $name, obj => 'setupBatteryDev', method => 'attr' } ); return if($err); @@ -8777,7 +8900,7 @@ sub _batSocTarget { my $t = $paref->{t}; # aktuelle Zeit return if(!isBatteryUsed ($name)); - + my $hash = $defs{$name}; my $oldd2care = CircularVal ($hash, 99, 'days2care', 0); my $ltsmsr = CircularVal ($hash, 99, 'lastTsMaxSocRchd', undef); @@ -8952,7 +9075,7 @@ sub _createSummaries { my $chour = $paref->{chour}; # aktuelle Stunde my $minute = $paref->{minute}; # aktuelle Minute - my $hash = $defs{$name}; + my $hash = $defs{$name}; $minute = (int $minute) + 1; # Minute Range umsetzen auf 1 bis 60 ## Initialisierung @@ -9046,12 +9169,12 @@ sub _createSummaries { my $batout = CurrentVal ($hash, "powerbatout", 0); # aktuelle Batterieentladung my $othprod = 0; # Summe Otherproducer - + for my $prn (1..$maxproducer) { # V1.32.0 : Erzeugung sonstiger Producer (01..03) hinzufügen $prn = sprintf "%02d", $prn; $othprod += CurrentVal ($hash, 'generationp'.$prn, 0); } - + my $consumption = int ($pvgen + $othprod - $gfeedin + $gcon - $batin + $batout); my $selfconsumption = int ($pvgen - $gfeedin - $batin); $selfconsumption = $selfconsumption < 0 ? 0 : $selfconsumption; @@ -9308,7 +9431,7 @@ sub __calcEnergyPieces { my $name = $paref->{name}; my $type = $paref->{type}; my $c = $paref->{consumer}; - + my $hash = $defs{$name}; my $etot = HistoryVal ($hash, $paref->{day}, sprintf("%02d",$paref->{nhour}), "csmt${c}", 0); @@ -9394,9 +9517,9 @@ sub ___csmSpecificEpieces { my $c = $paref->{consumer}; my $etot = $paref->{etot}; my $t = $paref->{t}; - + my $hash = $defs{$name}; - + if (ConsumerVal ($hash, $c, "onoff", "off") eq "on") { # Status "Aus" verzögern um Pausen im Waschprogramm zu überbrücken $data{$type}{$name}{consumers}{$c}{lastOnTime} = $t; } @@ -9499,7 +9622,7 @@ sub __planInitialSwitchTime { my $name = $paref->{name}; my $c = $paref->{consumer}; my $debug = $paref->{debug}; - + my $hash = $defs{$name}; my $dnp = ___noPlanRelease ($paref); @@ -9578,7 +9701,7 @@ sub __reviewSwitchTime { my $paref = shift; my $name = $paref->{name}; my $c = $paref->{consumer}; - + my $hash = $defs{$name}; my $pstate = ConsumerVal ($hash, $c, 'planstate', ''); my $plswon = ConsumerVal ($hash, $c, 'planswitchon', 0); # bisher geplante Switch on Zeit @@ -9807,7 +9930,7 @@ sub ___saveEhodpieces { my $c = $paref->{consumer}; my $startts = $paref->{startts}; # Unix Timestamp für geplanten Switch on my $stopts = $paref->{stopts}; # Unix Timestamp für geplanten Switch off - + my $hash = $defs{$name}; my $p = 1; delete $data{$type}{$name}{consumers}{$c}{ehodpieces}; @@ -10064,7 +10187,7 @@ sub __setTimeframeState { my $type = $paref->{type}; my $c = $paref->{consumer}; my $t = $paref->{t}; # aktueller Unixtimestamp - + my $hash = $defs{$name}; my $startts = ConsumerVal ($hash, $c, "planswitchon", undef); # geplante Unix Startzeit my $stopts = ConsumerVal ($hash, $c, "planswitchoff", undef); # geplante Unix Stopzeit @@ -10088,7 +10211,7 @@ sub __setConsRcmdState { my $type = $paref->{type}; my $c = $paref->{consumer}; my $debug = $paref->{debug}; - + my $hash = $defs{$name}; my $surplus = CurrentVal ($hash, 'surplus', 0); # aktueller Energieüberschuß my $nompower = ConsumerVal ($hash, $c, 'power', 0); # Consumer nominale Leistungsaufnahme (W) @@ -10263,7 +10386,7 @@ sub ___switchConsumerOff { my $t = $paref->{t}; # aktueller Unixtimestamp my $state = $paref->{state}; my $debug = $paref->{debug}; - + my $hash = $defs{$name}; my $pstate = ConsumerVal ($hash, $c, "planstate", ""); @@ -10344,7 +10467,7 @@ sub ___setConsumerSwitchingState { my $c = $paref->{consumer}; my $t = $paref->{t}; my $state = $paref->{state}; - + my $hash = $defs{$name}; my $simpCstat = simplifyCstate (ConsumerVal ($hash, $c, 'planstate', '')); my $calias = ConsumerVal ($hash, $c, 'alias', ''); # Consumer Device Alias @@ -10464,7 +10587,7 @@ sub __getCyclesAndRuntime { my $pcurr = $paref->{pcurr}; my $c = $paref->{consumer}; my $debug = $paref->{debug}; - + my $hash = $defs{$name}; ### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !! @@ -10587,7 +10710,7 @@ sub __setPhysLogSwState { my $c = $paref->{consumer}; my $pcurr = $paref->{pcurr}; my $debug = $paref->{debug}; - + my $hash = $defs{$name}; my $cpo = isConsumerPhysOn ($hash, $c) ? 'on' : 'off'; my $clo = isConsumerLogOn ($hash, $c, $pcurr) ? 'on' : 'off'; @@ -10676,7 +10799,7 @@ sub _estConsumptionForecast { my ($am, $hm) = parseParams ($medev); my $type = $paref->{type}; my $acref = $data{$type}{$name}{consumers}; - + my ($exconfc, $csme); ## Verbrauchsvorhersage für den nächsten Tag @@ -10696,24 +10819,24 @@ sub _estConsumptionForecast { } my $dcon = HistoryVal ($hash, $n, 99, 'con', 0); - + if(!$dcon) { debugLog ($paref, 'consumption|consumption_long', "Day >$n< has no registered consumption, ignore it."); next; } - + for my $c (sort{$a<=>$b} keys %{$acref}) { # historischer Verbrauch aller registrierten Verbraucher aufaddieren $exconfc = ConsumerVal ($hash, $c, 'exconfc', 0); # 1 -> Consumer Verbrauch von Erstelleung der Verbrauchsprognose ausschließen $csme = HistoryVal ($hash, $n, 99, "csme${c}", 0); - + if ($exconfc) { $dcon -= $csme; debugLog ($paref, 'consumption|consumption_long', "Consumer '$c' values excluded from forecast calc by 'exconfc' - day: $n, csme: $csme"); } } - + debugLog ($paref, 'consumption|consumption_long', "History Consumption day >$n< considering possible exclusions: $dcon"); - + $totcon += $dcon; $dnum++; } @@ -10736,7 +10859,7 @@ sub _estConsumptionForecast { for my $k (sort keys %{$data{$type}{$name}{nexthours}}) { my $nhtime = NexthoursVal ($hash, $k, "starttime", undef); # Startzeit next if(!$nhtime); - + $conhfc = { "01" => 0, "02" => 0, "03" => 0, "04" => 0, "05" => 0, "06" => 0, "07" => 0, "08" => 0, "09" => 0, "10" => 0, "11" => 0, "12" => 0, "13" => 0, "14" => 0, "15" => 0, "16" => 0, "17" => 0, "18" => 0, "19" => 0, "20" => 0, "21" => 0, "22" => 0, "23" => 0, "24" => 0, @@ -10763,9 +10886,9 @@ sub _estConsumptionForecast { my $hcon = HistoryVal ($hash, $m, $nhhr, 'con', 0); # historische Verbrauchswerte next if(!$hcon); - + debugLog ($paref, 'consumption_long', " historical Consumption added for $nhday -> date: $m, hod: $nhhr -> $hcon Wh"); - + if ($hcon < 0) { # V1.32.0 my $vl = 3; my $pre = '- WARNING -'; @@ -10776,13 +10899,13 @@ sub _estConsumptionForecast { } Log3 ($name, $vl, "$name $pre The stored Energy consumption of day/hour $m/$nhhr is negative. This appears to be an error. The incorrect value can be deleted with 'set $name reset consumption $m $nhhr'."); - } + } for my $c (sort{$a<=>$b} keys %{$acref}) { # historischen Verbrauch aller registrierten Verbraucher aufaddieren $exconfc = ConsumerVal ($hash, $c, 'exconfc', 0); # 1 -> Consumer Verbrauch von Erstelleung der Verbrauchsprognose ausschließen $csme = HistoryVal ($hash, $m, $nhhr, "csme${c}", 0); $consumerco += $csme; - + if ($exconfc) { debugLog ($paref, 'consumption_long', "Consumer '$c' values excluded from forecast calc by 'exconfc' - day: $m, hour: $nhhr, csme: $csme"); $consumerco -= $csme; # V1.32.0 @@ -10905,11 +11028,11 @@ sub _calcReadingsTomorrowPVFc { my $paref = shift; my $name = $paref->{name}; my $type = $paref->{type}; - + my $hash = $defs{$name}; my $h = $data{$type}{$name}{nexthours}; my $hods = AttrVal($name, 'ctrlNextDayForecastReadings', ''); - + return if(!keys %{$h} || !$hods); for my $idx (sort keys %{$h}) { @@ -10983,7 +11106,7 @@ sub calcValueImproves { my $hash = $defs{$name}; my $idts = CircularVal ($hash, 99, "attrInvChangedTs", ''); # Definitionstimestamp des Attr setupInverterDev - + return if(!$idts); my ($acu, $aln) = isAutoCorrUsed ($name); @@ -11013,9 +11136,10 @@ sub calcValueImproves { for my $h (1..23) { next if(!$chour || $h > $chour); - - $paref->{cpcf} = ReadingsVal ($name, 'pvCorrectionFactor_'.sprintf("%02d",$h), ''); # aktuelles pvCorf-Reading - $paref->{h} = $h; + + $paref->{cpcf} = ReadingsVal ($name, 'pvCorrectionFactor_'.sprintf("%02d",$h), ''); # aktuelles pvCorf-Reading + $paref->{aihit} = CircularVal ($hash, sprintf("%02d",$h), 'aihit', 0); # AI verwendet? + $paref->{h} = $h; _calcCaQcomplex ($paref); # Korrekturberechnung mit Bewölkung duchführen/speichern _calcCaQsimple ($paref); # einfache Korrekturberechnung duchführen/speichern @@ -11023,6 +11147,7 @@ sub calcValueImproves { delete $paref->{h}; delete $paref->{cpcf}; + delete $paref->{aihit}; } delete $paref->{aln}; @@ -11043,6 +11168,7 @@ sub _calcCaQcomplex { my $aln = $paref->{aln}; # Autolearning my $h = $paref->{h}; my $day = $paref->{day}; # aktueller Tag + my $aihit = $paref->{aihit}; my $hash = $defs{$name}; my $sr = ReadingsVal ($name, '.pvCorrectionFactor_'.sprintf("%02d",$h).'_cloudcover', ''); @@ -11058,8 +11184,8 @@ sub _calcCaQcomplex { return; } - my $pvrl = CircularVal ($hash, sprintf("%02d",$h), 'pvrl', 0); - my $pvfc = CircularVal ($hash, sprintf("%02d",$h), 'pvapifc', 0); + my $pvrl = CircularVal ($hash, sprintf("%02d",$h), 'pvrl', 0); + my $pvfc = CircularVal ($hash, sprintf("%02d",$h), 'pvapifc', 0); if (!$pvrl || !$pvfc) { storeReading ('.pvCorrectionFactor_'.sprintf("%02d",$h).'_cloudcover', 'done'); @@ -11087,14 +11213,16 @@ sub _calcCaQcomplex { storeReading ('.pvCorrectionFactor_'.sprintf("%02d",$h).'_cloudcover', 'done'); + $aihit = $aihit ? ' AI result used,' : ''; + if ($acu =~ /on_complex/xs) { if ($paref->{cpcf} !~ /manual/xs) { # pcf-Reading nur überschreiben wenn nicht 'manual xxx' gesetzt - storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h), $factor." (automatic - old factor: $oldfac, Sun Alt range: $sabin, Cloud range: $crang, Days in range: $dnum)"); + storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h), $factor." (automatic - old factor: $oldfac,$aihit Sun Alt range: $sabin, Cloud range: $crang, Days in range: $dnum)"); } else { - storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h), $paref->{cpcf}." / flexmatic result $factor for Sun Alt range: $sabin, Cloud range: $crang, Days in range: $dnum"); + storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h), $paref->{cpcf}." / flexmatic result $factor for Sun Alt range: $sabin,$aihit Cloud range: $crang, Days in range: $dnum"); } - + storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h).'_autocalc', 'done'); } @@ -11113,6 +11241,7 @@ sub _calcCaQsimple { my $aln = $paref->{aln}; # Autolearning my $h = $paref->{h}; my $day = $paref->{day}; # aktueller Tag + my $aihit = $paref->{aihit}; my $hash = $defs{$name}; my $sr = ReadingsVal ($name, '.pvCorrectionFactor_'.sprintf("%02d",$h).'_apipercentil', ''); @@ -11155,14 +11284,16 @@ sub _calcCaQsimple { storeReading ('.pvCorrectionFactor_'.sprintf("%02d",$h).'_apipercentil', 'done'); + $aihit = $aihit ? ' AI result used,' : ''; + if ($acu =~ /on_simple/xs) { if ($paref->{cpcf} !~ /manual/xs) { # pcf-Reading nur überschreiben wenn nicht 'manual xxx' gesetzt - storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h), $factor." (automatic - old factor: $oldfac, Days in range: $dnum)"); + storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h), $factor." (automatic - old factor: $oldfac,$aihit Days in range: $dnum)"); } else { - storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h), $paref->{cpcf}." / flexmatic result $factor, Days in range: $dnum"); + storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h), $paref->{cpcf}." / flexmatic result $factor,$aihit Days in range: $dnum"); } - + storeReading ('pvCorrectionFactor_'.sprintf("%02d",$h).'_autocalc', 'done'); } @@ -11323,14 +11454,14 @@ sub saveEnergyConsumption { my $gcon = ReadingsNum ($name, "Today_Hour".$shr."_GridConsumption", 0); my $batin = ReadingsNum ($name, "Today_Hour".$shr."_BatIn", 0); my $batout = ReadingsNum ($name, "Today_Hour".$shr."_BatOut", 0); - + my $con = $pvrl - $gfeedin + $gcon - $batin + $batout; - + for my $prn (1..$maxproducer) { # V1.32.0 : Erzeugung sonstiger Producer (01..03) hinzufügen $prn = sprintf "%02d", $prn; $con += ReadingsNum ($name, "Today_Hour".$shr."_PPreal".$prn, 0); } - + if (int $paref->{minute} > 30 && $con < 0) { # V1.32.0 : erst den "eingeschwungenen" Zustand mit mehreren Meßwerten auswerten my $vl = 3; my $pre = '- WARNING -'; @@ -11670,6 +11801,7 @@ sub entryGraphic { my $paref = { name => $name, + hash => $hash, type => $hash->{TYPE}, ftui => $ftui, pah => $pah, @@ -11801,6 +11933,8 @@ sub entryGraphic { if ($paref->{beam3cont} || $paref->{beam4cont}) { # Balkengrafik Ebene 2 my %hfcg2; + $hfcg2{barcount} = $hfcg1{barcount}; # Anzahl Balken der Ebene1 zur Begrenzung Ebene 2 übernehmen + $paref->{beam1cont} = $paref->{beam3cont}; $paref->{beam2cont} = $paref->{beam4cont}; $paref->{colorb1} = AttrVal ($name, 'graphicBeam3Color', $b3coldef); @@ -11877,8 +12011,8 @@ sub _checkSetupNotComplete { my $type = $hash->{TYPE}; ### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !! - ########################################################################################## - + ########################################################################################## + ########################################################################################## my $is = AttrVal ($name, 'setupInverterStrings', undef); # String Konfig @@ -12369,7 +12503,7 @@ sub __createUpdateIcon { my $name = $paref->{name}; my $lang = $paref->{lang}; my $ftui = $paref->{ftui}; - + my $upstate = ReadingsVal ($name, 'state', ''); my $naup = ReadingsVal ($name, 'nextCycletime', ''); @@ -12764,7 +12898,7 @@ sub ___ghoValForm { my $type = $paref->{type}; my $fn = $data{$type}{$name}{func}{ghoValForm}; - + return ($val, $unit) if(!$fn || !$dev || !$rdg || !defined $val); my $DEVICE = $dev; @@ -12839,9 +12973,9 @@ sub _showConsumerInGraphicBeam { my $type = $paref->{type}; my $hfcg = $paref->{hfcg}; my $lang = $paref->{lang}; - + my $hash = $defs{$name}; - + # get consumer list and display it in Graphics ################################################ my @consumers = sort{$a<=>$b} keys %{$data{$type}{$name}{consumers}}; # definierte Verbraucher ermitteln @@ -12903,7 +13037,7 @@ return; ################################################################ sub _graphicConsumerLegend { my $paref = shift; - my $name = $paref->{name}; + my $name = $paref->{name}; my ($clegendstyle, $clegend) = split '_', $paref->{clegend}; my $clink = $paref->{clink}; @@ -12914,7 +13048,7 @@ sub _graphicConsumerLegend { $paref->{clegend} = $clegend; return if(!$clegend ); - + my $hash = $defs{$name}; my $ftui = $paref->{ftui}; my $lang = $paref->{lang}; @@ -13138,7 +13272,7 @@ sub _beamGraphicFirstHour { my $kw = $paref->{kw}; my $day; - + my $hash = $defs{$name}; my $stt = NexthoursVal ($hash, "NextHour00", "starttime", '0000-00-00 24'); my ($year,$month,$day_str,$thishour) = $stt =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; @@ -13300,7 +13434,7 @@ sub _beamGraphicRemainingHours { $val1 = NexthoursVal ($hash, 'NextHour'.$nh, 'pvfc', 0); $val4 = NexthoursVal ($hash, 'NextHour'.$nh, 'confc', 0); $hfcg->{$i}{weather} = NexthoursVal ($hash, 'NextHour'.$nh, 'weatherid', 999); - $hfcg->{$i}{wcc} = NexthoursVal ($hash, 'NextHour'.$nh, 'cloudcover', '-'); + $hfcg->{$i}{wcc} = NexthoursVal ($hash, 'NextHour'.$nh, 'wcc', '-'); $hfcg->{$i}{sunalt} = NexthoursVal ($hash, 'NextHour'.$nh, 'sunalt', '-'); $hfcg->{$i}{sunaz} = NexthoursVal ($hash, 'NextHour'.$nh, 'sunaz', '-'); } @@ -13381,20 +13515,21 @@ sub _beamGraphic { my ($val, $z2, $z3, $z4, $he, $titz2, $titz3); - my $ret .= __weatherOnBeam ($paref); - my $m = $paref->{modulo} % 2; + my $barcount = $hfcg->{barcount} // 9999; # Anzahl Balken der vorangegangenen Ebene zur Begrenzung dieser Ebene + my $ret .= __weatherOnBeam ($paref); + my $m = $paref->{modulo} % 2; if ($show_diff eq 'top') { # Zusätzliche Zeile Ertrag - Verbrauch $ret .= ""; - my $ii; + + my $ii = 0; for my $i (0..($maxhours * 2) - 1) { # gleiche Bedingung wie oben next if(!$show_night && $hfcg->{$i}{weather} > 99 && !$hfcg->{$i}{beam1} && !$hfcg->{$i}{beam2}); $ii++; # wieviele Stunden haben wir bisher angezeigt ? - - last if($ii > $maxhours); # vorzeitiger Abbruch + last if($ii > $maxhours || $ii > $barcount); # vorzeitiger Abbruch $val = formatVal6 ($hfcg->{$i}{diff}, $kw, $hfcg->{$i}{weather}); @@ -13419,7 +13554,9 @@ sub _beamGraphic { && !$hfcg->{$i}{beam1} && !$hfcg->{$i}{beam2}); $ii++; - last if($ii > $maxhours); + last if($ii > $maxhours || $ii > $barcount); + + $hfcg->{barcount} = $ii; # Anzahl Balken zur Begrenzung der nächsten Ebene registrieren $height = 200 if(!$height); # Fallback, sollte eigentlich nicht vorkommen, außer der User setzt es auf 0 $maxVal = 1 if(!int $maxVal); # maxVal kann gerade bei kleineren maxhours Ausgaben in der Nacht leicht auf 0 fallen @@ -13428,7 +13565,7 @@ sub _beamGraphic { # Berechnung der Zonen ######################## if ($lotype eq 'single') { - $he = int(($maxVal - $hfcg->{$i}{beam1}) / $maxVal * $height) + $fsize; # Der zusätzliche Offset durch $fsize verhindert bei den meisten Skins dass die Grundlinie der Balken nach unten durchbrochen wird + $he = int(($maxVal - $hfcg->{$i}{beam1}) / $maxVal * $height) + $fsize; # Der zusätzliche Offset durch $fsize verhindert bei den meisten Skins dass die Grundlinie der Balken nach unten durchbrochen wird $z3 = int($height + $fsize - $he); $titz3 = qq/title="$hfcg->{0}{beam1txt}"/; } @@ -13755,7 +13892,9 @@ return $ret; ################################################################ sub _flowGraphic { my $paref = shift; + my $hash = $paref->{hash}; my $name = $paref->{name}; + my $type = $paref->{type}; my $flowgsize = $paref->{flowgsize}; my $flowgani = $paref->{flowgani}; my $flowgshift = $paref->{flowgshift}; # Verschiebung der Flußgrafikbox (muß negiert werden) @@ -13765,28 +13904,60 @@ sub _flowGraphic { my $flowgconPower = $paref->{flowgconsPower}; my $consDist = $paref->{flowgconsDist}; my $css = $paref->{css}; - - my $hash = $defs{$name}; + my $style = 'width:98%; height:'.$flowgsize.'px;'; - my $animation = $flowgani ? '@keyframes dash { to { stroke-dashoffset: 0; } }' : ''; # Animation Ja/Nein - my $cpv = ReadingsNum ($name, 'Current_PV', 0); + my $animation = $flowgani ? '@keyframes dash { to { stroke-dashoffset: 0; } }' : ''; # Animation Ja/Nein my $cgc = ReadingsNum ($name, 'Current_GridConsumption', 0); my $cgfi = ReadingsNum ($name, 'Current_GridFeedIn', 0); my $csc = ReadingsNum ($name, 'Current_SelfConsumption', 0); my $cc = CurrentVal ($hash, 'consumption', 0); - my $gp01 = CurrentVal ($hash, 'generationp01', 0); - my $gp02 = CurrentVal ($hash, 'generationp02', 0); - my $gp03 = CurrentVal ($hash, 'generationp03', 0); - my $cc_dummy = $cc; + my $cpv = ReadingsNum ($name, 'Current_PV', 0); my $batin = ReadingsNum ($name, 'Current_PowerBatIn', undef); my $batout = ReadingsNum ($name, 'Current_PowerBatOut', undef); my $soc = ReadingsNum ($name, 'Current_BatCharge', 100); + my $cc_dummy = $cc; - my $bat_color = $soc < 26 ? 'flowg bat25' : - $soc < 76 ? 'flowg bat50' : - 'flowg bat75'; + my $hasbat = 1; # initial Batterie vorhanden + my $flowgprods = 1; # Producer in der Energieflußgrafik anzeigen per default + my $ppcurr = {}; # Hashref Producer current power + my $cpcurr = {}; # Hashref Consumer current power - my $hasbat = 1; + ## definierte Producer ermitteln und deren + ## aktuelle Leistung bestimmen + ############################################ + my $producercount = 0; + my $ppall = 0; # Summe Erzeugung alle Poducer + my @producers; + + for my $i (1..$maxproducer) { + my $pn = sprintf "%02d", $i; + my $p = CurrentVal ($hash, 'generationp'.$pn, undef); + + if (defined $p) { + push @producers, sprintf "%02d", $i; + $ppcurr->{$pn} = $p; + $producercount += 1; + $ppall += $p; + } + } + + ## definierte Verbraucher ermitteln + ##################################### + my $consumercount = 0; + my @consumers; + + for my $c (sort{$a<=>$b} keys %{$data{$type}{$name}{consumers}}) { # definierte Verbraucher ermitteln + next if(isConsumerNoshow ($hash, $c) =~ /^[13]$/xs); # auszublendende Consumer nicht berücksichtigen + push @consumers, $c; + $cpcurr->{$c} = ReadingsNum ($name, "consumer${c}_currentPower", 0); + $consumercount += 1; + } + + ## Batterie + Werte festlegen + ############################### + my $bat_color = $soc < 26 ? 'flowg bat25' : + $soc < 76 ? 'flowg bat50' : + 'flowg bat75'; if (!defined($batin) && !defined($batout)) { $hasbat = 0; @@ -13794,53 +13965,62 @@ sub _flowGraphic { $batout = 0; $soc = 0; } - else { - #$csc -= $batout; - } my $grid_color = $cgfi ? 'flowg grid_color1' : 'flowg grid_color2'; - $grid_color = 'flowg grid_color3' if (!$cgfi && !$cgc && $batout); # dritte Farbe my $cgc_style = $cgc ? 'flowg active_in' : 'flowg inactive_in'; my $batout_style = $batout ? 'flowg active_out active_bat_out' : 'flowg inactive_in'; + $grid_color = 'flowg grid_color3' if(!$cgfi && !$cgc && $batout); # dritte Farbe + my $cgc_direction = 'M490,515 L670,590'; # Batterientladung ins Netz - my $cgc_direction = 'M490,305 L670,510'; # Batterientladung ins Netz - - if ($batout) { + if ($batout) { # Batterie wird entladen my $cgfo = $cgfi - $cpv; - if($cgfo > 1) { - $cgc_style = 'flowg active_out'; - $cgc_direction = 'M670,510 L490,305'; - $cgfi -= $cgfo; - $cgc = $cgfo; + if ($cgfo > 1) { + $cgc_style = 'flowg active_out'; + $cgc_direction = 'M670,590 L490,515'; + $cgfi -= $cgfo; + $cgc = $cgfo; } } - my $batout_direction = 'M902,305 L730,510'; # Batterientladung aus Netz + my $batout_direction = 'M902,515 L730,590'; - if ($batin) { + if ($batin) { # Batterie wird geladen my $gbi = $batin - $cpv; - if ($gbi > 1) { + if ($gbi > 1) { # Batterieladung anteilig aus Hausnetz geladen $batin -= $gbi; $batout_style = 'flowg active_in'; - $batout_direction = 'M730,510 L902,305'; + $batout_direction = 'M730,590 L902,515'; $batout = $gbi; } } - my $sun_color = $cpv ? 'flowg sun_active' : 'flowg sun_inactive'; - my $batin_style = $batin ? 'flowg active_in active_bat_in' : 'flowg inactive_out'; - my $csc_style = $csc && $cpv ? 'flowg active_out' : 'flowg inactive_out'; - my $cgfi_style = $cgfi ? 'flowg active_out' : 'flowg inactive_out'; + ## Werte / SteuerungVars anpassen + ################################### + $flowgcons = 0 if(!$consumercount); # Consumer Anzeige ausschalten wenn keine Consumer definiert + $flowgprods = 0 if(!$producercount); # Producer Anzeige ausschalten wenn keine Producer definiert + my $p2home = sprintf "%.1f", ($csc + $ppall); # Energiefluß von Sonne zum Haus: Selbstverbrauch + alle Producer + $p2home = sprintf "%.0f", $p2home if($p2home > 10); + $p2home = 0 if($p2home == 0); # 0.0 eliminieren wenn keine Leistung zum Haus + + ## SVG Box initialisieren + ########################### + my $sun_color = $cpv ? 'flowg sun_active' : 'flowg sun_inactive'; + my $batin_style = $batin ? 'flowg active_in active_bat_in' : 'flowg inactive_out'; + my $csc_style = $p2home ? 'flowg active_out' : 'flowg inactive_out'; + my $cgfi_style = $cgfi ? 'flowg active_out' : 'flowg inactive_out'; + + my $vbwidth = 800; # width and height specify the viewBox size + my $vbminx = -10 * $flowgshift; # min-x and min-y represent the smallest X and Y coordinates that the viewBox may have + my $vbminy = $flowgprods ? -25 : 100; + + my $vbhight = !$flowgcons ? 380 : + !$flowgconTime ? 590 : + 610; + + $vbhight += 100 if($flowgprods); - my $vbminx = -10 * $flowgshift; # min-x and min-y represent the smallest X and Y coordinates that the viewBox may have - my $vbminy = -25; - my $vbwidth = 800; # width and height specify the viewBox size - my $vbhight = !$flowgcons ? 480 : - $flowgconTime ? 700 : - 680; - my $vbox = "$vbminx $vbminy $vbwidth $vbhight"; my $ret = << "END0"; @@ -13851,7 +14031,7 @@ sub _flowGraphic { - + @@ -13879,33 +14059,55 @@ sub _flowGraphic { - + - + END0 - ## get consumer list and display in Graphics - ############################################## + ## Producer Liste und Icons in Grafik anzeigen + ################################################ my $pos_left = 0; - my $consumercount = 0; - my $consumer_start = 0; - my $currentPower = 0; - my @consumers; + my $producer_start = 0; + my $producerPower = 0; - if ($flowgcons) { - my $type = $paref->{type}; - - for my $c (sort{$a<=>$b} keys %{$data{$type}{$name}{consumers}}) { # definierte Verbraucher ermitteln - next if(isConsumerNoshow ($hash, $c) =~ /^[13]$/xs); # ausgeblendete Consumer nicht berücksichtigen - push @consumers, $c; + if ($flowgprods) { + if ($producercount % 2) { + $producer_start = 350 - ($consDist * (($producercount -1) / 2)); + } + else { + $producer_start = 350 - (($consDist / 2) * ($producercount-1)); } - $consumercount = scalar @consumers; + $pos_left = $producer_start + 25; + for my $prnxnum (@producers) { + my $palias = CurrentVal ($hash, 'aliasp'.$prnxnum, 'Producer'.$prnxnum); + my $picon = __substituteIcon ( { hash => $hash, # Icon des Producerdevices + name => $name, + pn => $prnxnum, + pcurr => $ppcurr->{$prnxnum} + } + ); + + $ret .= ''; + $ret .= "$palias".FW_makeImage($picon, ''); + $ret .= ' '; + + $pos_left += $consDist; + } + } + + ## Consumer Liste und Icons in Grafik anzeigen + ############################################### + $pos_left = 0; + my $consumer_start = 0; + my $currentPower = 0; + + if ($flowgcons) { if ($consumercount % 2) { $consumer_start = 350 - ($consDist * (($consumercount -1) / 2)); } @@ -13916,9 +14118,9 @@ END0 $pos_left = $consumer_start + 15; for my $c (@consumers) { - my $calias = ConsumerVal ($hash, $c, "alias", ""); # Name des Consumerdevices - $currentPower = ReadingsNum ($name, "consumer${c}_currentPower", 0); - my $cicon = __substConsumerIcon ($hash, $c, $currentPower); # Icon des Consumerdevices + my $calias = ConsumerVal ($hash, $c, "alias", ""); # Name des Consumerdevices + $currentPower = $cpcurr->{$c}; + my $cicon = __substituteIcon ({hash => $hash, name => $name, cn => $c, pcurr => $currentPower}); # Icon des Consumerdevices $cc_dummy -= $currentPower; $ret .= ''; @@ -13929,9 +14131,12 @@ END0 } } + ## Batterie, PV, Netz Laufketten + ################################## + if ($hasbat) { $ret .= << "END1"; - + END1 @@ -13945,38 +14150,71 @@ END1 if ($flowgconX) { # Dummy Consumer my $dumcol = $cc_dummy <= 0 ? '@grey' : q{}; # Einfärbung Consumer Dummy - $ret .= ''; - $ret .= "consumer_X".FW_makeImage('light_light_dim_100'.$dumcol, ''); + $ret .= ''; + $ret .= "consumer_X".FW_makeImage($consicondef.$dumcol, ''); $ret .= ' '; - } + } - $ret .= << "END2"; - - - - + $ret .= << "END2"; + + + + END2 if ($hasbat) { $ret .= << "END3"; - + END3 } - if ($flowgconX) { # Dummy Consumer + ## Dummy Consumer Laufketten + ############################## + if ($flowgconX) { my $consumer_style = 'flowg inactive_out'; $consumer_style = 'flowg active_in' if($cc_dummy > 1); + my $chain_color = ""; # Farbe der Laufkette Consumer-Dummy - my $chain_color = ""; # Farbe der Laufkette Consumer-Dummy if ($cc_dummy > 0.5) { $chain_color = 'style="stroke: #'.substr(Color::pahColor(0,500,1000,$cc_dummy,[0,255,0, 127,255,0, 255,255,0, 255,127,0, 255,0,0]),0,6).';"'; #$chain_color = 'style="stroke: #DF0101;"'; } - $ret .= qq{}; + $ret .= qq{}; } + ## Producer Laufketten + ######################## + if ($flowgprods) { + $pos_left = $producer_start * 2; + my $pos_left_start_con = 0; + my $distance_con = 25; + + if ($producercount % 2) { + $pos_left_start_con = 700 - ($distance_con * (($producercount -1) / 2)); + } + else { + $pos_left_start_con = 700 - ((($distance_con ) / 2) * ($producercount-1)); + } + + for my $prnxnum (@producers) { + my $p = $ppcurr->{$prnxnum}; + my $consumer_style = 'flowg inactive_out'; + $consumer_style = 'flowg active_out' if($p > 0); + my $chain_color = ''; # Farbe der Laufkette des Producers + + if ($p) { + #$chain_color = 'style="stroke: #'.substr(Color::pahColor(0,50,100,$p,[0,255,0, 127,255,0, 255,255,0, 255,127,0, 255,0,0]),0,6).';"'; + $chain_color = 'style="stroke: darkorange;"'; + } + + $ret .= qq{}; # Design Consumer Laufkette + $pos_left += ($consDist * 2); + $pos_left_start_con += $distance_con; + } + } + ## Consumer Laufketten ######################## if ($flowgcons) { @@ -13994,7 +14232,7 @@ END3 for my $c (@consumers) { my $power = ConsumerVal ($hash, $c, "power", 0); my $rpcurr = ConsumerVal ($hash, $c, "rpcurr", ""); # Reading für akt. Verbrauch angegeben ? - $currentPower = ReadingsNum ($name, "consumer${c}_currentPower", 0); + $currentPower = $cpcurr->{$c}; if (!$rpcurr && isConsumerPhysOn($hash, $c)) { # Workaround wenn Verbraucher ohne Leistungsmessung $currentPower = $power; @@ -14007,40 +14245,88 @@ END3 my $chain_color = ""; # Farbe der Laufkette des Consumers if ($p > 0.5) { - $chain_color = 'style="stroke: #'.substr(Color::pahColor(0,50,100,$p,[0,255,0, 127,255,0, 255,255,0, 255,127,0, 255,0,0]),0,6).';"'; + $chain_color = 'style="stroke: #'.substr(Color::pahColor(0,50,100,$p,[0,255,0, 127,255,0, 255,255,0, 255,127,0, 255,0,0]),0,6).';"'; #$chain_color = 'style="stroke: #DF0101;"'; } - $ret .= qq{}; # Design Consumer Laufkette + $ret .= qq{}; # Design Consumer Laufkette $pos_left += ($consDist * 2); $pos_left_start += $distance; } } - ## Angaben Dummy-Verbraucher - ############################# - $cc_dummy = sprintf("%.0f",$cc_dummy); - ## Textangaben an Grafikelementen ################################### - $ret .= qq{$cpv} if ($cpv); - $ret .= qq{$soc %} if ($hasbat); # Lage Ladungs Text - $ret .= qq{$csc} if ($csc && $cpv); - $ret .= qq{$cgfi} if ($cgfi); - $ret .= qq{$cgc} if ($cgc); - $ret .= qq{$batout} if ($batout && $hasbat); - $ret .= qq{$batin} if ($batin && $hasbat); - $ret .= qq{$cc}; # Current_Consumption Anlage - $ret .= qq{$cc_dummy} if ($flowgconX && $flowgconPower); # Current_Consumption Dummy + $cc_dummy = sprintf("%.0f", $cc_dummy); # Verbrauch Dummy-Consumer + $ret .= qq{$cpv} if ($cpv); + $ret .= qq{$soc %} if ($hasbat); # Lage Text Batterieladungszustand + $ret .= qq{$p2home} if ($p2home); + $ret .= qq{$cgfi} if ($cgfi); + $ret .= qq{$cgc} if ($cgc); + $ret .= qq{$batout} if ($batout && $hasbat); + $ret .= qq{$batin} if ($batin && $hasbat); + $ret .= qq{$cc}; # Current_Consumption Anlage + $ret .= qq{$cc_dummy} if ($flowgconX && $flowgconPower); # Current_Consumption Dummy - ## Consumer Anzeige - ##################### + ## Textangabe Producer + ######################## + if ($flowgprods) { + $pos_left = ($producer_start * 2) - 50; # -XX -> Start Lage producer Beschriftung + + for my $prnxnum (@producers) { + $currentPower = sprintf "%.2f", $ppcurr->{$prnxnum}; + $currentPower = sprintf "%.0f", $currentPower if($currentPower > 10); + + # Leistungszahl abhängig von der Größe entsprechend auf der x-Achse verschieben + ############################################################################### + if (length($currentPower) >= 5) { + $pos_left -= 40; + } + elsif (length($currentPower) >= 4) { + $pos_left -= 25; + } + elsif (length($currentPower) >= 3 and $currentPower ne "0.0") { + $pos_left -= 5; + } + elsif (length($currentPower) >= 2 and $currentPower ne "0.0") { + $pos_left += 7; + } + elsif (length($currentPower) == 1) { + $pos_left += 25; + } + + $ret .= qq{$currentPower} if($flowgconPower); # Lage producer Consumption + + # Leistungszahl abhängig von der Größe entsprechend auf der x-Achse wieder zurück an den Ursprungspunkt + ####################################################################################################### + if (length($currentPower) >= 5) { + $pos_left += 40; + } + elsif (length($currentPower) >= 4) { + $pos_left += 25; + } + elsif (length($currentPower) >= 3 and $currentPower ne "0.0") { + $pos_left += 5; + } + elsif (length($currentPower) >= 2 and $currentPower ne "0.0") { + $pos_left -= 7; + } + elsif (length($currentPower) == 1) { + $pos_left -= 25; + } + + $pos_left += ($consDist * 2); + } + } + + ## Textangabe Consumer + ######################## if ($flowgcons) { $pos_left = ($consumer_start * 2) - 50; # -XX -> Start Lage Consumer Beschriftung for my $c (@consumers) { - $currentPower = sprintf "%.1f", ReadingsNum($name, "consumer${c}_currentPower", 0); - $currentPower =~ s/\.0$// if (int($currentPower) > 0); # .0 am Ende interessiert nicht + $currentPower = sprintf "%.1f", $cpcurr->{$c}; + $currentPower = sprintf "%.0f", $currentPower if($currentPower > 10); my $consumerTime = ConsumerVal ($hash, $c, "remainTime", ""); # Restlaufzeit my $rpcurr = ConsumerVal ($hash, $c, "rpcurr", ""); # Readingname f. current Power @@ -14100,32 +14386,45 @@ return $ret; } ################################################################ -# prüfe ob Verbrauchericon + Farbe angegeben ist +# prüfe ob Icon + Farbe angegeben ist # und setze ggf. Ersatzwerte -# $c - Consumer Nummer +# $cn - Consumernummer (01...max) +# $pn - Producernummer (01...max) +# $pcurr - aktuelle Leistung / Verbrauch ################################################################ -sub __substConsumerIcon { - my $hash = shift; - my $c = shift; - my $pcurr = shift; +sub __substituteIcon { + my $paref = shift; + my $hash = $paref->{hash}; + my $name = $paref->{name}; + my $cn = $paref->{cn}; + my $pn = $paref->{pn}; + my $pcurr = $paref->{pcurr}; - my $name = $hash->{NAME}; - my $cicon = ConsumerVal ($hash, $c, "icon", ""); # Icon des Consumerdevices angegeben ? + my ($color, $icon); - if (!$cicon) { - $cicon = 'light_light_dim_100'; + if ($cn) { # Icon Consumer + $icon = ConsumerVal ($hash, $cn, 'icon', $consicondef); + } + elsif ($pn) { + $icon = CurrentVal ($hash, 'iconp'.$pn, $prodicondef); # Icon Producer } - my $color; - ($cicon,$color) = split '@', $cicon; + ($icon, $color) = split '@', $icon; - if (!$color) { - $color = isConsumerLogOn ($hash, $c, $pcurr) ? 'darkorange' : ''; + if ($cn) { + if (!$color) { + $color = isConsumerLogOn ($hash, $cn, $pcurr) ? 'darkorange' : ''; + } + } + elsif ($pn) { + if (!$pcurr) { + $color = 'grey'; + } } - $cicon .= '@'.$color if($color); + $icon .= '@'.$color if($color); -return $cicon; +return $icon; } ################################################################ @@ -14386,7 +14685,7 @@ sub finishTrain { my $aiinitstate = $paref->{aiinitstate}; my $aitrainFinishTs = $paref->{aitrainLastFinishTs}; my $aiRulesNumber = $paref->{aiRulesNumber}; - + delete $data{$type}{$name}{circular}{99}{aiRulesNumber}; $data{$type}{$name}{current}{aiAddedToTrain} = 0; @@ -14396,7 +14695,7 @@ sub finishTrain { $data{$type}{$name}{circular}{99}{runTimeTrainAI} = $runTimeTrainAI if(defined $runTimeTrainAI); # !! in Circular speichern um zu persistieren, setTimeTracking speichert zunächst in Current !! $data{$type}{$name}{circular}{99}{aitrainLastFinishTs} = $aitrainFinishTs if(defined $aitrainFinishTs); $data{$type}{$name}{circular}{99}{aiRulesNumber} = $aiRulesNumber if(defined $aiRulesNumber); - + if ($aitrainstate eq 'ok') { _readCacheFile ({ name => $name, type => $type, @@ -14448,9 +14747,9 @@ sub aiAddInstance { ## no critic "not used" my $name = $paref->{name}; my $type = $paref->{type}; my $taa = $paref->{taa}; # do train after add - + my $hash = $defs{$name}; - + return if(!isPrepared4AI ($hash)); my $err = aiInit ($paref); @@ -14545,7 +14844,7 @@ sub aiTrain { ## no critic "not used" ), ""); $block ? return ($serial) : return \&finishTrain ($serial); } - + eval { $dtree->train(); 1; } @@ -14570,7 +14869,7 @@ sub aiTrain { ## no critic "not used" } setTimeTracking ($hash, $cst, 'runTimeTrainAI'); # Zyklus-Laufzeit ermitteln - + $serial = encode_base64 (Serialize ( {name => $name, aitrainstate => 'ok', runTimeTrainAI => CurrentVal ($hash, 'runTimeTrainAI', ''), @@ -14599,7 +14898,7 @@ sub aiGetResult { my $nhtstr = $paref->{nhtstr}; my $hash = $defs{$name}; - + return 'AI usage is not prepared' if(!isPrepared4AI ($hash, 'full')); my $dtree = AiDetreeVal ($hash, 'aitrained', undef); @@ -14613,11 +14912,11 @@ sub aiGetResult { debugLog ($paref, 'aiData', "Start AI result check for hod: $hod"); - my $wcc = NexthoursVal ($hash, $nhtstr, 'cloudcover', 0); - my $rr1c = NexthoursVal ($hash, $nhtstr, 'totalrain', 0); - my $temp = NexthoursVal ($hash, $nhtstr, 'temp', 20); - my $sunalt = NexthoursVal ($hash, $nhtstr, 'sunalt', 0); - my $sunaz = NexthoursVal ($hash, $nhtstr, 'sunaz', 0); + my $wcc = NexthoursVal ($hash, $nhtstr, 'wcc', 0); + my $rr1c = NexthoursVal ($hash, $nhtstr, 'rr1c', 0); + my $temp = NexthoursVal ($hash, $nhtstr, 'temp', 20); + my $sunalt = NexthoursVal ($hash, $nhtstr, 'sunalt', 0); + my $sunaz = NexthoursVal ($hash, $nhtstr, 'sunaz', 0); my $tbin = temp2bin ($temp); my $cbin = cloud2bin ($wcc); @@ -14755,18 +15054,18 @@ sub aiInit { ## no critic "not used" my $paref = shift; my $name = $paref->{name}; my $type = $paref->{type}; - + my $hash = $defs{$name}; if (!isPrepared4AI ($hash)) { delete $data{$type}{$name}{circular}{99}{aiRulesNumber}; delete $data{$type}{$name}{circular}{99}{runTimeTrainAI}; - delete $data{$type}{$name}{circular}{99}{aitrainLastFinishTs}; - + delete $data{$type}{$name}{circular}{99}{aitrainLastFinishTs}; + my $err = CurrentVal ($hash, 'aicanuse', ''); debugLog ($paref, 'aiProcess', $err); - + $data{$type}{$name}{current}{aiinitstate} = $err; return $err; } @@ -14791,9 +15090,9 @@ sub aiAddRawData { my $day = $paref->{day} // strftime "%d", localtime(time); # aktueller Tag (range 01 to 31) my $ood = $paref->{ood} // 0; # only one (current) day my $rho = $paref->{rho}; # only this hour of day - + my $hash = $defs{$name}; - + delete $data{$type}{$name}{current}{aitrawstate}; my $err; @@ -14875,7 +15174,7 @@ sub aiDelRawData { my $type = $paref->{type}; my $hash = $defs{$name}; - + if (!keys %{$data{$type}{$name}{aidectree}{airaw}}) { return; } @@ -14973,7 +15272,7 @@ sub setPVhistory { my $reorgday = $paref->{reorgday} // q{}; # Tag der reorganisiert werden soll my $hash = $defs{$name}; - + $data{$type}{$name}{pvhist}{$day}{99}{dayname} = $dayname if($day); if ($hfspvh{$histname} && defined &{$hfspvh{$histname}{fn}}) { @@ -15128,16 +15427,16 @@ sub listDataPool { my $sub = sub { my $day = shift; - + #for my $dh (keys %{$h->{$day}}) { # if (!isNumeric ($dh)) { # delete $data{$type}{$name}{pvhist}{$day}{$dh}; # Log3 ($name, 2, qq{$name - INFO - invalid key "$day -> $dh" was deleted from pvHistory storage}); # } #} - + my $ret; - + for my $key (sort {$a<=>$b} keys %{$h->{$day}}) { my $pvrl = HistoryVal ($hash, $day, $key, 'pvrl', '-'); my $pvrlvd = HistoryVal ($hash, $day, $key, 'pvrlvd', '-'); @@ -15171,7 +15470,7 @@ sub listDataPool { my $pprl01 = HistoryVal ($hash, $day, $key, 'pprl01', '-'); my $pprl02 = HistoryVal ($hash, $day, $key, 'pprl02', '-'); my $pprl03 = HistoryVal ($hash, $day, $key, 'pprl03', '-'); - + if ($export eq 'csv') { $hexp->{$day}{$key}{PVreal} = $pvrl; $hexp->{$day}{$key}{PVrealValid} = $pvrlvd; @@ -15216,7 +15515,7 @@ sub listDataPool { $ret .= "etotalp01: $etotp01, etotalp02: $etotp02, etotalp03: $etotp03" if($key ne '99'); $ret .= "\n " if($key ne '99'); $ret .= "pprl01: $pprl01, pprl02: $pprl02, pprl03: $pprl03"; - $ret .= "\n "; + $ret .= "\n "; $ret .= "confc: $confc, con: $con, gcons: $gcons, conprice: $conprc"; $ret .= "\n "; $ret .= "gfeedin: $gfeedin, feedprice: $feedprc"; @@ -15228,14 +15527,14 @@ sub listDataPool { $ret .= "\n " if($key eq '99'); $ret .= "batmaxsoc: $batmsoc, batsetsoc: $batssoc" if($key eq '99'); $ret .= "\n "; - + if ($key ne '99') { $ret .= "wid: $wid, "; $ret .= "wcc: $wcc, "; $ret .= "rr1c: $rr1c, "; - $ret .= "pvcorrf: $pvcorrf"; + $ret .= "pvcorrf: $pvcorrf "; } - + $ret .= "temp: $temp, " if($temp); $ret .= "dayname: $dayname, " if($dayname); @@ -15301,6 +15600,9 @@ sub listDataPool { $ret .= "\n "; $ret .= $csm; } + else { + $ret .= "\n "; + } } return $ret; }; @@ -15459,9 +15761,9 @@ sub listDataPool { my $pvaifc = NexthoursVal ($hash, $idx, 'pvaifc', '-'); # PV Forecast der KI my $aihit = NexthoursVal ($hash, $idx, 'aihit', '-'); # KI ForeCast Treffer Status my $wid = NexthoursVal ($hash, $idx, 'weatherid', '-'); - my $neff = NexthoursVal ($hash, $idx, 'cloudcover', '-'); + my $wcc = NexthoursVal ($hash, $idx, 'wcc', '-'); my $crang = NexthoursVal ($hash, $idx, 'cloudrange', '-'); - my $rr1c = NexthoursVal ($hash, $idx, 'totalrain', '-'); + my $rr1c = NexthoursVal ($hash, $idx, 'rr1c', '-'); my $rrange = NexthoursVal ($hash, $idx, 'rainrange', '-'); my $rad1h = NexthoursVal ($hash, $idx, 'rad1h', '-'); my $pvcorrf = NexthoursVal ($hash, $idx, 'pvcorrf', '-'); @@ -15478,7 +15780,7 @@ sub listDataPool { $sq .= "\n "; $sq .= "pvapifc: $pvapifc, pvaifc: $pvaifc, pvfc: $pvfc, aihit: $aihit, confc: $confc"; $sq .= "\n "; - $sq .= "confcEx: $confcex, DoN: $don, wid: $wid, wcc: $neff, rr1c: $rr1c, temp=$temp"; + $sq .= "confcEx: $confcex, DoN: $don, wid: $wid, wcc: $wcc, rr1c: $rr1c, temp=$temp"; $sq .= "\n "; $sq .= "rad1h: $rad1h, sunaz: $sunaz, sunalt: $sunalt"; $sq .= "\n "; @@ -15499,12 +15801,12 @@ sub listDataPool { my $pvcorrf = NexthoursVal ($hash, $idx, 'pvcorrf', '-/-'); my $aihit = NexthoursVal ($hash, $idx, 'aihit', '-'); my $pvfc = NexthoursVal ($hash, $idx, 'pvfc', '-'); - my $neff = NexthoursVal ($hash, $idx, 'cloudcover', '-'); + my $wcc = NexthoursVal ($hash, $idx, 'wcc', '-'); my $sunalt = NexthoursVal ($hash, $idx, 'sunalt', '-'); my ($f,$q) = split "/", $pvcorrf; $sq .= "\n" if($sq); - $sq .= "Start: $nhts, Quality: $q, Factor: $f, AI usage: $aihit, PV expect: $pvfc Wh, Sun Alt: $sunalt, Cloud: $neff"; + $sq .= "Start: $nhts, Quality: $q, Factor: $f, AI usage: $aihit, PV expect: $pvfc Wh, Sun Alt: $sunalt, Cloud: $wcc"; } } @@ -16618,7 +16920,7 @@ sub createAssociatedWith { my $badev = AttrVal ($name, 'setupBatteryDev', ''); # Battery Device ($aba,$h) = parseParams ($badev); $badev = $aba->[0] // ""; - + for my $c (sort{$a<=>$b} keys %{$data{$type}{$name}{consumers}}) { # Consumer Devices my $consumer = AttrVal ($name, "consumer${c}", ""); my ($ac,$hc) = parseParams ($consumer); @@ -16637,12 +16939,12 @@ sub createAssociatedWith { push @nd, $indev; push @nd, $medev; push @nd, $badev; - + for my $prn (1..$maxproducer) { $prn = sprintf "%02d", $prn; my $pdc = AttrVal ($name, "setupOtherProducer${prn}", ""); my ($prd) = parseParams ($pdc); - push @nd, $prd->[0] if($prd->[0]); + push @nd, $prd->[0] if($prd->[0]); } my @ndn = (); @@ -18217,13 +18519,13 @@ return ($pvrlsum, $pvfcsum, $dnumsum); # $nhr: nächste Stunde (NextHour00, NextHour01,...) # $key: starttime - Startzeit der abgefragten nächsten Stunde # hourofday - Stunde des Tages -# pvfc - PV Vorhersage in Wh +# pvfc - PV Vorhersage in Wh # pvaifc - erwartete PV Erzeugung der KI (Wh) # aihit - Trefferstatus KI # weatherid - DWD Wetter id -# cloudcover - DWD Wolkendichte +# wcc - DWD Wolkendichte # cloudrange - berechnete Bewölkungsrange -# totalrain - Gesamtniederschlag während der letzten Stunde kg/m2 +# rr1c - Gesamtniederschlag während der letzten Stunde kg/m2 # rad1h - Globalstrahlung (kJ/m2) # confc - prognostizierter Hausverbrauch (Wh) # confcEx - prognostizierter Hausverbrauch ohne registrierte Consumer (Wh) @@ -18779,7 +19081,7 @@ to ensure that the system configuration is correct.
Azimuth values are integers in the range -180 to 180. Although the specified identifiers can be used, - it is recommended to specify the exact azimuth value in the attribute. This allows any intermediate values such + it is recommended to specify the exact azimuth value in the attribute. This allows any intermediate values such as 83, 48 etc. to be specified.

@@ -18969,8 +19271,8 @@ to ensure that the system configuration is correct. Manual correction factor for hour XX of the day.
(default: 1.0)

- - Depending on the setting pvCorrectionFactor_Auto ('off' or 'on_.*'), + + Depending on the setting pvCorrectionFactor_Auto ('off' or 'on_.*'), a static or dynamic default setting is made:

@@ -19541,7 +19843,7 @@ to ensure that the system configuration is correct.
  • consumerXX <Device Name> type=<type> power=<power> [switchdev=<device>]
    - [mode=<mode>] [icon=<Icon>] [mintime=<minutes> | SunPath[:<Offset_Sunrise>:<Offset_Sunset>]]
    + [mode=<mode>] [icon=<Icon>[@<Color>]] [mintime=<minutes> | SunPath[:<Offset_Sunrise>:<Offset_Sunset>]]
    [on=<command>] [off=<command>] [swstate=<Readingname>:<on-Regex>:<off-Regex>] [asynchron=<Option>]
    [notbefore=<Expression>] [notafter=<Expression>] [locktime=<offlt>[:<onlt>]]
    [auto=<Readingname>] [pcurr=<Readingname>:<Unit>[:<Threshold>]] [etotal=<Readingname>:<Einheit>[:<Threshold>]]
    @@ -19610,7 +19912,7 @@ to ensure that the system configuration is correct.            The load is started even if there is insufficient PV surplus, provided that a set "swoncond" condition is met and "swoffcond" is not met. - icon Icon to represent the consumer in the overview graphic (optional) + icon Icon and, if applicable, its color for displaying the consumer in the overview graphic (optional) mintime Scheduling duration (minutes or "SunPath") of the consumer. (optional) By specifying SunPath, planning is done according to sunrise and sunset. @@ -19712,7 +20014,7 @@ to ensure that the system configuration is correct. attr <name> consumer01 wallplug icon=scene_dishwasher@orange type=dishwasher mode=can power=2500 on=on off=off notafter=20 etotal=total:kWh:5
    attr <name> consumer02 WPxw type=heater mode=can power=3000 mintime=180 on="on-for-timer 3600" notafter=12 auto=automatic
    attr <name> consumer03 Shelly.shellyplug2 type=other power=300 mode=must icon=it_ups_on_battery mintime=120 on=on off=off swstate=state:on:off auto=automatic pcurr=relay_0_power:W etotal:relay_0_energy_Wh:Wh swoncond=EcoFlow:data_data_socSum:-?([1-7][0-9]|[0-9]) swoffcond:EcoFlow:data_data_socSum:100
    - attr <name> consumer04 Shelly.shellyplug3 icon=scene_microwave_oven type=heater power=2000 mode=must notbefore=07 mintime=600 on=on off=off etotal=relay_0_energy_Wh:Wh pcurr=relay_0_power:W auto=automatic interruptable=eg.wz.wandthermostat:diff-temp:(22)(\.[2-9])|([2-9][3-9])(\.[0-9]):0.2
    + attr <name> consumer04 Shelly.shellyplug3 icon=scene_microwave_oven@ed type=heater power=2000 mode=must notbefore=07 mintime=600 on=on off=off etotal=relay_0_energy_Wh:Wh pcurr=relay_0_power:W auto=automatic interruptable=eg.wz.wandthermostat:diff-temp:(22)(\.[2-9])|([2-9][3-9])(\.[0-9]):0.2
    attr <name> consumer05 Shelly.shellyplug4 icon=sani_buffer_electric_heater_side type=heater mode=must power=1000 notbefore=7 notafter=20:10 auto=automatic pcurr=actpow:W on=on off=off mintime=SunPath interruptable=1
    attr <name> consumer06 Shelly.shellyplug5 icon=sani_buffer_electric_heater_side type=heater mode=must power=1000 notbefore=07:05 notafter={return'20:05'} auto=automatic pcurr=actpow:W on=on off=off mintime=SunPath:60:-120 interruptable=1
    attr <name> consumer07 SolCastDummy icon=sani_buffer_electric_heater_side type=heater mode=can power=600 auto=automatic pcurr=actpow:W on=on off=off mintime=15 asynchron=1 locktime=300:1200 interruptable=1 noshow=noShow
    @@ -19725,7 +20027,7 @@ to ensure that the system configuration is correct. If the corresponding prerequisites are met, training data is collected and stored for the module-internal AI.
    The data is deleted when it has exceeded the specified holding period (days).
    - (default: 1095) + (default: 1825)

  • @@ -19738,6 +20040,28 @@ to ensure that the system configuration is correct.
    + +
  • ctrlAreaFactorUsage
    + (DWD model only, experimental)

    + + When using the DWD model, an area factor of the solar modules is taken into account to calculate the + expected generation. This experimental attribute determines the method for determining the area factor. +

    + +
      + + + + + + + + +
      fix a uniquely determined area factor is used (default)
      trackFull the area factor is calculated continuously depending on the position of the sun and applied to the total global radiation
      trackShared the area factor is calculated continuously depending on the position of the sun and applied to an approximated
      proportion of the direct radiation in the global radiation
      trackFlex combines the 'trackFull' and 'trackShared' methods. The system switches from 'trackFull' to 'trackShared'
      at a cloud cover of >=80%.
      +
    +
  • +
    +
  • ctrlAutoRefresh
    If set, active browser pages of the FHEMWEB device that has called up the SolarForecast device are @@ -20115,7 +20439,7 @@ to ensure that the system configuration is correct. (default: 1)

  • - +
  • flowGraphicShift <Pixel/10>
    Horizontal shift of the energy flow graph.
    @@ -20680,18 +21004,19 @@ to ensure that the system configuration is correct. Note: Deleting the attribute also removes the internally corresponding data.

  • - - -
  • setupOtherProducerXX <Device Name> pcurr=<Readingname>:<Unit> etotal=<Readingname>:<Unit>

    - Defines any device and its readings for the delivery of other generation values - (e.g. CHP, wind generation, emergency generator). + +
  • setupOtherProducerXX <Device Name> pcurr=<Readingname>:<Unit> etotal=<Readingname>:<Unit> [icon=<Icon>[@<Color>]]

    + + Defines any device and its readings for the delivery of other generation values + (e.g. CHP, wind generation, emergency generator). This device is not intended for PV generation. It can also be a dummy device with corresponding readings.

      + @@ -20703,7 +21028,7 @@ to ensure that the system configuration is correct.
        Example:
        - attr <name> setupOtherProducer01 windwheel pcurr=total_pac:kW etotal=etotal:kWh + attr <name> setupOtherProducer01 windwheel pcurr=total_pac:kW etotal=etotal:kWh icon=Ventilator_wind@darkorange

      @@ -20773,7 +21098,8 @@ to ensure that the system configuration is correct. Free use of the Forecast.Solar API. does not require registration. API requests are limited to 12 within one hour in the free version. There is no daily limit. The module automatically determines the optimal query interval - depending on the configured strings. + depending on the configured strings.
      + Note: Based on previous experience, unreliable and not recommended.

      VictronKI-API
      @@ -20812,7 +21138,7 @@ to ensure that the system configuration is correct.
      - +
    • setupRoofTops <Stringname1>=<pk> [<Stringname2>=<pk> <Stringname3>=<pk> ...]
      (only when using Model SolCastAPI)

      @@ -21294,7 +21620,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. Voreinstellung des Korrekturfaktors für die Stunde XX des Tages.
      (default: 1.0)

      - + In Abhängigkeit vom Setting pvCorrectionFactor_Auto ('off' bzw. 'on_.*') erfolgt eine statische oder dynamische Voreinstellung:

      @@ -21552,8 +21878,8 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.
    • - - + + @@ -21618,12 +21944,11 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. + - -
      icon Icon and, if applicable, color for displaying the producer in the flow chart (optional)
      pcurr Reading which returns the current generation as a positive value or a self-consumption (special case) as a negative value
      etotal Reading which supplies the total energy generated (a continuously ascending counter)
      If the reading violates the specification of a continuously rising counter,
      confcEx erwarteter Energieverbrauch ohne Anteile Verbraucher mit gesetztem Schlüssel exconfc=1
      crange berechneter Bewölkungsbereich
      correff verwendeter Korrekturfaktor/Qualität
      Faktor/- -> keine Qualität definiert
      Faktor/0..1 - Qualität der PV Prognose (1 = beste Qualität)
      <Faktor>/- -> keine Qualität definiert
      <Faktor>/0..1 - Qualität der PV Prognose (1 = beste Qualität)
      DoN Sonnenauf- und untergangsstatus (0 - Nacht, 1 - Tag)
      hourofday laufende Stunde des Tages
      pvapifc erwartete PV Erzeugung (Wh) der verwendeten API inkl. einer eventuellen Korrektur
      pvrlvd 1-'pvrl' ist gültig und wird im Lernprozess berücksichtigt, 0-'pvrl' ist als abnormal bewertet
      pvcorrf verwendeter Autokorrekturfaktor / erreichte Prognosequalität
      rad1h Globalstrahlung (kJ/m2)
      rr1c Gesamtniederschlag in der letzten Stunde kg/m2
      sunalt Höhe der Sonne (in Dezimalgrad)
      sunaz Azimuth der Sonne (in Dezimalgrad)
      wid Identifikationsnummer des Wetters
      wcc effektive Wolkenbedeckung
      rr1c Gesamtniederschlag in der letzten Stunde kg/m2
  • @@ -21867,7 +22192,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.
  • consumerXX <Device Name> type=<type> power=<power> [switchdev=<device>]
    - [mode=<mode>] [icon=<Icon>] [mintime=<minutes> | SunPath[:<Offset_Sunrise>:<Offset_Sunset>]]
    + [mode=<mode>] [icon=<Icon>[@<Farbe>]] [mintime=<minutes> | SunPath[:<Offset_Sunrise>:<Offset_Sunset>]]
    [on=<Kommando>] [off=<Kommando>] [swstate=<Readingname>:<on-Regex>:<off-Regex>] [asynchron=<Option>]
    [notbefore=<Ausdruck>] [notafter=<Ausdruck>] [locktime=<offlt>[:<onlt>]]
    [auto=<Readingname>] [pcurr=<Readingname>:<Einheit>[:<Schwellenwert>]] [etotal=<Readingname>:<Einheit>[:<Schwellenwert>]]
    @@ -21935,7 +22260,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.            Der Start des Verbrauchers erfolgt auch bei ungenügendem PV-Überschuß sofern eine gesetzte "swoncond" Bedingung erfüllt und "swoffcond" nicht erfüllt ist. - icon Icon zur Darstellung des Verbrauchers in der Übersichtsgrafik (optional) + icon Icon und ggf. dessen Farbe zur Darstellung des Verbrauchers in der Übersichtsgrafik (optional) mintime Einplanungsdauer (Minuten oder "SunPath") des Verbrauchers. (optional) Mit der Angabe von SunPath erfolgt die Planung entsprechend des Sonnenauf- und untergangs. @@ -22037,7 +22362,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. attr <name> consumer01 wallplug icon=scene_dishwasher@orange type=dishwasher mode=can power=2500 on=on off=off notafter=20 etotal=total:kWh:5
    attr <name> consumer02 WPxw type=heater mode=can power=3000 mintime=180 on="on-for-timer 3600" notafter=12 auto=automatic
    attr <name> consumer03 Shelly.shellyplug2 type=other power=300 mode=must icon=it_ups_on_battery mintime=120 on=on off=off swstate=state:on:off auto=automatic pcurr=relay_0_power:W etotal:relay_0_energy_Wh:Wh swoncond=EcoFlow:data_data_socSum:-?([1-7][0-9]|[0-9]) swoffcond:EcoFlow:data_data_socSum:100
    - attr <name> consumer04 Shelly.shellyplug3 icon=scene_microwave_oven type=heater power=2000 mode=must notbefore=07 mintime=600 on=on off=off etotal=relay_0_energy_Wh:Wh pcurr=relay_0_power:W auto=automatic interruptable=eg.wz.wandthermostat:diff-temp:(22)(\.[2-9])|([2-9][3-9])(\.[0-9]):0.2
    + attr <name> consumer04 Shelly.shellyplug3 icon=scene_microwave_oven@red type=heater power=2000 mode=must notbefore=07 mintime=600 on=on off=off etotal=relay_0_energy_Wh:Wh pcurr=relay_0_power:W auto=automatic interruptable=eg.wz.wandthermostat:diff-temp:(22)(\.[2-9])|([2-9][3-9])(\.[0-9]):0.2
    attr <name> consumer05 Shelly.shellyplug4 icon=sani_buffer_electric_heater_side type=heater mode=must power=1000 notbefore=7 notafter=20:10 auto=automatic pcurr=actpow:W on=on off=off mintime=SunPath interruptable=1
    attr <name> consumer06 Shelly.shellyplug5 icon=sani_buffer_electric_heater_side type=heater mode=must power=1000 notbefore=07:20 notafter={return'20:05'} auto=automatic pcurr=actpow:W on=on off=off mintime=SunPath:60:-120 interruptable=1
    attr <name> consumer07 SolCastDummy icon=sani_buffer_electric_heater_side type=heater mode=can power=600 auto=automatic pcurr=actpow:W on=on off=off mintime=15 asynchron=1 locktime=300:1200 interruptable=1 noshow=noShow
    @@ -22050,7 +22375,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. Sind die entsprechenden Voraussetzungen gegeben, werden Trainingsdaten für die modulinterne KI gesammelt und gespeichert.
    Die Daten werden gelöscht wenn sie die angegebene Haltedauer (Tage) überschritten haben.
    - (default: 1095) + (default: 1825)

  • @@ -22063,6 +22388,29 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.
    + +
  • ctrlAreaFactorUsage
    + (nur Model DWD, experimentell)

    + + Bei Verwendung des Model DWD wird zur Berechnung der voraussichtlichen Erzeugung ein Flächenfaktor der + Solarmodule berücksichtigt. Dieses experimentelle Attribut bestimmt das Verfahren zur Ermittlung des + Flächenfaktors. +

    + +
      + + + + + + + + +
      fix es wird ein einmalig ermittelter Flächenfaktor verwendet (default)
      trackFull der Flächenfaktor wird kontinuierlich abhängig vom Sonnenstand berechnet und auf die gesamte Globalstrahlung angewendet
      trackShared der Flächenfaktor wird kontinuierlich abhängig vom Sonnenstand berechnet und auf einen approximierten Anteil der
      Direktstrahlung an der Globalstrahlung angewendet
      trackFlex kombiniert die Verfahren 'trackFull' und 'trackShared'. Es erfolgt eine Umschaltung von 'trackFull' auf 'trackShared'
      bei einer Bewölkung von >=80%.
      +
    +
  • +
    +
  • ctrlAutoRefresh
    Wenn gesetzt, werden aktive Browserseiten des FHEMWEB-Devices welches das SolarForecast-Device aufgerufen hat, nach der @@ -22442,7 +22790,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. (default: 1)

  • - +
  • flowGraphicShift <Pixel/10>
    Horizontale Verschiebung der Energieflußgrafik.
    @@ -23005,18 +23353,19 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. Hinweis: Durch Löschen des Attributes werden ebenfalls die intern korrespondierenden Daten entfernt.

  • - - -
  • setupOtherProducerXX <Device Name> pcurr=<Readingname>:<Einheit> etotal=<Readingname>:<Einheit>

    - Legt ein beliebiges Device und dessen Readings zur Lieferung sonstiger Erzeugungswerte fest - (z.B. BHKW, Winderzeugung, Notstromaggregat). + +
  • setupOtherProducerXX <Device Name> pcurr=<Readingname>:<Einheit> etotal=<Readingname>:<Einheit> [icon=<Icon>[@<Farbe>]]

    + + Legt ein beliebiges Device und dessen Readings zur Lieferung sonstiger Erzeugungswerte fest + (z.B. BHKW, Winderzeugung, Notstromaggregat). Dieses Device ist nicht für PV-Erzeugung vorgsehen. Es kann auch ein Dummy Device mit entsprechenden Readings sein.

      + @@ -23028,7 +23377,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.
        Beispiel:
        - attr <name> setupOtherProducer01 windwheel pcurr=total_pac:kW etotal=etotal:kWh + attr <name> setupOtherProducer01 windwheel pcurr=total_pac:kW etotal=etotal:kWh icon=Ventilator_wind@darkorange

      @@ -23099,7 +23448,8 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. Die kostenfreie Nutzung der Forecast.Solar API erfordert keine Registrierung. Die API-Anfragen sind in der kostenfreien Version auf 12 innerhalb einer Stunde begrenzt. Ein Tageslimit gibt es dabei nicht. Das Modul ermittelt automatisch das optimale Abfrageintervall - in Abhängigkeit der konfigurierten Strings. + in Abhängigkeit der konfigurierten Strings.
      + Hinweis: Nach den bisherigen Erfahrungen unzuverlässig und nicht zu empfehlen.

      VictronKI-API
      @@ -23139,7 +23489,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.
      - +
    • setupRoofTops <Stringname1>=<pk> [<Stringname2>=<pk> <Stringname3>=<pk> ...]
      (nur bei Verwendung Model SolCastAPI)

      @@ -23226,6 +23576,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. "Time::HiRes": 0, "MIME::Base64": 0, "Math::Trig": 0, + "List::Util": 0, "Storable": 0 }, "recommends": {
    • icon Icon und ggf. Farbe zur Darstellung des Producers in der Flowgrafik (optional)
      pcurr Reading welches die aktuelle Erzeugung als positiven Wert oder einen Eigenverbrauch (Sonderfall) als negativen Wert liefert
      etotal Reading welches die gesamte erzeugte Energie liefert (ein stetig aufsteigender Zähler)
      Sollte des Reading die Vorgabe eines stetig aufsteigenden Zählers verletzen, behandelt