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 .= "