diff --git a/fhem/CHANGED b/fhem/CHANGED index 782914230..7ca861fca 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: Major Release 1.49.0 + (see https://forum.fhem.de/index.php?msg=1337341) - change: 36_Shelly: some log-levels - feature: configDB: add support for mariadb connections - change: 74_AutomowerConnect: add new error codes, implement new spelling of diff --git a/fhem/FHEM/76_SolarForecast.pm b/fhem/FHEM/76_SolarForecast.pm index 3113b73e2..c19db3b27 100644 --- a/fhem/FHEM/76_SolarForecast.pm +++ b/fhem/FHEM/76_SolarForecast.pm @@ -160,6 +160,13 @@ BEGIN { # Versions History intern my %vNotesIntern = ( + "1.49.0" => "23.03.2025 _listDataPoolApiData: fix warning item1, new option OpenMeteoDWD_D2-API with preparation for satellite support ". + "add Attr graphicBeamHeightLevel3, Compatibility of Rad1h data between DWD and OpenMeteo established ". + "set reset aiData deletes raw data also, _transferAPIRadiationValues: AI PV estimate limited to inverter capacity summary ". + "__calcPVestimates: pv power summary of all strings connected to inverter limited to inverter capacity summary ". + "_batChargeRecmd: fix calc if more than one batteries are installed, set aiDecTree: new option rawDataGHIreplace ". + "new Attr plantControl with keys feedinPowerLimit, batteryPreferredCharge, consForecastInPlanning ". + "Attr affectBatteryPreferredCharge, affectConsForecastInPlanning, ctrlShowLink are obsolete ", "1.48.0" => "14.03.2025 edit commandref, add graphicBeam layer 5 and 6, attr ctrlAIdataStorageDuration, ctrlAIshiftTrainStart removed ", "1.47.3" => "11.03.2025 adjust weather_ids and management of significant weather, _calcDataEveryFullHour: change attrInvChangedTs Management ". "split __batteryOnBeam into _beamFillupBatValues and itself, expand bat key 'show' by top, bottom ". @@ -386,6 +393,7 @@ my %vNotesIntern = ( ## Konstanten ###################### use constant { + INFINIITY => ~0 >> 1, # "Unendlich" LPOOLLENLIM => 140, # Breitenbegrenzung der Ausgabe von List Pooldaten KJ2KWH => 0.0002777777778, # Umrechnungsfaktor kJ in kWh KJ2WH => 0.2777777778, # Umrechnungsfaktor kJ in Wh @@ -431,7 +439,7 @@ use constant { OMETEOREPDEF => 900, # default Abrufintervall Open-Meteo API (s) VRMAPIREPDEF => 300, # default Abrufintervall Victron VRM API Forecast SOLCMAXREQDEF => 50, # max. täglich mögliche Requests SolCast API - OMETMAXREQ => 9700, # Beschränkung auf max. mögliche Requests Open-Meteo API + OMETMAXREQ => 8000, # Beschränkung auf max. mögliche Requests Open-Meteo API LEADTIME => 3600, # relative Zeit vor Sonnenaufgang zur Freigabe API Abruf / Verbraucherplanung LAGTIME => 1800, # Nachlaufzeit relativ zu Sunset bis Sperrung API Abruf @@ -560,19 +568,18 @@ my @rconfigs = qw( pvCorrectionFactor_Auto energyH4Trigger ); # Anlagenkonfiguration: maßgebliche Attribute -my @aconfigs = qw( affectBatteryPreferredCharge affectConsForecastIdentWeekdays - affectConsForecastInPlanning affectSolCastPercentile - aiControl +my @aconfigs = qw( affectConsForecastIdentWeekdays affectConsForecastLastDays + affectSolCastPercentile + aiControl consumerLegend consumerAdviceIcon consumerLink ctrlBackupFilesKeep ctrlConsRecommendReadings ctrlGenPVdeviation ctrlInterval ctrlLanguage ctrlNextDayForecastReadings ctrlNextHoursSoCForecastReadings - ctrlShowLink ctrlSolCastAPImaxReq + ctrlSolCastAPImaxReq ctrlSolCastAPIoptimizeReq ctrlSpecialReadings ctrlUserExitFn - setupWeatherDev1 setupWeatherDev2 setupWeatherDev3 disable flowGraphicControl graphicBeamWidth - graphicBeamHeightLevel1 graphicBeamHeightLevel2 + graphicBeamHeightLevel1 graphicBeamHeightLevel2 graphicBeamHeightLevel3 graphicBeam1Content graphicBeam2Content graphicBeam3Content graphicBeam4Content graphicBeam5Content graphicBeam6Content graphicBeam1Color graphicBeam2Color graphicBeam3Color graphicBeam4Color graphicBeam5Color graphicBeam6Color graphicBeam1FontColor graphicBeam2FontColor graphicBeam3FontColor graphicBeam4FontColor graphicBeam5FontColor graphicBeam6FontColor @@ -580,7 +587,9 @@ my @aconfigs = qw( affectBatteryPreferredCharge affectConsForecastIdentWeekdays graphicHeaderDetail graphicHeaderShow graphicHistoryHour graphicHourCount graphicHourStyle graphicLayoutType graphicSelect graphicShowDiff graphicShowNight graphicShowWeather graphicSpaceSize graphicWeatherColor graphicWeatherColorNight + plantControl setupMeterDev setupInverterStrings setupRadiationAPI setupStringPeak + setupWeatherDev1 setupWeatherDev2 setupWeatherDev3 setupRoofTops ); @@ -694,6 +703,7 @@ my %hattr = ( # H setupRoofTops => { fn => \&_attrRoofTops }, flowGraphicControl => { fn => \&_attrflowGraphicControl }, aiControl => { fn => \&_attraiControl }, + plantControl => { fn => \&_attrplantControl }, ); for my $bn (1..MAXBATTERIES) { @@ -1062,20 +1072,20 @@ my %htitles = ( # Wetterintertretation # https://www.dwd.de/DE/forschung/wettervorhersage/num_modellierung/01_num_vorhersagemodelle/01c_wetterinterpretation/wetter_interpretation.pdf?__blob=publicationFile&v=7 # -# Falls keiner ww-Schlüssel >= 45 eintritt, wird das Wetter als nicht signifikant eingestuft. An -# Gitterpunkten ohne signifikantes Wetter wird der Gesamtbedeckungsgrad, verschlüsselt -# -# Wenn ferner keine der zuvor genannten Nebelbedingungen erfüllt ist, wird lediglich der -# Gesamtbedeckungsgrad verschlüsselt. +# Falls keiner ww-Schlüssel >= 45 eintritt, wird das Wetter als nicht signifikant eingestuft. An +# Gitterpunkten ohne signifikantes Wetter wird der Gesamtbedeckungsgrad, verschlüsselt +# +# Wenn ferner keine der zuvor genannten Nebelbedingungen erfüllt ist, wird lediglich der +# Gesamtbedeckungsgrad verschlüsselt. # 0.00 % - 6.25 % dann ww=0, # 6.25 % - 43.75 % dann ww=1, # 43.75 % - 81.25 % dann ww=2, # 81.25 % - 100.00 % dann ww=3, # -# ww = 00 wolkenlos -# ww = 01 leicht bewölkt -# ww = 02 wolkig -# ww = 03 stark bewölkt bis bedeckt +# ww = 00 wolkenlos +# ww = 01 leicht bewölkt +# ww = 02 wolkig +# ww = 03 stark bewölkt bis bedeckt ################################################# my %weather_ids = ( '0' => { s => '0', icon => 'weather_sun', txtd => 'wolkenloser Himmel', txte => 'cloudless sky' }, @@ -1473,13 +1483,13 @@ sub Initialize { my $srd = join ",", sort keys (%hcsr); my ($consumer, $setupbat, $ctrlbatsm, $setupprod, $setupinv, $beamcont, $beamcol, $beamfontcol, @allc); - + for my $c (1..MAXCONSUMER) { $c = sprintf "%02d", $c; $consumer .= "consumer${c}:textField-long "; push @allc, $c; } - + for my $n (1..6) { $beamcont .= "graphicBeam${n}Content "; $beamcol .= "graphicBeam${n}Color:colorpicker,RGB "; @@ -1518,9 +1528,7 @@ sub Initialize { $hash->{AttrFn} = \&Attr; $hash->{NotifyFn} = \&Notify; $hash->{ReadyFn} = \&runTask; - $hash->{AttrList} = "affectBatteryPreferredCharge:slider,0,1,100 ". - "affectConsForecastIdentWeekdays:1,0 ". - "affectConsForecastInPlanning:1,0 ". + $hash->{AttrList} = "affectConsForecastIdentWeekdays:1,0 ". "affectConsForecastLastDays:selectnumbers,1,1,180,0,lin ". "affectSolCastPercentile:select,10,50,90 ". "aiControl:textField-long ". @@ -1535,7 +1543,6 @@ sub Initialize { "ctrlLanguage:DE,EN ". "ctrlNextDayForecastReadings:multiple-strict,$hod ". "ctrlNextHoursSoCForecastReadings ". - "ctrlShowLink:1,0 ". "ctrlSolCastAPImaxReq:selectnumbers,5,5,60,0,lin ". "ctrlSolCastAPIoptimizeReq:1,0 ". "ctrlSpecialReadings:multiple-strict,$srd ". @@ -1544,6 +1551,7 @@ sub Initialize { "flowGraphicControl:textField-long ". "graphicBeamHeightLevel1 ". "graphicBeamHeightLevel2 ". + "graphicBeamHeightLevel3 ". "graphicBeamWidth:slider,20,5,100 ". "graphicEnergyUnit:Wh,kWh ". "graphicHeaderOwnspec:textField-long ". @@ -1561,6 +1569,7 @@ sub Initialize { "graphicSpaceSize ". "graphicWeatherColor:colorpicker,RGB ". "graphicWeatherColorNight:colorpicker,RGB ". + "plantControl:textField-long ". "setupInverterStrings ". "setupMeterDev:textField-long ". "setupWeatherDev1 ". @@ -1583,8 +1592,8 @@ sub Initialize { ### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !! ########################################################################################################################## - # my $av = 'obsolete#-#use#attr#flowGraphicControl#instead'; # 07.03.2025 - # $hash->{AttrList} .= " ctrlAIdataStorageDuration:$av ctrlAIshiftTrainStart:$av "; + my $av = 'obsolete#-#use#attr#plantControl#instead'; + $hash->{AttrList} .= " affectBatteryPreferredCharge:$av affectConsForecastInPlanning:$av ctrlShowLink:$av"; # 22.03.2025 ########################################################################################################################## $hash->{FW_hideDisplayName} = 1; # Forum 88667 @@ -1592,9 +1601,8 @@ sub Initialize { # $hash->{FW_addDetailToSummary} = 1; # $hash->{FW_atPageEnd} = 1; # wenn 1 -> kein Longpoll ohne informid in HTML-Tag - $hash->{AttrRenameMap} = { "ctrlBatSocManagement" => "ctrlBatSocManagement01", # 01.01.25 - "ctrlStatisticReadings" => "ctrlSpecialReadings", # 02.01.25 - }; + # $hash->{AttrRenameMap} = { "ctrlStatisticReadings" => "ctrlSpecialReadings", + # }; eval { FHEM::Meta::InitMod( __FILE__, $hash ) }; ## no critic 'eval' @@ -1673,18 +1681,6 @@ sub Set { ); my $resets = join ",",@re; - my @fcdevs = qw( OpenMeteoDWD-API - OpenMeteoDWDEnsemble-API - OpenMeteoWorld-API - SolCast-API - ForecastSolar-API - VictronKI-API - ); - - push @fcdevs, devspec2array ("TYPE=DWD_OpenData"); - - my $rdd = join ",", @fcdevs; - for my $h (@chours) { push @cfs, 'pvCorrectionFactor_'. sprintf("%02d",$h); } @@ -1740,7 +1736,7 @@ sub Set { ## KI spezifische Setter ########################## if ($ipai) { - $setlist .= "aiDecTree:addInstAndTrain,addRawData "; + $setlist .= "aiDecTree:addInstAndTrain,addRawData,rawDataGHIreplace "; } ## Batterie spezifische Setter @@ -1870,7 +1866,6 @@ return; sub _setroofIdentPair { ## no critic "not used" my $paref = shift; my $name = $paref->{name}; - my $type = $paref->{type}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; @@ -2374,6 +2369,8 @@ sub _setreset { ## no critic "not used" delete $data{$name}{current}{aitrainstate}; delete $data{$name}{current}{aiaddistate}; delete $data{$name}{current}{aigetresult}; + + delete $data{$name}{aidectree}{airaw}; my @ftd = ( $airaw.$name, $aitrained.$name @@ -2581,6 +2578,9 @@ sub _setaiDecTree { ## no critic "not used" elsif ($prop eq 'addRawData') { aiAddRawData ($paref); } + elsif ($prop eq 'rawDataGHIreplace') { + __getopenMeteoGHIreplace ($paref); + } return; } @@ -3595,7 +3595,7 @@ sub __getDWDSolarData { $data{$name}{statusapi}{DWD}{'?All'}{lastretrieval_timestamp} = $t; $data{$name}{statusapi}{DWD}{'?All'}{todayDoneAPIrequests} += 1; - my $fctime = ReadingsVal ($raname, 'fc_time', '-'); + my $fctime = ReadingsVal ($raname, 'fc_time', '-'); $data{$name}{current}{dwdRad1hDev} = $raname; $data{$name}{current}{dwdRad1hAge} = $fctime; $data{$name}{current}{dwdRad1hAgeTS} = timestringToTimestamp ($fctime); @@ -3672,10 +3672,10 @@ sub __getDWDSolarData { $af = ___areaFactorFix ($ti, $az); # Flächenfaktor: https://wiki.fhem.de/wiki/Ertragsprognose_PV $pv = $rad * $af * KJ2KWH * $peak * PRDEF; # Rad wird in kW/m2 erwartet } - + $af = sprintf "%.2f", $af; $pv = sprintf "%.1f", $pv; - + $data{$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, AF: $af, dirfac: $sdr"); @@ -4262,20 +4262,19 @@ sub __getopenMeteoData { } } - debugLog ($paref, 'apiCall', qq{Open-Meteo API Call - the daily API requests -> limited to: OMETMAXREQ, done: $donearq}); + debugLog ($paref, 'apiCall', "Open-Meteo API Call - the daily API requests -> limited to: ".OMETMAXREQ.", done: $donearq"); - my $submodel = InternalVal ($hash->{NAME}, $reqm, ''); - - $paref->{allstrings} = AttrVal ($name, 'setupInverterStrings', ''); - $paref->{submodel} = $submodel eq 'OpenMeteoDWDAPI' ? 'DWD ICON Seamless' : - $submodel eq 'OpenMeteoDWDEnsembleAPI' ? 'DWD ICON Seamless Ensemble' : - $submodel eq 'OpenMeteoWorldAPI' ? 'World Best Match' : - 'unknown'; - - return "The Weather Model '$submodel' is not a valid Open-Meteo Weather Model" if($paref->{submodel} eq 'unknown'); + my $submodel = InternalVal ($name, $reqm, 'unknown'); + my $allstrings = AttrVal ($name, 'setupInverterStrings', ''); + + #if ($reqm eq 'MODEL' && $submodel eq 'OpenMeteoDWDD2API') { # Satellitenunterstützung dazuladen + # $allstrings .= ',SatelliteRadiation,'.$allstrings; + #} $paref->{callequivalent} = $submodel eq 'OpenMeteoDWDEnsembleAPI' ? 20 : 1; + $paref->{allstrings} = $allstrings; $paref->{begin} = 1; + $paref->{submodel} = $submodel; $paref->{requestmode} = $reqm; __openMeteoDWD_ApiRequest ($paref); @@ -4283,8 +4282,58 @@ sub __getopenMeteoData { return; } +################################################################################################ +# historische GHI Daten von OpenMeteo abrufen und in aiRawData ersetzen +################################################################################################ +sub __getopenMeteoGHIreplace { + my $paref = shift; + my $name = $paref->{name}; + my $lang = $paref->{lang}; + my $debug = $paref->{debug}; + + my $hash = $defs{$name}; + my $donearq = StatusAPIVal ($hash, 'OpenMeteo', '?All', 'todayDoneAPIrequests', 0); + + if ($donearq >= OMETMAXREQ) { + my $msg = "The limit of maximum OMETMAXREQ daily API requests is reached or already exceeded. Process is exited."; + Log3 ($name, 1, "$name - ERROR - $msg"); + return $msg; + } + + my $nk = scalar keys %{$data{$name}{aidectree}{airaw}}; + return if(!$nk); + + my @ha = sort keys %{$data{$name}{aidectree}{airaw}}; + my $fstidx = $ha[0]; + my $lstidx = $ha[$nk-1]; + + my $fsty = substr $fstidx, 0, 4; + my $fstm = substr $fstidx, 4, 2; + my $fstd = substr $fstidx, 6, 2; + + my $lsty = substr $lstidx, 0, 4; + my $lstm = substr $lstidx, 4, 2; + my $lstd = substr $lstidx, 6, 2; + + debugLog ($paref, 'apiCall', "Open-Meteo API Call - the daily API requests -> limited to: ".OMETMAXREQ.", done: $donearq"); + debugLog ($paref, 'apiCall', "Open-Meteo API Call - Refill Global Horizontal Irradiance (GHI) - Start: $fsty-$fstm-$fstd, End: $lsty-$lstm-$lstd"); + + $paref->{callequivalent} = 1; + $paref->{allstrings} = 'Dummy'; + $paref->{begin} = 1; + $paref->{submodel} = 'HistoricalData'; + $paref->{requestmode} = 'GHIREFILL'; + $paref->{t} = int time; + $paref->{startdate} = "$fsty-$fstm-$fstd"; + $paref->{enddate} = "$lsty-$lstm-$lstd"; + + __openMeteoDWD_ApiRequest ($paref); + +return; +} + ######################################################################################################################## -# Open-Meteo DWD ICON API Request +# Open-Meteo API Request # Open data weather forecasts from the German weather service DWD # Quelle Seite: https://open-meteo.com/ # @@ -4307,19 +4356,18 @@ return; # ######################################################################################################################## sub __openMeteoDWD_ApiRequest { - my $paref = shift; - my $name = $paref->{name}; - my $type = $paref->{type}; - my $allstrings = $paref->{allstrings}; # alle Strings - my $debug = $paref->{debug}; - my $lang = $paref->{lang}; - my $t = $paref->{t} // int time; - my $submodel = $paref->{submodel}; # abzufragendes Datenmodell + my $paref = shift; + my $name = $paref->{name}; + my $allstrings = $paref->{allstrings}; # alle Strings + my $debug = $paref->{debug}; + my $lang = $paref->{lang}; + my $t = $paref->{t} // int time; + my $submodel = $paref->{submodel}; # abzufragendes Datenmodell my $requestmode = $paref->{requestmode}; - my $hash = $defs{$name}; + my $hash = $defs{$name}; - if (!$allstrings) { # alle Strings wurden abgerufen + if (!$allstrings) { # alle Strings wurden abgerufen my $apiitv = StatusAPIVal ($hash, 'OpenMeteo', '?All', 'currentAPIinterval', OMETEOREPDEF); readingsSingleUpdate ($hash, 'nextRadiationAPICall', $hqtxt{after}{$lang}.' '.(timestampToTimestring ($t + $apiitv, $lang))[0], 1); @@ -4329,51 +4377,36 @@ sub __openMeteoDWD_ApiRequest { } my ($string, $err); - ($string, $allstrings) = split ",", $allstrings, 2; - my ($set, $lat, $lon, $elev) = locCoordinates(); + ($string, $allstrings) = split ",", $allstrings, 2; + + if ($string eq 'SatelliteRadiation') { # Trenner-String: ab jetzt Satelliten Abfrage + $submodel = 'SatelliteRadiation'; + $paref->{submodel} = $submodel; + ($string, $allstrings) = split ",", $allstrings, 2; + } - if (!$set) { - $err = qq{ERROR - the attribute 'latitude' and/or 'longitude' in global device is not set}; + $paref->{string} = $string; + my $url = ___createOpenMeteoURL ($paref); + delete $paref->{string}; + + if (!$url) { + $err = qq{ERROR OpenMeteo API Call - no URL could be created for submodel: $submodel}; Log3 ($name, 1, "$name - $err"); return; } - my $tilt = StringVal ($hash, $string, 'tilt', ''); - my $az = StringVal ($hash, $string, 'azimut', ''); + my $deburl = $url; + $deburl =~ s/&/&/g; - if ($requestmode eq 'WEATHERMODEL' && $string eq 'KI-based') {$tilt = 0; $az = 0;} # Dummy Settings - - if ($tilt eq '' || $az eq '') { - $err = qq{ERROR OpenMeteo API Call - the reading 'setupStringAzimuth' and/or 'setupStringDeclination' is not set}; - Log3 ($name, 1, "$name - $err"); - return; - } - - my $url = "https://api.open-meteo.com/v1/forecast?"; - $url = "https://ensemble-api.open-meteo.com/v1/ensemble?" if($submodel =~ /Ensemble/xs); # Ensemble Modell gewählt - $url .= "models=icon_seamless" if($submodel =~ /DWD\sICON\sSeamless/xs); - $url .= "models=best_match" if($submodel eq 'World Best Match'); - $url .= "&latitude=".$lat; - $url .= "&longitude=".$lon; - $url .= "&hourly=temperature_2m,rain,weather_code,cloud_cover,is_day,global_tilted_irradiance"; - $url .= "¤t=temperature_2m,weather_code,cloud_cover" if($submodel !~ /Ensemble/xs); - $url .= "&minutely_15=global_tilted_irradiance" if($submodel !~ /Ensemble/xs); - $url .= "&daily=sunrise,sunset" if($submodel !~ /Ensemble/xs); - $url .= "&forecast_hours=48"; - $url .= "&forecast_days=2"; - $url .= "&tilt=".$tilt; - $url .= "&azimuth=".$az; - - debugLog ($paref, 'apiCall', qq{Open-Meteo API Call - Request for PV-String "$string" with Data Model >$submodel<:\n$url}); + debugLog ($paref, 'apiCall', qq{Open-Meteo API Call - Request for PV-String "$string" with Data Model >$submodel<:\n$deburl}); debugLog ($paref, 'apiCall|apiProcess', qq{Open-Meteo API Call - Request Mode: $requestmode}); - my $caller = (caller(0))[3]; # Rücksprungmarke + my $caller = (caller(0))[3]; # Rücksprungmarke my $param = { url => $url, timeout => 30, name => $name, - type => $paref->{type}, debug => $debug, header => 'Accept: application/json', submodel => $submodel, @@ -4399,7 +4432,7 @@ return; } ################################################################################################ -# Open-Meteo DWD ICON API Response +# Open-Meteo API Response # # Rad1h vom DWD - Globalstrahlung in kJ/m2 # @@ -4424,12 +4457,11 @@ sub __openMeteoDWD_ApiResponse { my $myjson = shift; my $name = $paref->{name}; - my $type = $paref->{type}; my $caller = $paref->{caller}; my $string = $paref->{string}; my $allstrings = $paref->{allstrings}; - my $requestmode = $paref->{requestmode}; # MODEL oder WEATHERMODEL - my $stc = $paref->{stc}; # Startzeit API Abruf + my $requestmode = $paref->{requestmode}; # MODEL / WEATHERMODEL / GHIREFILL + my $stc = $paref->{stc}; # Startzeit API Abruf my $lang = $paref->{lang}; my $debug = $paref->{debug}; my $submodel = $paref->{submodel}; @@ -4437,12 +4469,13 @@ sub __openMeteoDWD_ApiResponse { my $hash = $defs{$name}; my $t = int time; my $sta = [gettimeofday]; # Start Response Verarbeitung + my $nghi = 0; $paref->{t} = $t; my $msg; if ($err ne "") { - $msg = 'Open-Meteo DWD ICON API server response: '.$err; + $msg = 'Open-Meteo API server response: '.$err; Log3 ($name, 1, "$name - $msg"); @@ -4458,7 +4491,7 @@ sub __openMeteoDWD_ApiResponse { my ($success) = evaljson ($hash, $myjson); if (!$success) { - $msg = 'ERROR - invalid Open-Meteo DWD ICON API server response'; + $msg = 'ERROR - invalid Open-Meteo API server response'; Log3 ($name, 1, "$name - $msg"); @@ -4481,7 +4514,7 @@ sub __openMeteoDWD_ApiResponse { # reason: if ($jdata->{'error'}) { - $msg = "Open-Meteo DWD ICON API server ERROR response: ".$jdata->{'reason'}; + $msg = "Open-Meteo API server ERROR response: ".$jdata->{'reason'}; Log3 ($name, 3, "$name - $msg"); @@ -4507,24 +4540,27 @@ sub __openMeteoDWD_ApiResponse { my $peak = StringVal ($hash, $string, 'peak', 0); # String Peak (kWp) $peak *= 1000; # kWp in Wp - ## Akt. Werte - ################# - my ($curwid, $curwcc, $curtmp, $curstr); + ## aktuelle Wetterdaten + ######################### + my ($curwid, $currain, $curwcc, $curtmp, $curstr); - if (defined $jdata->{current}{time}) { - ($err, $curstr) = timestringUTCtoLocal ($name, $jdata->{current}{time}, '%Y-%m-%dT%H:%M'); + if ($submodel ne 'SatelliteRadiation') { + if (defined $jdata->{current}{time}) { + ($err, $curstr) = timestringUTCtoLocal ($name, $jdata->{current}{time}, '%Y-%m-%dT%H:%M'); - if ($err) { - $msg = 'ERROR - Open-Meteo invalid time conversion: '.$err; - Log3 ($name, 1, "$name - $msg"); - singleUpdateState ( {hash => $hash, state => $err, evt => 1} ); - return; - } + if ($err) { + $msg = 'ERROR - Open-Meteo invalid time conversion: '.$err; + Log3 ($name, 1, "$name - $msg"); + singleUpdateState ( {hash => $hash, state => $err, evt => 1} ); + return; + } - $curwid = $jdata->{current}{weather_code}; - $curwcc = $jdata->{current}{cloud_cover}; - $curtmp = $jdata->{current}{temperature_2m}; - } + $curwid = $jdata->{current}{weather_code}; + $curwcc = $jdata->{current}{cloud_cover}; + $currain = $jdata->{current}{rain}; + $curtmp = $jdata->{current}{temperature_2m}; + } + } ## Stundenwerte ################# @@ -4541,124 +4577,191 @@ sub __openMeteoDWD_ApiResponse { } my $ots = timestringToTimestamp ($otmstr); - my $pvtmstr = (timestampToTimestring ($ots-3600))[0]; # Strahlung wird als Durchschnitt der !vorangegangenen! Stunde geliefert! + my $pvtmstr = (timestampToTimestring ($ots - 3600))[0]; # Strahlung wird als Durchschnitt der !vorangegangenen! Stunde geliefert! - if (timestringToTimestamp($pvtmstr) < $refts) { + if (timestringToTimestamp ($pvtmstr) < $refts && $submodel ne 'HistoricalData') { $k++; - next; # Daten älter als akt. Tag 00:00:00 verwerfen + next; # Daten älter als akt. Tag 00:00:00 verwerfen } - my $rad1wh = $jdata->{hourly}{global_tilted_irradiance}[$k]; # Wh/m2 - my $rad = 10 * (sprintf "%.0f", ($rad1wh * WH2KJ) / 10); # Umrechnung Wh/m2 in kJ/m2 -> - my $pv = sprintf "%.2f", int ($rad1wh / 1000 * $peak * PRDEF); # Rad wird in kWh/m2 erwartet - - my $don = $jdata->{hourly}{is_day}[$k]; - my $temp = $jdata->{hourly}{temperature_2m}[$k]; - my $rain = $jdata->{hourly}{rain}[$k]; # Regen in Millimeter = kg/m2 - my $wid = $jdata->{hourly}{weather_code}[$k]; - my $wcc = $jdata->{hourly}{cloud_cover}[$k]; - - if ($debug =~ /apiProcess/xs) { + ## Strahlungsdaten + #################### + if ($submodel ne 'HistoricalData') { if ($requestmode eq 'MODEL') { - Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $pvtmstr - Rad1Wh: $rad1wh, Rad1kJ: $rad, PV est: $pv Wh"); - } + my $gtiwh = $jdata->{hourly}{global_tilted_irradiance}[$k]; # GTI in Wh/m2 + my $radwh = $jdata->{hourly}{shortwave_radiation}[$k]; # Solarstrahlung GHI + + if (defined $radwh) { # Globalstrahlung für KI + my $radkJ = 10 * (sprintf "%.0f", ($radwh * WH2KJ) / 10); # Umrechnung Wh/m2 in kJ/m2 + $data{$name}{solcastapi}{'?All'}{$pvtmstr}{Rad1h} = $radkJ; # Startstunde verschieben + + debugLog ($paref, 'apiProcess', "Open-Meteo API $pvtmstr - Global Radiation Rad1h: $radkJ"); + } + + if (defined $gtiwh) { # Global Tilted Radiation - zur PV Berechnung jedes Strings + my $pv = sprintf "%.2f", int ($gtiwh / 1000 * $peak * PRDEF); # GTI wird in kWh/m2 genutzt - Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $pvtmstr - RR1c: $rain"); - Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $otmstr - DoN: $don"); - Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $otmstr - Temp: $temp"); - Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $otmstr - Weather Code: $wid"); - Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $otmstr - Cloud Cover: $wcc"); - - if ($k == 0) { - Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $otmstr - current Temp: $curtmp") if(defined $curtmp); - Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $curstr - current Weather Code: $curwid") if(defined $curwid); - Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API $curstr - current Cloud Cover: $curwcc") if(defined $curwcc); + $data{$name}{solcastapi}{$string}{$pvtmstr}{pv_estimate50} = $pv; # Startstunde verschieben + $data{$name}{solcastapi}{$string}{$pvtmstr}{GTIWh} = $gtiwh; # Startstunde verschieben, GTI wird für jeden String separat geliefert + + debugLog ($paref, 'apiProcess', "Open-Meteo API $pvtmstr - GTIWh: $gtiwh, PV estimate: $pv Wh"); + } } } - - if ($requestmode eq 'MODEL') { - $data{$name}{solcastapi}{$string}{$pvtmstr}{pv_estimate50} = $pv; # Startstunde verschieben - - if ($paref->{begin}) { # im ersten Call den DS löschen -> dann Aufsummierung - delete $data{$name}{solcastapi}{'?All'}{$pvtmstr}{Rad1h}; + + ## Refill GHI Strahlungsdaten + ############################### + if ($submodel eq 'HistoricalData') { + if ($requestmode eq 'GHIREFILL') { + my $srwh = $jdata->{hourly}{shortwave_radiation}[$k]; # Solarstrahlung GHI + + if ($srwh) { # Globalstrahlung für KI + my $ghikj = 10 * (sprintf "%.0f", ($srwh * WH2KJ) / 10); # Umrechnung Wh/m2 in kJ/m2 + + $pvtmstr =~ /^(\d{4})-(\d{2})-(\d{2})\s(\d{2})/xs; + my $tidx = $1.$2.$3.(sprintf "%02d", ($4 + 1)); + + if (AiRawdataVal ($name, $tidx, 'rad1h', 0)) { + $data{$name}{aidectree}{airaw}{$tidx}{rad1h} = $ghikj; + $nghi++; + debugLog ($paref, 'apiProcess', "Open-Meteo API Index $tidx - Global Radiation GHI replaced: $ghikj"); + } + } } - - $data{$name}{solcastapi}{'?All'}{$pvtmstr}{Rad1h} += $rad; # Startstunde verschieben, Rad Werte aller Strings addieren } ## Wetterdaten ################ - my $fwtg = formatWeatherTimestrg ($pvtmstr); # Zeit gemäß DWD_OpenData-Format + if ($submodel ne 'SatelliteRadiation' && $submodel ne 'HistoricalData') { + my $don = $jdata->{hourly}{is_day}[$k]; + my $temp = $jdata->{hourly}{temperature_2m}[$k]; + my $rain = $jdata->{hourly}{rain}[$k]; # Regen in Millimeter = kg/m2 + my $wid = $jdata->{hourly}{weather_code}[$k]; + my $wcc = $jdata->{hourly}{cloud_cover}[$k]; - $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{rr1c} = $rain; - $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{StartTime} = $pvtmstr; + my $fwtg = formatWeatherTimestrg ($pvtmstr); # Zeit gemäß DWD_OpenData-Format - $fwtg = formatWeatherTimestrg ($otmstr); # Zeit gemäß DWD_OpenData-Format + $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{rr1c} = $rain if(defined $rain); + $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{StartTime} = $pvtmstr; - $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{don} = $don; - $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{neff} = $wcc; - $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{ww} = $wid; - $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{ttt} = $temp; - $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{UpdateTime} = $rt; - $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{StartTime} = $otmstr; + $fwtg = formatWeatherTimestrg ($otmstr); # Zeit gemäß DWD_OpenData-Format - if ($k == 0) { - $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{neff} = $curwcc if(defined $curwcc); - $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{ww} = $curwid if(defined $curwid); - $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{ttt} = $curtmp if(defined $curtmp); - } + $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{don} = $don if(defined $don); + $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{neff} = $wcc if(defined $wcc); + $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{ww} = $wid if(defined $wid); + $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{ttt} = $temp if(defined $temp); + $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{UpdateTime} = $rt; + $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{StartTime} = $otmstr; - $k++; - } - - ## Tageswerte - ############### - $k = 0; - - while ($jdata->{daily}{time}[$k]) { - my $oday = $jdata->{daily}{time}[$k]; - - ($err, my $sunrise) = timestringUTCtoLocal ($name, $jdata->{daily}{sunrise}[$k], '%Y-%m-%dT%H:%M'); - ($err, my $sunset) = timestringUTCtoLocal ($name, $jdata->{daily}{sunset}[$k], '%Y-%m-%dT%H:%M'); - - if ($err) { - $msg = 'ERROR - Open-Meteo invalid time conversion: '.$err; - Log3 ($name, 1, "$name - $msg"); - singleUpdateState ( {hash => $hash, state => $err, evt => 1} ); - return; - } - - if ($k == 0) { - $data{$name}{weatherapi}{OpenMeteo}{sunrise}{today} = $sunrise; - $data{$name}{weatherapi}{OpenMeteo}{sunset}{today} = $sunset; - - if ($debug =~ /apiProcess/xs) { - Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API - Sunrise Today: $sunrise"); - Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API - SunSet Today: $sunset"); + if ($k == 0) { + $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{neff} = $curwcc if(defined $curwcc); + $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{ww} = $curwid if(defined $curwid); + $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{ttt} = $curtmp if(defined $curtmp); + $data{$name}{weatherapi}{OpenMeteo}{$fwtg}{rr1c} = $currain if(defined $currain); } - } - - if ($k == 1) { - $data{$name}{weatherapi}{OpenMeteo}{sunrise}{tomorrow} = $sunrise; - $data{$name}{weatherapi}{OpenMeteo}{sunset}{tomorrow} = $sunset; if ($debug =~ /apiProcess/xs) { - Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API - Sunrise Tomorrow: $sunrise"); - Log3 ($name, 1, "$name DEBUG> Open-Meteo DWD ICON API - SunSet Tomorrow: $sunset"); + Log3 ($name, 1, "$name DEBUG> Open-Meteo API $pvtmstr - RR1c: ". (defined $rain ? $rain : '-')); + Log3 ($name, 1, "$name DEBUG> Open-Meteo API $otmstr - DoN: ". (defined $don ? $don : '-')); + Log3 ($name, 1, "$name DEBUG> Open-Meteo API $otmstr - Temp: ". (defined $temp ? $temp : '-')); + Log3 ($name, 1, "$name DEBUG> Open-Meteo API $otmstr - Weather Code: ".(defined $wid ? $wid : '-')); + Log3 ($name, 1, "$name DEBUG> Open-Meteo API $otmstr - Cloud Cover: ". (defined $wcc ? $wcc : '-')); + + if ($curstr && $k == 0) { + Log3 ($name, 1, "$name DEBUG> Open-Meteo API $curstr - current Temp: ". (defined $curtmp ? $curtmp : '-')); + Log3 ($name, 1, "$name DEBUG> Open-Meteo API $curstr - current Weather Code: ".(defined $curwid ? $curwid : '-')); + Log3 ($name, 1, "$name DEBUG> Open-Meteo API $curstr - current Cloud Cover: ". (defined $curwcc ? $curwcc : '-')); + Log3 ($name, 1, "$name DEBUG> Open-Meteo API $curstr - current Rain: ". (defined $currain ? $currain : '-')); + } } } $k++; } + + ## Tageswerte (Sonnenauf- und untergang) + ########################################## + if ($submodel ne 'SatelliteRadiation') { + $k = 0; + + while ($jdata->{daily}{time}[$k]) { + my $oday = $jdata->{daily}{time}[$k]; + + ($err, my $sunrise) = timestringUTCtoLocal ($name, $jdata->{daily}{sunrise}[$k], '%Y-%m-%dT%H:%M'); + ($err, my $sunset) = timestringUTCtoLocal ($name, $jdata->{daily}{sunset}[$k], '%Y-%m-%dT%H:%M'); + + if ($err) { + $msg = 'ERROR - Open-Meteo invalid time conversion: '.$err; + Log3 ($name, 1, "$name - $msg"); + singleUpdateState ( {hash => $hash, state => $err, evt => 1} ); + return; + } + + if ($k == 0) { + $data{$name}{weatherapi}{OpenMeteo}{sunrise}{today} = $sunrise; + $data{$name}{weatherapi}{OpenMeteo}{sunset}{today} = $sunset; + + if ($debug =~ /apiProcess/xs) { + Log3 ($name, 1, "$name DEBUG> Open-Meteo API - Sunrise Today: $sunrise"); + Log3 ($name, 1, "$name DEBUG> Open-Meteo API - SunSet Today: $sunset"); + } + } + + if ($k == 1) { + $data{$name}{weatherapi}{OpenMeteo}{sunrise}{tomorrow} = $sunrise; + $data{$name}{weatherapi}{OpenMeteo}{sunset}{tomorrow} = $sunset; + + if ($debug =~ /apiProcess/xs) { + Log3 ($name, 1, "$name DEBUG> Open-Meteo API - Sunrise Tomorrow: $sunrise"); + Log3 ($name, 1, "$name DEBUG> Open-Meteo API - SunSet Tomorrow: $sunset"); + } + } + + $k++; + } + } + + ## 15 Minuten Werte + ##################### + if ($requestmode eq 'MODEL' && $submodel ne 'SatelliteRadiation') { + $paref->{jdata} = $jdata; + $paref->{indicator} = 'global_tilted_irradiance'; + + my $haggr = ___15Minutes2HourAggregator ($paref); # 15 Minuten zu 1h Aggregation + + if ($haggr) { + for my $tmstr (sort keys %{$haggr->{hourly}}) { + my $gtiwh = $haggr->{hourly}{$tmstr}{$paref->{indicator}}; + my $pv = sprintf "%.2f", int ($gtiwh / 1000 * $peak * PRDEF); + + #$data{$name}{solcastapi}{$string}{$tmstr}{GTIWh} = $gtiwh; + #$data{$name}{solcastapi}{$string}{$tmstr}{pv_estimate50} = $pv; + + #debugLog ($paref, 'apiProcess', "Open-Meteo API - do 15 min Aggr $tmstr - GTIWh: $gtiwh, PV estimate: $pv Wh"); + } + } + + delete $paref->{indicator}; + delete $paref->{jdata}; + } } ___setOpenMeteoAPIcallKeyData ($paref); - Log3 ($name, 4, qq{$name - Open-Meteo DWD ICON API answer received for string "$string"}); + if ($nghi) { + $err = writeCacheToFile ($hash, 'airaw', $airaw.$name); + + if (!$err) { + $data{$name}{current}{aitrawstate} = 'ok'; + Log3 ($name, 3, qq{$name - aiRawData -> number of datasets replaced rad1h: $nghi}); + debugLog ($paref, 'aiProcess', "AI raw data saved into file: ".$airaw.$name); + } + } + + Log3 ($name, 4, qq{$name - Open-Meteo API answer received for string "$string"}); my $param = { name => $name, - type => $type, debug => $debug, allstrings => $allstrings, submodel => $submodel, @@ -4673,6 +4776,149 @@ sub __openMeteoDWD_ApiResponse { return &$caller($param); } +################################################################ +# OpenMeteo URL zusammenstellen +################################################################ +sub ___createOpenMeteoURL { + my $paref = shift; + my $name = $paref->{name}; + my $submodel = $paref->{submodel}; # abzufragendes Datenmodell + my $requestmode = $paref->{requestmode}; + my $string = $paref->{string}; + + my $err; + my ($set, $lat, $lon, $elev) = locCoordinates(); + + if (!$set) { + $err = qq{ERROR - the attribute 'latitude' and/or 'longitude' in global device is not set}; + Log3 ($name, 1, "$name - $err"); + return; + } + + my $tilt = StringVal ($name, $string, 'tilt', ''); + my $az = StringVal ($name, $string, 'azimut', ''); + + if ($requestmode eq 'WEATHERMODEL' && $string eq 'KI-based') {$tilt = 0; $az = 0;} # Dummy Settings + if ($requestmode eq 'GHIREFILL' && $string eq 'Dummy') {$tilt = 0; $az = 0;} # Dummy Settings + + if ($tilt eq '' || $az eq '') { + $err = qq{ERROR OpenMeteo API Call - the reading 'setupStringAzimuth' and/or 'setupStringDeclination' is not set}; + Log3 ($name, 1, "$name - $err"); + return; + } + + my $url; + + if ($submodel eq 'OpenMeteoDWDEnsembleAPI') { # Ensemble Modell gewählt + $url = "https://ensemble-api.open-meteo.com/v1/ensemble?"; + $url .= "&latitude=".$lat; + $url .= "&longitude=".$lon; + $url .= "&hourly=temperature_2m,rain,weather_code,cloud_cover,is_day,global_tilted_irradiance,shortwave_radiation"; + $url .= "&forecast_hours=48"; + $url .= "&forecast_days=2"; + $url .= "&tilt=".$tilt; + $url .= "&azimuth=".$az; + } + + if ($submodel eq 'OpenMeteoDWDAPI' || $submodel eq 'OpenMeteoDWDD2API' || $submodel eq 'OpenMeteoWorldAPI') { + $url = "https://api.open-meteo.com/v1/forecast?"; + $url .= "models=icon_seamless" if($submodel eq 'OpenMeteoDWDAPI'); + $url .= "models=icon_d2" if($submodel eq 'OpenMeteoDWDD2API'); + $url .= "models=best_match" if($submodel eq 'OpenMeteoWorldAPI'); + $url .= "&latitude=".$lat; + $url .= "&longitude=".$lon; + $url .= "&hourly=temperature_2m,rain,weather_code,cloud_cover,is_day,global_tilted_irradiance,shortwave_radiation"; + $url .= "¤t=temperature_2m,weather_code,rain,cloud_cover"; + $url .= "&minutely_15=rain,global_tilted_irradiance,shortwave_radiation"; + $url .= "&daily=sunrise,sunset"; + $url .= "&forecast_hours=48"; + $url .= "&forecast_days=2"; + $url .= "&tilt=".$tilt; + $url .= "&azimuth=".$az; + } + + if ($submodel eq 'SatelliteRadiation') { + $url = "https://satellite-api.open-meteo.com/v1/archive?"; + $url .= "models=satellite_radiation_seamless"; + $url .= "&latitude=".$lat; + $url .= "&longitude=".$lon; + $url .= "&hourly=global_tilted_irradiance"; + $url .= "&forecast_hours=24"; + $url .= "&utm_source=substack"; + $url .= "&utm_medium=email"; + $url .= "&tilt=".$tilt; + $url .= "&azimuth=".$az; + } + + if ($submodel eq 'HistoricalData') { + $url = "https://archive-api.open-meteo.com/v1/archive?"; + $url .= "latitude=".$lat; + $url .= "&longitude=".$lon; + $url .= "&hourly=shortwave_radiation"; + $url .= "&start_date=".$paref->{startdate}; + $url .= "&end_date=".$paref->{enddate}; + } + +return $url; +} + +################################################################ +# 15-Minuten Werte auf eine Stunde aggregieren +# Return: Referenz auf Ergebnishash +################################################################ +sub ___15Minutes2HourAggregator { + my $paref = shift; + my $name = $paref->{name}; + my $jdata = $paref->{jdata}; + my $indicator = $paref->{indicator}; # der zu bearbeitende Indikator + + return if(!$jdata || !$indicator); + + my ($haggr, $nct, $lzstr); + my $k = 0; + + while ($jdata->{minutely_15}{time}[$k]) { + my $tstr = $jdata->{minutely_15}{time}[$k]; + + $lzstr = $jdata->{minutely_15}{time}[$k] if($tstr =~ /T..:00/xs); + + if (!$lzstr) { + $k++; + next; + } + + if ($tstr =~ /T..:15/xs) { + $nct = $lzstr; + } + + if (!$nct) { + $k++; + next; + } + + $haggr->{hourly}{$nct}{$indicator} = $jdata->{minutely_15}{$indicator}[$k]; + + $k++; + } + + if ($haggr) { + for my $key (sort keys %{$haggr->{hourly}}) { + my ($err, $otmstr) = timestringUTCtoLocal ($name, $key, '%Y-%m-%dT%H:%M'); + + if ($err) { + Log3 ($name, 1, "$name - ERROR - Open-Meteo invalid time conversion in Aggregator: $err"); + delete $haggr->{hourly}{$key}{$indicator}; + next; + } + + $haggr->{hourly}{$otmstr}{$indicator} = sprintf "%.0f", $haggr->{hourly}{$key}{$indicator}; + delete $haggr->{hourly}{$key}; + } + } + +return $haggr; +} + ################################################################ # Kennzahlen aus letzten Open-Meteo Request ableiten ################################################################ @@ -5363,21 +5609,21 @@ sub __getaiRuleStrings { ## no critic "not used" my $rs = 'no rules delivered'; my (@rsl, %entities); my $tn = 0; - + for my $dtree (@{$objref}) { eval { my @rules = $dtree->rule_statements(); # Returns a list of strings that describe the tree in rule-form $tn++; - + if ($tn == 1) { # nur den ersten Tree ausgeben push @rsl, ' '; push @rsl, 'Tree: '.$tn; push @rsl, ' '; - push @rsl, @rules; + push @rsl, @rules; } - + $entities{$tn}{rules} = scalar @rules; # Anzahl der Regeln $entities{$tn}{nodes} = $dtree->nodes(); # Returns the number of nodes in the trained decision tree - $entities{$tn}{depth} = $dtree->depth(); # Returns the depth of the tree. This is the maximum number of decisions that would need to be made to classify an unseen instance, i.e. the length of the longest path from the tree's root to a leaf. + $entities{$tn}{depth} = $dtree->depth(); # Returns the depth of the tree. This is the maximum number of decisions that would need to be made to classify an unseen instance, i.e. the length of the longest path from the tree's root to a leaf. 1; } or do { return $@; @@ -5387,18 +5633,18 @@ sub __getaiRuleStrings { ## no critic "not used" my $atf = CircularVal ($hash, 99, 'aitrainLastFinishTs', 0); $atf = ''.$hqtxt{ailatr}{$lang}.' '.($atf ? (timestampToTimestring ($atf, $lang))[0] : '-'); my $art = $hqtxt{aitris}{$lang}.' '.CircularVal ($hash, 99, 'runTimeTrainAI', '-'); - + my $agt = CurrentVal ($hash, 'aiLastGetResultTime', ''); $agt = ''.$hqtxt{ailgrt}{$lang}.' '.($agt ? ($agt * 1000).' ms' : '-'); if (@rsl) { my $l = scalar @rsl; $rs = "Trained AI Object contains an Ensemble of $tn trees (only the first Tree is printed out)\n\n"; - + for my $tree (1..$tn) { $rs .= "Tree: $tree -> Number of Rules: $entities{$tree}{rules} / Number of Nodes: $entities{$tree}{nodes} / Depth: $entities{$tree}{depth} \n"; } - + $rs .= "\n\n"; $rs .= "Rules: ".$hqtxt{airule}{$lang}."\n"; $rs .= "Nodes: ".$hqtxt{ainode}{$lang}."\n"; @@ -5627,17 +5873,16 @@ sub Attr { ### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !! ###################################################################################################################### - #if ($cmd eq 'set' && $aName =~ /^ctrlAIdataStorageDuration|ctrlAIshiftTrainStart$/) { - # #my $msg = "The attribute $aName is obsolete and will be deleted soon. Please save your Configuration."; - # my $msg = "The attribute $aName is replaced by 'aiControl'. Please press 'save config' when restart is finished."; - # if (!$init_done) { - # Log3 ($name, 1, "$name - $msg"); - # #return qq{Device "$name" -> $msg}; - # } - # else { - # return $msg; - # } - #} + if ($cmd eq 'set' && $aName =~ /^affectBatteryPreferredCharge|affectConsForecastInPlanning|ctrlShowLink$/) { # 22.03.2025 + #my $msg = "The attribute $aName is obsolete and will be deleted soon. Please press 'save config' when restart is finished."; + my $msg = "The attribute $aName is replaced by 'plantControl'."; + if (!$init_done) { + Log3 ($name, 1, "$name - $msg"); + } + else { + return $msg; + } + } ###################################################################################################################### if ($aName eq 'disable') { @@ -5872,9 +6117,9 @@ sub _attrconsumer { ## no critic "not used" return "swstate off-Regex: $err" if($err); } - if (exists $h->{mintime}) { + if (exists $h->{mintime}) { my $mintime = $h->{mintime}; - + if ($mintime !~ /^SunPath/xsi && $mintime =~ /.*:.*/xs) { my ($dv, $rd) = split ':', $mintime; ($err) = isDeviceValid ( { name => $name, obj => $dv, method => 'string' } ); @@ -6059,8 +6304,8 @@ sub _attraiControl { ## no critic "not used" my $hash = $defs{$name}; - for my $av ( qw( aiStorageDuration - aiTrainStart + for my $av ( qw( aiStorageDuration + aiTrainStart aiTreesPV ) ) { @@ -6092,6 +6337,52 @@ sub _attraiControl { ## no critic "not used" return; } +################################################################ +# Attr plantControl +################################################################ +sub _attrplantControl { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $aVal = $paref->{aVal}; + my $cmd = $paref->{cmd}; + + my $hash = $defs{$name}; + + for my $av ( qw( batteryPreferredCharge + consForecastInPlanning + feedinPowerLimit + showLink + ) ) { + + delete $data{$name}{current}{$av}; + } + + if ($cmd eq 'set') { + my $valid = { + batteryPreferredCharge => '([0-9]|[1-9][0-9]|100)', + consForecastInPlanning => '0|1', + feedinPowerLimit => '\d+', + showLink => '0|1', + }; + + my ($a, $h) = parseParams ($aVal); + + for my $key (keys %{$h}) { + my $comp = $valid->{$key}; + next if(!$comp); + + if ($h->{$key} =~ /^$comp$/xs) { + $data{$name}{current}{$key} = $h->{$key}; + } + else { + return "The key '$key=$h->{$key}' is not specified correctly. Please refer to the command reference."; + } + } + } + +return; +} + ################################################################ # Attr setupMeterDev ################################################################ @@ -6422,11 +6713,11 @@ sub _attrBatteryDev { ## no critic "not used" if (!$h->{pin} || !$h->{pout} || !$h->{cap}) { return qq{One or more of the keys 'pin, pout, cap' are missing. Please note the command reference.}; } - + if ($h->{show} && $h->{show} =~ /:/xs) { my ($show, $pos) = split ':', $h->{show}; $pos //= 'xx'; - + if ($pos !~ /^top|bottom$/xs) { return qq{The key 'show' is not set correctly. Please note the command reference.}; } @@ -6538,15 +6829,6 @@ sub _attrRadiationAPI { ## no critic "not used" return qq{The device "$aVal" doesn't exist or has no TYPE "DWD_OpenData"}; } - #my $awdev1 = AttrVal ($name, 'setupWeatherDev1', ''); - - #if (($awdev1 eq 'OpenMeteoDWD-API' && $aVal ne 'OpenMeteoDWD-API') || - # ($awdev1 eq 'OpenMeteoDWDEnsemble-API' && $aVal ne 'OpenMeteoDWDEnsemble-API') || - # ($awdev1 eq 'OpenMeteoWorld-API' && $aVal ne 'OpenMeteoWorld-API')) { - # return "The attribute 'setupWeatherDev1' is set to '$awdev1'. \n". - # "Change that attribute to another weather device first if you want use an other API."; - #} - if ($aVal =~ /(SolCast|OpenMeteoDWD|OpenMeteoDWDEnsemble|OpenMeteoWorld)-API/xs) { return "The library FHEM::Utility::CTZ is missing. Please update FHEM completely." if($ctzAbsent); @@ -7163,20 +7445,20 @@ sub readCacheFile { if (ref $objref ne 'ARRAY') { return "The file $file was restored but the content is not an ARRAY"; } - + for my $obj (@{$objref}) { my $class = blessed ($obj); my $valid = $obj->isa('AI::DecisionTree'); return 'The trained object is not AI::DecisionTree' if(!$valid); } - + undef @{$data{$name}{aidectree}{aitrained}}; delete $data{$name}{aidectree}{aitrained}; - + push @{$data{$name}{aidectree}{aitrained}}, @{$objref}; - + undef @{$objref}; - + $data{$name}{current}{aitrainstate} = 'ok'; Log3 ($name, 3, qq{$name - cached data "$title" restored}); @@ -7293,7 +7575,7 @@ sub writeCacheToFile { if ($cachename eq 'aitrained') { my $objref = AiDetreeVal ($hash, 'aitrained', ''); return 'trained object is not an ARRAY' if(ref $objref ne 'ARRAY'); - + for my $obj (@{$objref}) { return 'wrong trained object' if(ref $obj ne 'AI::DecisionTree'); } @@ -7603,6 +7885,7 @@ sub _addDynAttr { $adwds = join ",", @alldwd if(@alldwd); my @fcdevs = qw( OpenMeteoDWD-API + OpenMeteoDWD_D2-API OpenMeteoDWDEnsemble-API OpenMeteoWorld-API SolCast-API @@ -7623,8 +7906,8 @@ sub _addDynAttr { ###################################################################### for my $step (1..MAXWEATHERDEV) { if ($step == 1) { - push @deva, ($adwds ? "setupWeatherDev1:OpenMeteoDWD-API,OpenMeteoDWDEnsemble-API,OpenMeteoWorld-API,$adwds" : - "setupWeatherDev1:OpenMeteoDWD-API,OpenMeteoDWDEnsemble-API,OpenMeteoWorld-API"); + push @deva, ($adwds ? "setupWeatherDev1:OpenMeteoDWD-API,OpenMeteoDWD_D2-API,OpenMeteoDWDEnsemble-API,OpenMeteoWorld-API,$adwds" : + "setupWeatherDev1:OpenMeteoDWD-API,OpenMeteoDWD_D2-API,OpenMeteoDWDEnsemble-API,OpenMeteoWorld-API"); next; } @@ -7697,25 +7980,39 @@ sub centralTask { ### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !! ########################################################################################################################## - delete $data{$name}{circular}{'00'}; # 04.02.2025 - readingsDelete ($hash, '.migrated'); # 01.02.25 + #delete $data{$name}{circular}{'00'}; # 04.02.2025 + readingsDelete ($hash, '.migrated'); # 01.02.25 + + my $pcb = AttrVal ($name, 'affectBatteryPreferredCharge', undef); # 22.03.2025 + my $apc = AttrVal ($name, 'plantControl', ''); - for my $ck (keys %{$data{$name}{circular}}) { # 30.12.2024 - $data{$name}{circular}{$ck}{batin01} = delete $data{$name}{circular}{$ck}{batin} if(defined $data{$name}{circular}{$ck}{batin}); - $data{$name}{circular}{$ck}{batout01} = delete $data{$name}{circular}{$ck}{batout} if(defined $data{$name}{circular}{$ck}{batout}); - } + if (defined $pcb) { + my $newval = $apc." batteryPreferredCharge=$pcb"; + CommandAttr (undef, "$name plantControl $newval"); + ::CommandDeleteAttr (undef, "$name affectBatteryPreferredCharge"); + } + ###### + + my $afp = AttrVal ($name, 'affectConsForecastInPlanning', undef); # 22.03.2025 + my $pc1 = AttrVal ($name, 'plantControl', ''); - for my $dy (sort keys %{$data{$name}{pvhist}}) { # 01.01.2025 - for my $hr (sort keys %{$data{$name}{pvhist}{$dy}}) { - $data{$name}{pvhist}{$dy}{$hr}{batintotal01} = delete $data{$name}{pvhist}{$dy}{$hr}{batintotal} if(defined $data{$name}{pvhist}{$dy}{$hr}{batintotal}); - $data{$name}{pvhist}{$dy}{$hr}{batouttotal01} = delete $data{$name}{pvhist}{$dy}{$hr}{batouttotal} if(defined $data{$name}{pvhist}{$dy}{$hr}{batouttotal}); - $data{$name}{pvhist}{$dy}{$hr}{batin01} = delete $data{$name}{pvhist}{$dy}{$hr}{batin} if(defined $data{$name}{pvhist}{$dy}{$hr}{batin}); - $data{$name}{pvhist}{$dy}{$hr}{batout01} = delete $data{$name}{pvhist}{$dy}{$hr}{batout} if(defined $data{$name}{pvhist}{$dy}{$hr}{batout}); - $data{$name}{pvhist}{$dy}{$hr}{batmaxsoc01} = delete $data{$name}{pvhist}{$dy}{$hr}{batmaxsoc} if(defined $data{$name}{pvhist}{$dy}{$hr}{batmaxsoc}); - $data{$name}{pvhist}{$dy}{$hr}{batsetsoc01} = delete $data{$name}{pvhist}{$dy}{$hr}{batsetsoc} if(defined $data{$name}{pvhist}{$dy}{$hr}{batsetsoc}); - } - } + if (defined $afp) { + my $newval = $pc1." consForecastInPlanning=$afp"; + CommandAttr (undef, "$name plantControl $newval"); + ::CommandDeleteAttr (undef, "$name affectConsForecastInPlanning"); + } + ###### + + my $csl = AttrVal ($name, 'ctrlShowLink', undef); # 22.03.2025 + my $pc2 = AttrVal ($name, 'plantControl', ''); + if (defined $csl) { + my $newval = $pc2." showLink=$csl"; + CommandAttr (undef, "$name plantControl $newval"); + ::CommandDeleteAttr (undef, "$name ctrlShowLink"); + } + ###### + my $n = 0; # 01.02.25 -> Datenmigration pvrlsum, pvfcsum, dnumsum in pvrl_*, pvfc_* for my $hh (1..24) { $hh = sprintf "%02d", $hh; @@ -7749,7 +8046,7 @@ sub centralTask { if ($n) { Log3 ($name, 1, "$name - NOTE - the stored PV real and forecast datasets (quantity: $n) were migrated to the new module structure"); } - + ########################################################################################################################## if (!CurrentVal ($hash, 'allStringsFullfilled', 0)) { # die String Konfiguration erstellen wenn noch nicht erfolgreich ausgeführt @@ -7847,6 +8144,8 @@ sub centralTask { $centpars->{evt} = 1; singleUpdateState ( {hash => $hash, state => $centpars->{state}, evt => $centpars->{evt}} ); } + + undef %{$centpars}; return; } @@ -8219,7 +8518,7 @@ sub _specialActivities { ## bestimmte einmalige Aktionen ################################## $chour = int $chour; - $minute = int $minute; + $minute = int $minute; my $aitrh = CurrentVal ($name, 'aiTrainStart', AITRSTARTDEF); # Stunde f. Start AI-Training ## Task 1 @@ -8565,7 +8864,7 @@ sub _transferWeatherValues { my $rr1c = $data{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{rr1c}; # Gesamtniederschlag (1-stündig) letzte 1 Stunde my $temp = $data{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{ttt}; # Außentemperatur my $don = $data{$name}{weatherdata}{"fc${fd}_${fh}"}{merge}{don}; # Tag/Nacht-Grenze - + if (defined $wid && (!$wid || $wid == 100)) { $wcc = 0; # V 1.47.2 debugLog ($paref, 'collectData', "Adjust cloud cover ratio (wcc) due to significant weather (ww) - ww: $wid -> wcc: $wcc"); @@ -8980,8 +9279,8 @@ return; sub _transferAPIRadiationValues { my $paref = shift; my $name = $paref->{name}; - my $type = $paref->{type}; my $t = $paref->{t}; # Epoche Zeit + my $lang = $paref->{lang}; my $chour = $paref->{chour}; my $date = $paref->{date}; my $hash = $defs{$name}; @@ -8991,7 +9290,10 @@ sub _transferAPIRadiationValues { my @strings = sort keys %{$data{$name}{strings}}; return if(!@strings); - my $lang = $paref->{lang}; + my $invcapsum = 0; + for my $in (keys %{$data{$name}{inverters}}) { + $invcapsum += InverterVal ($hash, $in, 'invertercap', 0); # Limit Leistungssumme aller Inverters + } for my $num (0..47) { my ($fd,$fh) = calcDayHourMove ($chour, $num); @@ -9047,10 +9349,10 @@ sub _transferAPIRadiationValues { } $paref->{sabin} = sunalt2bin ($sunalt); - my $est = __calcPVestimates ($paref); + my $pvest = __calcPVestimates ($paref); my ($msg, $pvaifc) = aiGetResult ($paref); # KI Entscheidungen abfragen - - $data{$name}{nexthours}{$nhtstr}{pvapifc} = $est; # durch API gelieferte PV Forecast + + $data{$name}{nexthours}{$nhtstr}{pvapifc} = $pvest; # durch API gelieferte PV Forecast delete $paref->{fd}; delete $paref->{fh1}; @@ -9066,11 +9368,17 @@ sub _transferAPIRadiationValues { my $pvfc; 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 % + if ($invcapsum && $pvaifc > $invcapsum) { + $pvaifc = $invcapsum; # PV AI Vorhersage auf Summe aller WR Kapazität begrenzen - if ($msg eq 'accurate') { # KI liefert 'accurate' Treffer -> verwenden + debugLog ($paref, "radiationProcess", "PV AI forecast start time $wantdt limited to $invcapsum Wh due to inverter capacity summary"); + } + + my $airn = CircularVal ($hash, 99, 'aiRulesNumber', 0) / CurrentVal ($name, 'aiTreesPV', AINUMTREES); + my $aivar = 100; + $aivar = sprintf "%.0f", (100 * $pvaifc / $pvest) if($pvest); # Übereinstimmungsgrad KI Forecast zu API Forecast in % + + if ($msg eq 'accurate') { # KI liefert 'accurate' Treffer -> verwenden if ($airn >= AIACCTRNMIN || ($aivar >= AIACCLOWLIM && $aivar <= AIACCUPLIM)) { $data{$name}{nexthours}{$nhtstr}{aihit} = 1; $pvfc = $pvaifc; @@ -9079,7 +9387,7 @@ sub _transferAPIRadiationValues { debugLog ($paref, 'aiData', qq{AI Hit - accurate result used -> aiRulesNum: $airn, variance: $aivar, hod: $hod, Rad1h: $rad1h, pvfc: $pvfc Wh}); } } - elsif ($msg eq 'spreaded') { # Abweichung AI von Standardvorhersage begrenzen + elsif ($msg eq 'spreaded') { # Abweichung AI von Standardvorhersage begrenzen if ($airn >= AISPREADTRNMIN || ($aivar >= AISPREADLOWLIM && $aivar <= AISPREADUPLIM)) { $data{$name}{nexthours}{$nhtstr}{aihit} = 1; $pvfc = $pvaifc; @@ -9094,17 +9402,17 @@ sub _transferAPIRadiationValues { } if ($useai) { - $data{$name}{nexthours}{$nhtstr}{pvaifc} = $pvaifc; # durch AI gelieferte PV Forecast + $data{$name}{nexthours}{$nhtstr}{pvaifc} = $pvaifc; # durch AI gelieferte PV Forecast } else { delete $data{$name}{nexthours}{$nhtstr}{pvaifc}; $data{$name}{nexthours}{$nhtstr}{aihit} = 0; - $pvfc = $est; + $pvfc = $pvest; debugLog ($paref, 'aiData', "use PV from API (no AI or AI result tolerance overflow) -> hod: $hod, Rad1h: ".(defined $rad1h ? $rad1h : '-').", pvfc: $pvfc Wh"); } - $data{$name}{nexthours}{$nhtstr}{pvfc} = $pvfc; # resultierende PV Forecast zuweisen + $data{$name}{nexthours}{$nhtstr}{pvfc} = $pvfc; # resultierende PV Forecast zuweisen if ($num < 23 && $fh < 24) { # Ringspeicher PV forecast Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350 $data{$name}{circular}{sprintf "%02d",$fh1}{pvapifc} = NexthoursVal ($hash, $nhtstr, 'pvapifc', undef); @@ -9184,7 +9492,6 @@ return; sub __calcPVestimates { my $paref = shift; my $name = $paref->{name}; - my $type = $paref->{type}; my $wantdt = $paref->{wantdt}; my $hod = $paref->{hod}; my $fd = $paref->{fd}; @@ -9195,7 +9502,7 @@ sub __calcPVestimates { my $reld = $fd == 0 ? "today" : $fd == 1 ? "tomorrow" : "unknown"; 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 $temp = NexthoursVal ($hash, "NextHour".sprintf ("%02d",$num), "temp", TEMPBASEDEF); # vorhergesagte Temperatur Stunde X my ($acu, $aln) = isAutoCorrUsed ($name); $paref->{wcc} = $wcc; @@ -9203,8 +9510,9 @@ sub __calcPVestimates { delete $paref->{wcc}; my ($lh,$sq,$peakloss, $modtemp); - my $pvsum = 0; - my $peaksum = 0; + my $pvsum = 0; + my $peaksum = 0; + my $invcapsum = 0; for my $string (sort keys %{$data{$name}{strings}}) { my $peak = StringVal ($hash, $string, 'peak', 0); # String Peak (kWp) @@ -9222,31 +9530,21 @@ sub __calcPVestimates { delete $paref->{temp}; } - $peak *= 1000; - my $est = RadiationAPIVal ($hash, $string, $wantdt, 'pv_estimate50', 0); - my $pv = sprintf "%.1f", ($est * $hc); # Korrekturfaktor anwenden - - my $invcap = 0; + $peak *= 1000; + my $pvest = RadiationAPIVal ($hash, $string, $wantdt, 'pv_estimate50', 0); + my $pv = sprintf "%.1f", ($pvest * $hc); # Korrekturfaktor anwenden for my $in (keys %{$data{$name}{inverters}}) { my $istrings = InverterVal ($hash, $in, 'istrings', ''); # dem Inverter zugeordnete Strings next if(!grep /^$string$/, (split ',', $istrings)); - $invcap = InverterVal ($hash, $in, 'invertercap', 0); # Max. Leistung des Inverters - - last; - } - - if ($invcap && $pv > $invcap) { - $pv = $invcap; # PV Vorhersage auf WR Kapazität begrenzen - - debugLog ($paref, "radiationProcess", "PV forecast start time $wantdt limited to $pv Wh due to inverter capacity"); + $invcapsum += InverterVal ($hash, $in, 'invertercap', 0); # Max. Leistung des Inverters } if ($debug =~ /radiationProcess/xs) { $lh = { # Log-Hash zur Ausgabe "String Peak" => $peak. " W", - "Estimated PV generation (raw)" => $est. " Wh", + "Estimated PV generation (raw)" => $pvest." Wh", "Estimated PV generation (calc)" => $pv. " Wh", "PV correction factor" => $hc, "PV correction quality" => $hq, @@ -9271,7 +9569,14 @@ sub __calcPVestimates { $data{$name}{current}{allstringspeak} = $peaksum; # temperaturbedingte Korrektur der installierten Peakleistung in W $pvsum = $peaksum if($peaksum && $pvsum > $peaksum); # Vorhersage nicht größer als die Summe aller PV-Strings Peak - $pvsum = sprintf "%.0f", $pvsum; + + if ($invcapsum && $pvsum > $invcapsum) { + $pvsum = $invcapsum; # PV Vorhersage auf WR Kapazität begrenzen + + debugLog ($paref, "radiationProcess", "PV forecast start time $wantdt limited to $invcapsum Wh due to inverter capacity summary"); + } + + $pvsum = sprintf "%.0f", $pvsum; if ($debug =~ /radiationProcess/xs) { $lh = { # Log-Hash zur Ausgabe @@ -9742,7 +10047,7 @@ sub _transferBatteryValues { my ($err, $badev, $h) = isDeviceValid ( { name => $name, obj => 'setupBatteryDev'.$bn, method => 'attr' } ); next if($err); - + my $warnin = ''; my $warnout = ''; my ($pin,$piunit) = split ":", $h->{pin}; # Readingname/Unit für aktuelle Batterieladung @@ -9769,10 +10074,10 @@ sub _transferBatteryValues { my $btotout = ReadingsNum ($badev, $bout, 0) * $boutuf; # totale Batterieentladung (Wh) my $btotin = ReadingsNum ($badev, $bin, 0) * $binuf; # totale Batterieladung (Wh) my $soc = ReadingsNum ($badev, $batchr, 0); - + my $show = $h->{show} // 0; # Batterie in Balkengrafik anzeigen my $pos = 'top'; - + if ($show =~ /:/xs) { ($show, $pos) = split ':', $show; } @@ -9822,7 +10127,7 @@ sub _transferBatteryValues { }; ($pbi,$pbo) = substSpecialCases ($params); - + if ($debug =~ /collectData/x) { Log3 ($name, 1, "$name DEBUG> Battery Power data after resolving the special case 'pou eq -pin' =>"); Log3 ($name, 1, "$name DEBUG> pin: $pbi W, pout: $pbo W"); @@ -9870,7 +10175,7 @@ sub _transferBatteryValues { Log3 ($name, $vl, "$name $pre The BatIn Energy of Battery '$badev' is lower than the value saved before. This situation is unexpected and the Energy generated of current hour of this Battery is set to '0'."); $warnin = ' (WARNING invalid BatIn Energy occured - see Logfile)'; } - + $data{$name}{circular}{sprintf("%02d",$nhour)}{'batin'.$bn} = $batinthishour; # Ringspeicher Battery In Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350 writeToHistory ( { paref => $paref, key => 'batinthishour'.$bn, val => $batinthishour, hour => $nhour } ); @@ -9901,7 +10206,7 @@ sub _transferBatteryValues { Log3 ($name, $vl, "$name $pre The BatOut Energy of Battery '$badev' is lower than the value saved before. This situation is unexpected and the Energy generated of current hour of this Battery is set to '0'."); $warnout = ' (WARNING invalid BatOut Energy occured - see Logfile)'; } - + $data{$name}{circular}{sprintf("%02d",$nhour)}{'batout'.$bn} = $batoutthishour; # Ringspeicher Battery In Forum: https://forum.fhem.de/index.php/topic,117864.msg1133350.html#msg1133350 writeToHistory ( { paref => $paref, key => 'batoutthishour'.$bn, val => $batoutthishour, hour => $nhour } ); @@ -10202,14 +10507,11 @@ sub _batChargeRecmd { return if(!isBatteryUsed ($name)); - my $hash = $defs{$name}; - my $rodpvfc = ReadingsNum ($name, 'RestOfDayPVforecast', 0); # PV Prognose Rest des Tages - my $tompvfc = ReadingsNum ($name, 'Tomorrow_PVforecast', 0); # PV Prognose nächster Tag - my $confcss = CurrentVal ($hash, 'tdConFcTillSunset', 0); # Verbrauchsprognose bis Sonnenuntergang - my $tomconfc = ReadingsNum ($name, 'Tomorrow_ConsumptionForecast', 0); # Verbrauchsprognose nächster Tag - my $pvCu = ReadingsNum ($name, 'Current_PV', 0); # aktuelle PV Erzeugung - my $curcon = ReadingsNum ($name, 'Current_Consumption', 0); # aktueller Verbrauch - my $inplim = 0; + my $hash = $defs{$name}; + my $pvCu = ReadingsNum ($name, 'Current_PV', 0); # aktuelle PV Erzeugung + my $curcon = ReadingsNum ($name, 'Current_Consumption', 0); # aktueller Verbrauch + my $feedinlim = CurrentVal ($name, 'feedinPowerLimit', INFINIITY); # Einspeiselimit in W + my $inplim = 0; ## Inverter Limits ermitteln ############################## @@ -10246,11 +10548,16 @@ sub _batChargeRecmd { return; } - my $csoc = BatteryVal ($hash, $bn, 'bcharge', 0); # aktuelle Ladung in % - my $batoptsoc = ReadingsNum ($name, 'Battery_OptimumTargetSoC_'.$bn, 0); # aktueller optimierter SoC - my $cgbt = AttrVal ($name, 'ctrlBatSocManagement'.$bn, undef); - my $sf = __batCapShareFactor ($hash, $bn); # Anteilsfaktor der Batterie XX Kapazität an Gesamtkapazität - my $lowSoc = 0; + my $maxfctim = timestringToTimestamp (ReadingsVal ($name, 'Today_MaxPVforecastTime', '')) // $t; + my $rodpvfc = ReadingsNum ($name, 'RestOfDayPVforecast', 0); # PV Prognose Rest des Tages + my $tompvfc = ReadingsNum ($name, 'Tomorrow_PVforecast', 0); # PV Prognose nächster Tag + my $tomconfc = ReadingsNum ($name, 'Tomorrow_ConsumptionForecast', 0); # Verbrauchsprognose nächster Tag + my $batoptsoc = ReadingsNum ($name, 'Battery_OptimumTargetSoC_'.$bn, 0); # aktueller optimierter SoC + my $confcss = CurrentVal ($name, 'tdConFcTillSunset', 0); # Verbrauchsprognose bis Sonnenuntergang + my $csoc = BatteryVal ($hash, $bn, 'bcharge', 0); # aktuelle Ladung in % + my $cgbt = AttrVal ($name, 'ctrlBatSocManagement'.$bn, undef); + my $sf = __batCapShareFactor ($hash, $bn); # Anteilsfaktor der Batterie XX Kapazität an Gesamtkapazität + my $lowSoc = 0; if ($cgbt) { ($lowSoc) = __parseAttrBatSoc ($name, $cgbt); @@ -10262,7 +10569,9 @@ sub _batChargeRecmd { debugLog ($paref, 'batteryManagement', "Bat $bn Charge Rcmd - Installed Battery capacity: $batinstcap Wh, Percentage of total capacity: ".(sprintf "%.1f", $sf*100)." %"); debugLog ($paref, 'batteryManagement', "Bat $bn Charge Rcmd - The PV generation, consumption and surplus listed below are based on the battery's share of the total capacity!"); - my $socwh = sprintf "%.0f", ($batinstcap * $csoc / 100); # aktueller SoC in Wh + my $socwh = sprintf "%.0f", ($batinstcap * $csoc / 100); # aktueller SoC in Wh + my $whneed = $batinstcap - $socwh; + my $sfmargin = $whneed * 0.25; # Sicherheitszuschlag: X% der benötigten Ladeenergie (Wh) ## Auswertung für jede kommende Stunde ######################################## @@ -10275,6 +10584,7 @@ sub _batChargeRecmd { my $confc = NexthoursVal ($hash, 'NextHour'.$nhr, 'confc', 0); my $pvfc = NexthoursVal ($hash, 'NextHour'.$nhr, 'pvfc', 0); my $stt = NexthoursVal ($hash, 'NextHour'.$nhr, 'starttime', ''); + my $nhts = timestringToTimestamp ($stt) // $t; # Unix Timestamp von NextHours Starttime $stt = (split /[-:]/, $stt)[2] if($stt); my $crel = 0; # Ladefreigabe 0 per Default @@ -10282,12 +10592,12 @@ sub _batChargeRecmd { ## Aufteilung Energie auf Batterie XX im Verhältnis aller Bat ############################################################### - $pvfc = sprintf "%.0f", $sf * $pvfc; - $confcss = sprintf "%.0f", $sf * $confcss; - $confc = sprintf "%.0f", $sf * $confc; - $rodpvfc = sprintf "%.0f", $sf * $rodpvfc; - $tomconfc = sprintf "%.0f", $sf * $tomconfc; - $tompvfc = sprintf "%.0f", $sf * $tompvfc; + $pvfc = sprintf "%.0f", ($sf * $pvfc); + $confcss = sprintf "%.0f", ($sf * $confcss); + $confc = sprintf "%.0f", ($sf * $confc); + $rodpvfc = sprintf "%.0f", ($sf * $rodpvfc); + $tomconfc = sprintf "%.0f", ($sf * $tomconfc); + $tompvfc = sprintf "%.0f", ($sf * $tompvfc); ## (Rest) PV-Überschuß für den Tag #################################### @@ -10296,6 +10606,7 @@ sub _batChargeRecmd { $confcss -= $confc; # Verbrauch bis Sonnenuntergang - Verbrauch Fc aktuelle Stunde $confcss = 0 if($confcss < 0); $rodpvfc -= $pvfc; + $rodpvfc = 0 if($rodpvfc < 0); $spday = $rodpvfc - $confcss; # PV-Überschußprognose (Rest) heutiger Tag } else { # nächster Tag @@ -10309,22 +10620,20 @@ sub _batChargeRecmd { $spday = 0 if($spday < 0); # PV Überschuß Prognose bis Sonnenuntergang ## Ladefreigabe - ################# - my $whneed = $batinstcap - $socwh; - my $maxfctim = timestringToTimestamp (ReadingsVal ($name, 'Today_MaxPVforecastTime', '')) // $t; - - my $sfmargin = $whneed * 0.25; # Sicherheitszuschlag: X% der benötigten Ladeenergie (Wh) - - if ( $whneed + $sfmargin >= $spday || ($today && $t > $maxfctim)) {$crel = 1} # change V 1.47.0: Ladefreigabe wenn benötigte Ladeenergie >= Restüberschuß des Tages zzgl. Sicherheitsaufschlag - if ( !$num && ($pvCu - $curcon) >= $inplim ) {$crel = 1} # Ladefreigabe wenn akt. PV Leistung >= WR-Leistungsbegrenzung - if ( !$cgbt ) {$crel = 1} # immer Ladefreigabe wenn kein BatSoc-Management + ################# + if ( $whneed + $sfmargin >= $spday ) {$crel = 1} # Ladefreigabe wenn benötigte Ladeenergie >= Restüberschuß des Tages zzgl. Sicherheitsaufschlag + if ( $today && $t >= $maxfctim) {$crel = 1} # change V 1.49.0: Ladefreigabe wenn akt. Zeit >= Zeit bei max. Tagesertragsertwartung + if ( $today && $nhts >= $maxfctim) {$crel = 1} # change V 1.49.0: Ladefreigabe bei Prognosezeit >= Zeit bei max. Tagesertragsertwartung + if ( !$num && ($pvCu - $curcon) >= $inplim ) {$crel = 1} # Ladefreigabe wenn akt. PV Leistung - Abschläge >= WR-Leistungsbegrenzung + if ( !$num && ($pvCu - $curcon) >= $feedinlim ) {$crel = 1} # Ladefreigabe wenn akt. PV Leistung - Abschläge >= Einspeiselimit der Anlage + if ( !$cgbt ) {$crel = 1} # immer Ladefreigabe wenn kein BatSoc-Management ## SOC-Prognose ################# # change V 1.47.0 my $fceff = $pvfc - $confc; # effektiver PV Überschuß (effektiver Verbrauch wenn < 0) - $socwh += $crel ? ($fceff > 0 ? $fceff * STOREFFDEF : $fceff / STOREFFDEF) : + $socwh += $crel ? ($fceff > 0 ? $fceff * STOREFFDEF : $fceff / STOREFFDEF) : ($fceff > 0 ? 0 : $fceff / STOREFFDEF); # PV Prognose nur einbeziehen wenn Ladefreigabe - + $socwh = $socwh < $lowSocwh ? $lowSocwh : $socwh < $batoptsocwh ? $batoptsocwh : # SoC Prognose in Wh $socwh > $batinstcap ? $batinstcap : @@ -10340,14 +10649,20 @@ sub _batChargeRecmd { } ); # Readings NextHourXX_Bat_XX_ChargeForecast erstellen - my $msg = "(currsoc: $csoc %, SoCfc: $progsoc %, soc: $socwh Wh, pvfc: $pvfc, confc: $confc, Surp Day: $spday Wh, Curr PV: $pvCu W, Curr Consumption: $curcon W, Limit: $inplim W)"; - + my $msg = "(CurrSoc: $csoc %, SoCfc: $socwh Wh, whneed: $whneed, pvfc: $pvfc, rodpvfc: $rodpvfc, confcss: $confcss, SurpDay: $spday Wh, CurrPV: $pvCu W, CurrCons: $curcon W, Limit: $inplim W)"; + if ($num) { - $msg = "(SoCfc: $progsoc %, soc: $socwh Wh, pvfc: $pvfc, confc: $confc, Surp Day: $spday Wh)"; + $msg = "(SoCfc: $progsoc % / $socwh Wh, whneed: $whneed, pvfc: $pvfc, rodpvfc: $rodpvfc, confcss: $confcss, SurpDay: $spday Wh)"; + + if (!$today) { + $msg = "(SoCfc: $progsoc % / $socwh Wh, whneed: $whneed, pvfc: $pvfc, tompvfc: $tompvfc, tomconfc: $tomconfc, SurpDay: $spday Wh)"; + } } else { storeReading ('Battery_ChargeRecommended_'.$bn, $crel); # Reading nur für aktuelle Stunde } + + $whneed = $batinstcap - $socwh; $data{$name}{nexthours}{'NextHour'.$nhr}{'rcdchargebat'.$bn} = $crel; $data{$name}{nexthours}{'NextHour'.$nhr}{'soc'.$bn} = $progsoc; @@ -10408,8 +10723,8 @@ sub _createSummaries { my $tdConFcTillSunset = 0; my $remainminutes = 60 - $minute; # verbleibende Minuten der aktuellen Stunde - my $restofhourpvfc = (NexthoursVal($hash, "NextHour00", 'pvfc', 0)) / 60 * $remainminutes; - my $restofhourconfc = (NexthoursVal($hash, "NextHour00", 'confc', 0)) / 60 * $remainminutes; + my $restofhourpvfc = (NexthoursVal($hash, "NextHour00", 'pvfc', 0)) / 60 * $remainminutes; + my $restofhourconfc = (NexthoursVal($hash, "NextHour00", 'confc', 0)) / 60 * $remainminutes; $next1HoursSum->{PV} = $restofhourpvfc; $next2HoursSum->{PV} = $restofhourpvfc; @@ -10797,7 +11112,7 @@ sub __calcEnergyPieces { debug => $paref->{debug} } ); - + if ($err) { Log3 ($name, 1, "$name - ERROR in consumer $c config: $err"); return; @@ -11088,17 +11403,17 @@ sub ___doPlanning { my $debug = $paref->{debug}; my $lang = $paref->{lang}; my $nh = $data{$name}{nexthours}; - my $cicfip = AttrVal ($name, 'affectConsForecastInPlanning', 0); # soll Consumption Vorhersage in die Überschußermittlung eingehen ? + my $cicfip = CurrentVal ($name, 'consForecastInPlanning', 0); # soll Consumption Vorhersage in die Überschußermittlung eingehen ? my $hash = $defs{$name}; - debugLog ($paref, "consumerPlanning", qq{consumer "$c" - consider consumption forecast in consumer planning (attr 'affectConsForecastInPlanning'): }.($cicfip ? 'yes' : 'no')); + debugLog ($paref, "consumerPlanning", qq{consumer "$c" - consider consumption forecast in consumer planning (attr 'plantControl'): }.($cicfip ? 'yes' : 'no')); my %max; my %mtimes; - ## max. PV-Forecast bzw. Überschuß (bei gesetzen affectConsForecastInPlanning) ermitteln - ########################################################################################## + ## max. PV-Forecast bzw. Überschuß (bei gesetzen consForecastInPlanning) ermitteln + #################################################################################### for my $idx (sort keys %{$nh}) { my $pvfc = NexthoursVal ($hash, $idx, 'pvfc', 0); my $confcex = NexthoursVal ($hash, $idx, 'confcEx', 0); # prognostizierter Verbrauch ohne registrierte Consumer mit gesetzten Schlüssel exconfc @@ -11157,7 +11472,7 @@ sub ___doPlanning { if ($err) { Log3 ($name, 1, "$name - ERROR in consumer $c config: $err"); return; - } + } debugLog ($paref, "consumerPlanning", qq{consumer "$c" - mode: $mode, mintime: $mintime, relevant method: surplus}); @@ -11873,7 +12188,7 @@ sub ___setConsumerSwitchingState { debug => $paref->{debug} } ); - + if ($err) { $state = qq{Consumer '$calias' ERROR in config: $err}; Log3 ($name, 1, "$name - $state"); @@ -12130,14 +12445,13 @@ sub ___enableSwitchByBatPrioCharge { my $name = $paref->{name}; my $c = $paref->{consumer}; - my $hash = $defs{$name}; my $ena = 1; - my $pcb = AttrVal ($name, 'affectBatteryPreferredCharge', 0); # Vorrangladung Batterie zu X% + my $pcb = CurrentVal ($name, 'affectBatteryPreferredCharge', 0); # Vorrangladung Batterie zu X% my ($badev) = isBatteryUsed ($name); return $ena if(!$pcb || !$badev); # Freigabe Schalten Consumer wenn kein Prefered Battery/Soll-Ladung 0 oder keine Batterie installiert - my $bcharge = BatteryVal ($hash, '01', 'bcharge', 0); # aktuelle Ladung in % + my $bcharge = BatteryVal ($name, '01', 'bcharge', 0); # aktuelle Ladung in % $ena = 0 if($bcharge < $pcb); # keine Freigabe wenn Batterieladung kleiner Soll-Ladung return $ena; @@ -12393,7 +12707,7 @@ sub _calcConsForecast_circular { my $nhhr = sprintf "%02d", int (strftime "%H", localtime($utime)) + 1; # Stunde des Tages vom NextHours Key (01,02,...24) next if(!defined $usage{$nhhr}{con}); # V 1.47.0 - + $data{$name}{nexthours}{$k}{confcEx} = $usage{$nhhr}{conex}; $data{$name}{nexthours}{$k}{confc} = $usage{$nhhr}{con}; # prognostizierter Verbrauch (Median) @@ -12585,9 +12899,9 @@ sub _calcDataEveryFullHour { if ($acu) { readingsSingleUpdate ($hash, '.pvCorrectionFactor_Auto_Soll', ($aln ? $acu : $acu.' noLearning'), 0) if($acu =~ /on/xs); - + my $idts = CircularVal ($hash, 99, "attrInvChangedTs", ''); # Definitionstimestamp des Attr setupInverterDev01 - + if (!$idts) { # V 1.47.3 $data{$name}{circular}{99}{attrInvChangedTs} = int $t; return; @@ -13042,7 +13356,7 @@ sub _genSpecialReadings { } elsif ($hcsr{$kpi}{fnr} == 4) { storeReading ($prpo.'_'.$kpi, &{$hcsr{$kpi}{fn}} ($hash, $day, $hcsr{$kpi}{par}, $hcsr{$kpi}{par1}, $def).$hcsr{$kpi}{unit}); - } + } elsif ($hcsr{$kpi}{fnr} == 5) { if ($kpi eq 'SunHours_Remain') { my $ss = &{$hcsr{$kpi}{fn}} ($hash, 'sunsetTodayTs', $def); @@ -13191,7 +13505,7 @@ sub _genSpecialReadings { storeReading ($prpo.'_'.$kpi, $radc.$hcsr{$kpi}{unit}); } - + if ($kpi eq 'todayConsumptionForecast') { for my $hod (sort keys %{$data{$name}{pvhist}{$day}}) { next if(!$hod || $hod == 99); @@ -13212,7 +13526,7 @@ sub _genSpecialReadings { storeReading ($prpo.'_'.$kpi.'_'.$hod, $confc.$hcsr{$kpi}{unit}); } - } + } if ($kpi eq 'conForecastTillNextSunrise') { my $confc = 0; @@ -13408,7 +13722,7 @@ sub entryGraphic { my $colspan = $maxhours + 2; my $ret = q{}; - $ret .= "$dlink
" if(AttrVal($name, 'ctrlShowLink', 0)); + $ret .= "$dlink
" if(CurrentVal ($name, 'showLink', 0)); #$ret .= ""; $ret .= ""; @@ -13468,7 +13782,7 @@ sub entryGraphic { my %hfcg1; $paref->{chartlvl} = 1; # Balkengrafik Ebene 1 $paref->{hfcg} = \%hfcg1; # hfcg = hash forecast graphic - + ## Werte aktuelle Stunde ########################## $paref->{thishour} = _beamGraphicFirstHour ($paref); @@ -13484,7 +13798,7 @@ sub entryGraphic { $paref->{maxCon} = $back->{maxCon}; $paref->{maxDif} = $back->{maxDif}; # für Typ diff $paref->{minDif} = $back->{minDif}; # für Typ diff - + ## Batteriewerte füllen ######################### _beamFillupBatValues ($paref); @@ -13504,7 +13818,7 @@ sub entryGraphic { if ($paref->{beam3cont} || $paref->{beam4cont}) { # Balkengrafik Ebene 2 my %hfcg2; - $paref->{chartlvl} = 2; + $paref->{chartlvl} = 2; $paref->{beam1cont} = $paref->{beam3cont}; $paref->{beam2cont} = $paref->{beam4cont}; $paref->{colorb1} = AttrVal ($name, 'graphicBeam3Color', B3COLDEF); @@ -13514,7 +13828,7 @@ sub entryGraphic { $paref->{height} = AttrVal ($name, 'graphicBeamHeightLevel2', $paref->{height}); $paref->{weather} = 0; $paref->{hfcg} = \%hfcg2; - + # Werte aktuelle Stunde ########################## $paref->{thishour} = _beamGraphicFirstHour ($paref); @@ -13526,7 +13840,7 @@ sub entryGraphic { $paref->{maxCon} = $back->{maxCon}; $paref->{maxDif} = $back->{maxDif}; # für Typ diff $paref->{minDif} = $back->{minDif}; # für Typ diff - + ## Batteriewerte füllen ######################### _beamFillupBatValues ($paref); @@ -13541,23 +13855,23 @@ sub entryGraphic { delete $paref->{maxDif}; delete $paref->{minDif}; } - + ## Balkengrafik Ebene 3 ######################### if ($paref->{beam5cont} || $paref->{beam6cont}) { # Balkengrafik Ebene 3 my %hfcg3; - $paref->{chartlvl} = 3; + $paref->{chartlvl} = 3; $paref->{beam1cont} = $paref->{beam5cont}; $paref->{beam2cont} = $paref->{beam6cont}; $paref->{colorb1} = AttrVal ($name, 'graphicBeam5Color', B5COLDEF); $paref->{colorb2} = AttrVal ($name, 'graphicBeam6Color', B6COLDEF); $paref->{fcolor1} = AttrVal ($name, 'graphicBeam5FontColor', B5FONTCOLDEF); $paref->{fcolor2} = AttrVal ($name, 'graphicBeam6FontColor', B6FONTCOLDEF); - $paref->{height} = AttrVal ($name, 'graphicBeamHeightLevel2', $paref->{height}); + $paref->{height} = AttrVal ($name, 'graphicBeamHeightLevel3', $paref->{height}); $paref->{weather} = 0; $paref->{hfcg} = \%hfcg3; - + # Werte aktuelle Stunde ########################## $paref->{thishour} = _beamGraphicFirstHour ($paref); @@ -13569,7 +13883,7 @@ sub entryGraphic { $paref->{maxCon} = $back->{maxCon}; $paref->{maxDif} = $back->{maxDif}; # für Typ diff $paref->{minDif} = $back->{minDif}; # für Typ diff - + ## Batteriewerte füllen ######################### _beamFillupBatValues ($paref); @@ -14616,10 +14930,10 @@ return ($val, $unit); sub _levelSeparator { my $paref = shift; my $dstyle = $paref->{dstyle}; # TD-Style - my $maxhours = $paref->{maxhours}; - + my $maxhours = $paref->{maxhours}; + my $colspan = $maxhours + 2; - + my $ret = ""; $ret .= "
"; $ret .= ""; @@ -15159,7 +15473,7 @@ sub _beamGraphicRemainingHours { $beam1cont eq 'energycosts' ? $val6 : $beam1cont eq 'gridfeedin' ? $val7 : $beam1cont eq 'feedincome' ? $val8 : - $beam1cont =~ /batsocforecast_/xs ? $hbsocs->{$i}{(split '_', $beam1cont)[1]} : + $beam1cont =~ /batsocforecast_/xs ? $hbsocs->{$i}{(split '_', $beam1cont)[1]} : undef; $hfcg->{$i}{beam2} = $beam2cont eq 'pvForecast' ? $val1 : @@ -15293,18 +15607,18 @@ sub _beamGraphic { # Die Tabelle ist recht schmal angelegt, aber nur so lassen sich Umbrüche erzwingen my ($val, $z2, $z3, $z4, $he, $titz2, $titz3); - + $paref->{beampos} = 'top'; # Lagedefinition "über den Balken" my $ret = q{}; - + ## Wetteranzeige über den Balken ################################## $ret .= __weatherOnBeam ($paref) if($weather); - + ## Batterieanzeige über den Balken #################################### $ret .= __batteryOnBeam ($paref); - + my $m = $paref->{modulo} % 2; if ($show_diff eq 'top') { # Zusätzliche Zeile Ertrag - Verbrauch @@ -15594,13 +15908,13 @@ sub _beamGraphic { $ret .= ""; $ret .= ""; - + $paref->{beampos} = 'bottom'; # Lagedefinition "unter den Balken" ## Batterieanzeige unterhalb der Balken ######################################### $ret .= __batteryOnBeam ($paref); - + delete $paref->{beampos}; return $ret; @@ -15872,7 +16186,7 @@ sub _flowGraphic { $bat2home = 0; $soc = 0; } - + debugLog ($paref, 'graphic', "Battery initial summary - batin: $batin, bat2home: $bat2home"); ## Resultierende von Laden und Entladen berechnen @@ -15884,7 +16198,7 @@ sub _flowGraphic { if ($x > 0) { $batin = $x; } else { $bat2home = abs $x; } # es darf nur $batin ODER $bat2home mit einem Wert > 0 geben debugLog ($paref, 'graphic', "Battery Node summary after calculating resultant - batin: $batin, bat2home: $bat2home"); - + my $bat_color = $soc < 26 ? "$stna bat25" : $soc < 76 ? "$stna bat50" : "$stna bat75"; @@ -16565,7 +16879,7 @@ sub __substituteIcon { $inorcmd = $inorcmd ? $inorcmd : ''; $icharge = $icharge ? $icharge : ''; $idischrg = $idischrg ? $idischrg : ''; - + my $cgbt = AttrVal ($name, 'ctrlBatSocManagement'.$pn, ''); if (defined $flag) { # Empfehlungszeitraum @@ -17241,7 +17555,7 @@ sub aiManageInstance { my $paref = shift; my $name = $paref->{name}; my $hash = $defs{$name}; - + return if(!isPrepared4AI ($hash)); delete $hash->{HELPER}{AIBLOCKRUNNING} if(defined $hash->{HELPER}{AIBLOCKRUNNING}{pid} && $hash->{HELPER}{AIBLOCKRUNNING}{pid} =~ /DEAD/xs); @@ -17290,11 +17604,11 @@ sub aiAddInstance { return $serial; } - + my $cst = [gettimeofday]; # Train Startzeit - + my @pvhdata; - + for my $idx (sort keys %{$data{$name}{aidectree}{airaw}}) { next if(!$idx); @@ -17319,7 +17633,7 @@ sub aiAddInstance { push @pvhdata, { rad1h => $rad1h, temp => $tbin, wcc => $cbin, wid => $wid, rr1c => $rr1c, sunalt => $sunalt, hod => $hod, pvrl => $pvrl }; } - + if (!scalar @pvhdata) { $serial = encode_base64 (Serialize ( { name => $name, aitrainstate => "aiAddInstance not performed due to no Raw data", @@ -17329,13 +17643,13 @@ sub aiAddInstance { return $serial; } - + my $numtrees = CurrentVal ($name, 'aiTreesPV', AINUMTREES); - + for my $tn (1 .. $numtrees) { # Trainiere mehrere Entscheidungsbäume auf unterschiedlichen Stichproben my @sampled = sample_data (\@pvhdata); my ($err, $dtree) = aiInit ($paref); - + if ($err) { Log3 ($name, 2, "$name - ERROR - AI::DecisionTree init error: $err"); $serial = encode_base64 (Serialize ( { name => $name, @@ -17346,9 +17660,9 @@ sub aiAddInstance { return $serial; } - + Log3 ($name, 3, "$name - AI::DecisionTree initialized") if($tn == 1); - + my $aiAddedToTrain = 0; for my $instance (@sampled) { @@ -17372,9 +17686,9 @@ sub aiAddInstance { return $serial; }; - + $aiAddedToTrain++; - + debugLog ($paref, 'aiProcess', "AI Instance added Tree $tn - ". "hod: $instance->{hod}, ". "sunalt: $instance->{sunalt}, ". @@ -17382,12 +17696,12 @@ sub aiAddInstance { "wcc: ".(defined $instance->{wcc} ? $instance->{wcc} : '-').", ". "wid: ".(defined $instance->{wid} ? $instance->{wid} : '-').", ". "rr1c: ".(defined $instance->{rr1c} ? $instance->{rr1c} : '-').", ". - "temp: ".(defined $instance->{temp} ? $instance->{temp} : '-'), + "temp: ".(defined $instance->{temp} ? $instance->{temp} : '-'), 4); } - + debugLog ($paref, 'aiProcess', "AI Instance add - Tree: $tn -> ".$aiAddedToTrain." entities added for training ".(AttrVal ($name, 'verbose', 3) < 4 ? '(set verbose 4 for output more detail)' : '')); - + $data{$name}{aidectree}{object}{$tn}{dtree} = $dtree; $data{$name}{aidectree}{object}{$tn}{enum} = $aiAddedToTrain; } @@ -17402,14 +17716,14 @@ return $serial; ################################################################ # KI trainieren ################################################################ -sub aiTrain { +sub aiTrain { my $paref = shift; my $name = $paref->{name}; my $cst = $paref->{cst}; # Train Startzeit my $hash = $defs{$name}; my ($serial, $err); - + my $object = AiDetreeVal ($hash, 'object', undef); if (!$object) { @@ -17420,10 +17734,10 @@ sub aiTrain { aicanuse => 'ok' } ), ""); - + return $serial; } - + my @ensemble; # Erstelle das Ensemble my %entities; my $numtrees = CurrentVal ($name, 'aiTreesPV', AINUMTREES); @@ -17431,7 +17745,7 @@ sub aiTrain { for my $tn (1 .. $numtrees) { # Trainiere mehrere Entscheidungsbäume auf unterschiedlichen Stichproben my $dtree = $object->{$tn}{dtree}; # dtree Objekt my $enum = $object->{$tn}{enum}; # Anazhl Elemente im Tree - + eval { $dtree->train(); 1; } @@ -17445,16 +17759,16 @@ sub aiTrain { return $serial; }; - + push @ensemble, $dtree; - + $entities{$tn} = $enum; $entities{rn} += scalar $dtree->rule_statements(); } - + undef @{$data{$name}{aidectree}{aitrained}}; delete $data{$name}{aidectree}{aitrained}; - + push @{$data{$name}{aidectree}{aitrained}}, @ensemble; $err = writeCacheToFile ($hash, 'aitrained', $aitrained.$name); @@ -17462,11 +17776,11 @@ sub aiTrain { if (!$err) { $rn = delete $entities{rn}; - + while (my ($tree, $ent) = each %entities) { debugLog ($paref, 'aiProcess', "AI trained Tree: $tree, number of entities: $ent"); } - + debugLog ($paref, 'aiProcess', qq{Training instances and their associated information where purged from the AI object}); debugLog ($paref, 'aiProcess', qq{AI trained and saved data into file: }.$aitrained.$name); } @@ -17476,7 +17790,7 @@ sub aiTrain { aicanuse => 'ok' } ), ""); - + return $serial; } @@ -17583,11 +17897,11 @@ sub aiGetResult { my $rad1h = NexthoursVal ($hash, $nhtstr, 'rad1h', 0); return "no rad1h for hod: $hod" if($rad1h <= 0); - + my $cst = [gettimeofday]; # aiGetResult Startzeit debugLog ($paref, 'aiData', "Start AI result check for hod: $hod"); - + my $wcc = NexthoursVal ($hash, $nhtstr, 'wcc', 0); my $wid = NexthoursVal ($name, $nhtstr, 'weatherid', 0); my $rr1c = NexthoursVal ($hash, $nhtstr, 'rr1c', 0); @@ -17598,7 +17912,7 @@ sub aiGetResult { my $tbin = temp2bin ($temp); my $cbin = cloud2bin ($wcc); my $sabin = sunalt2bin ($sunalt); - + my $new_data = { # Prognose für neue Daten rad1h => $rad1h, temp => $tbin, @@ -17609,16 +17923,16 @@ sub aiGetResult { sunaz => $sunaz, hod => $hod }; - + ## Accurate Decision ###################### my @total_prediction; my $tn = 0; - + for my $dtree (@{$objref}) { $tn++; my $res; - + eval { $res = $dtree->get_result (attributes => $new_data); push @total_prediction, $res if(defined $res); 1; @@ -17626,32 +17940,32 @@ sub aiGetResult { or do { Log3 ($name, 1, "$name - aiGetResult ERROR: $@"); return $@; }; - + debugLog ($paref, 'aiData', "got AI result from Tree number $tn: $res") if(defined $res); } - + my $tprnum = scalar @total_prediction; if ($tprnum) { my $avg_prediction = sprintf '%.0f', avgArray (\@total_prediction, $tprnum); # my $avg_prediction = sprintf '%.0f', medianArray (\@total_prediction); - + debugLog ($paref, 'aiData', qq{AI accurate result found: pvaifc: $avg_prediction (hod: $hod, sunaz: $sunaz, sunalt: $sabin, Rad1h: $rad1h, wcc: $wcc, rr1c: $rr1c, temp: $tbin)}); return ('accurate', $avg_prediction); } - + ## Spread Decision #################### # undef @total_prediction; # $tn = 0; - + # debugLog ($paref, 'aiData', qq{AI no accurate result found with initial value "Rad1h: $rad1h" (hod: $hod)}); - + # for my $dtree (@{$objref}) { # $tn++; - + # debugLog ($paref, 'aiData', "Start get AI spreaded result from Tree number $tn"); - + # my ($msg, $res) = _aiGetSpread ( { name => $name, # rad1h => $rad1h, # temp => $tbin, @@ -17665,8 +17979,8 @@ sub aiGetResult { # debug => $paref->{debug} # } # ); - -# push @total_prediction, $res if($msg eq 'spreaded'); + +# push @total_prediction, $res if($msg eq 'spreaded'); # } # my $sprnum = scalar @total_prediction; @@ -17675,13 +17989,13 @@ sub aiGetResult { # if ($sprnum) { # my $avg_prediction = sprintf '%.0f', avgArray (\@total_prediction, $sprnum); # # my $avg_prediction = sprintf '%.0f', medianArray (\@total_prediction); - + # debugLog ($paref, 'aiData', qq{AI spreaded result found: pvaifc: $avg_prediction (hod: $hod, sunaz: $sunaz, sunalt: $sabin, Rad1h: $rad1h, wcc: $wcc, rr1c: $rr1c, temp: $tbin)}); # return ('spreaded', $avg_prediction); # } setTimeTracking ($hash, $cst, 'aiLastGetResultTime'); # aiGetResult-Laufzeit ermitteln - + return 'No AI decision delivered'; } @@ -17820,7 +18134,7 @@ sub aiAddRawData { } last if(int $pvd > int $day); - + if (!$ood) { # V 1.47.2 -> für manuelles Auffüllen mit Setter $dayname = HistoryVal ($hash, $pvd, 99, 'dayname', undef); } @@ -17943,11 +18257,11 @@ return $ridx; ################################################################ # Hilfsfunktion zum Erstellen einer Stichprobe ################################################################ -sub sample_data { +sub sample_data { my $data = shift; my @shuffled = shuffle @$data; my $sample_size = int (scalar (@$data) * 0.8); - + return @shuffled[0 .. $sample_size - 1]; # Teilmenge zurückgeben } @@ -18956,6 +19270,7 @@ sub _listDataPoolApiData { my $sp2 = _ldpspaces ($tag, $sp1); while (my ($tag1, $item1) = each %{$git->($itref->{$idx}{$tag})}) { + $item1 //= '-'; $sq .= ($s2 ? $sp2 : "")."$tag1: ".$item1."\n"; $s2 = 1; delete $itref->{$idx}{$tag}{$tag1}; @@ -20373,7 +20688,7 @@ sub getConsumerMintime { my $tdiff = (CurrentVal ($hash, 'sunsetTodayTs', 0) + $setshift) - (defined $startts ? $startts : $sunrisestartts); - + $mintime = sprintf '%.1f', ($tdiff / 60); if ($debug =~ /consumerPlanning/x && !$nolog) { @@ -20397,17 +20712,17 @@ sub getConsumerMintime { if ($debug =~ /consumerPlanning/x && !$nolog) { Log3 ($name, 1, "$name DEBUG> consumer '$c' - use Device:Reading -> '$mintime' for getting mintime"); } - + my ($dv, $rd) = split ':', $mintime; ($err) = isDeviceValid ( { name => $name, obj => $dv, method => 'string' } ); - + if (!$err) { $mintime = ReadingsVal ($dv, $rd, ''); - + if (!isNumeric ($mintime)) { $err = "The reading '$rd' of device '$dv' is invalid or doesn't contain a numeric value"; } - } + } } return ($err, $mintime); @@ -20468,13 +20783,15 @@ sub setModel { elsif ($radapi =~ /VictronKI-/xs) { $hash->{MODEL} = 'VictronKiAPI'; } elsif ($radapi =~ /OpenMeteoDWDEnsemble-/xs) { $hash->{MODEL} = 'OpenMeteoDWDEnsembleAPI'; } elsif ($radapi =~ /OpenMeteoDWD-/xs) { $hash->{MODEL} = 'OpenMeteoDWDAPI'; } + elsif ($radapi =~ /OpenMeteoDWD_D2-/xs) { $hash->{MODEL} = 'OpenMeteoDWDD2API'; } elsif ($radapi =~ /OpenMeteoWorld-/xs) { $hash->{MODEL} = 'OpenMeteoWorldAPI'; } else { $hash->{MODEL} = 'DWD'; } if ($wthapi =~ /OpenMeteoDWDEnsemble-/xs) { $hash->{WEATHERMODEL} = 'OpenMeteoDWDEnsembleAPI'; } elsif ($wthapi =~ /OpenMeteoDWD-/xs) { $hash->{WEATHERMODEL} = 'OpenMeteoDWDAPI'; } + elsif ($wthapi =~ /OpenMeteoDWD_D2-/xs) { $hash->{WEATHERMODEL} = 'OpenMeteoDWDD2API'; } elsif ($wthapi =~ /OpenMeteoWorld-/xs) { $hash->{WEATHERMODEL} = 'OpenMeteoWorldAPI'; } - else { $hash->{WEATHERMODEL} = 'DWD'; } + else { $hash->{WEATHERMODEL} = 'DWD'; } return; } @@ -22720,11 +23037,16 @@ to ensure that the system configuration is correct.
    - - - - -
    addInstAndTrain The AI is enriched with the currently available PV, radiation and environmental data.
    The AI is then trained using the historical data.
    Successfully generated decision trees are saved in the file system.
    addRawData Relevant PV, radiation and environmental data are extracted and stored for later use.
    + addInstAndTrain The AI is enriched with the currently available PV, radiation and environmental data. + The AI is then trained using the historical data. + Successfully generated decision trees are saved in the file system. + + addRawData Relevant PV, radiation and environmental data are extracted and stored for later use. + + rawDataGHIreplace Historical GHI (Global Horizontal Irradiance) values are retrieved from the Open-Meteo service and the values in aiRawData + (see get ... valDecTree aiRawData) replaces existing values ‘rad1h’ + or adds them if they are not available. +
@@ -23048,7 +23370,7 @@ to ensure that the system configuration is correct.
    - + @@ -23442,10 +23764,11 @@ to ensure that the system configuration is correct.
    aiData deletes an existing AI instance including all training data and reinitialises it
    aiData deletes an existing AI instance including all training and raw data and reinitializes it
    batteryTriggerSet deletes the trigger points of the battery storage
    consumerPlanning deletes the planning data of all registered consumers
    To delete the planning data of only one consumer, use:
    - - - - + + + + +
    Rad1h if available, expected global irradiation (GI) or global tilt irradiation (GTI) in kJ/m2
    pv_estimateXX Expected PV generation (Wh)
    KI-based expected PV generation (Wh) of the VictronKI-API
    KI-based_co expected energy consumption (Wh) of the VictronKI-API
    Rad1h expected global irradiation (GI) in kJ/m2
    GTIWh expected Global Tilted Radiation (GTI) in Wh/m2
    pv_estimateXX Expected PV generation (Wh)
    KI-based expected PV generation (Wh) of the VictronKI-API
    KI-based_co expected energy consumption (Wh) of the VictronKI-API
@@ -23614,24 +23937,6 @@ to ensure that the system configuration is correct.

      - -
    • affectBatteryPreferredCharge
      - Consumers with the can mode are only switched on when the specified battery charge (%) - is reached.
      - Consumers with the must mode do not observe the priority charging of the battery.
      - (default: 0) -
    • -
      - - -
    • affectConsForecastInPlanning
      - If set, the consumption forecast is also taken into account in addition to the PV forecast when scheduling the - consumer.
      - Standard consumer planning is based on the PV forecast only.
      - (default: 0) -
    • -
      -
    • affectConsForecastIdentWeekdays
      If set, only the same weekdays (Mon..Sun) are included in the calculation of the consumption forecast.
      @@ -23667,12 +23972,12 @@ to ensure that the system configuration is correct. (default: 50)

    • - +
    • aiControl <Schlüssel1=Wert1> <Schlüssel2=Wert2> ...
      By optionally specifying the following key=value pairs, various properties of the AI support can be properties of the AI support can be influenced.
      - AI support for PV forecast autocorrection is activated with the set command + AI support for PV forecast autocorrection is activated with the set command pvCorrectionFactor_Auto switched on.
      The entry can be made in several lines.

      @@ -23699,17 +24004,11 @@ to ensure that the system configuration is correct.
        Example:
        - attr <name> aiControl aiTrainStart=7 aiStorageDuration=3000 aiTreesPV=3 + attr <name> aiControl aiTrainStart=7 aiStorageDuration=3000 aiTreesPV=3

    • - - -
    • alias
      - In connection with "ctrlShowLink" any display name. -
    • -
    • consumerAdviceIcon
      @@ -23818,7 +24117,7 @@ to ensure that the system configuration is correct. must - The consumer is optimally planned, even if there will probably not be enough PV surplus.            The load is started even if there is insufficient PV surplus, provided that a set "swoncond" condition is met and "swoffcond" is not met. - <Device>:<Reading> - Device/Reading combination to be able to change the planning mode dynamically. + <Device>:<Reading> - Device/Reading combination to be able to change the planning mode dynamically. The reading must return 'can' or 'must'. icon Icon and, if applicable, its color for displaying the consumer in the overview graphic (optional) @@ -23882,7 +24181,7 @@ to ensure that the system configuration is correct. default - the PV surplus is read directly from the 'Current_Surplus' reading. (default) median - the median of the last PV surplus measurements (max. 20) is used. 2 .. 20 - the PV surplus used is calculated from the average of the specified number of measured values. - <Device>:<Reading> - Device/Reading combination that provides a numerical PV surplus value in Watt + <Device>:<Reading> - Device/Reading combination that provides a numerical PV surplus value in Watt determined or calculated by the user. spignorecond Condition to ignore a missing PV surplus (optional). If the condition is fulfilled, the load is switched on according to @@ -24110,13 +24409,6 @@ to ensure that the system configuration is correct.

    • - -
    • ctrlShowLink
      - Display of the link to the detailed view of the device above the graphic area.
      - (default: 1) -
    • -
      -
    • ctrlSolCastAPImaxReq
      (only when using Model SolCastAPI)

      @@ -24166,7 +24458,7 @@ to ensure that the system configuration is correct. SunMinutes_Remain the remaining minutes until sunset of the current day SunHours_Remain the remaining hours until sunset of the current day todayConsumption the energy consumption of the house on the current day - todayConsumptionForecastDayConsumption forecast for the current day + todayConsumptionForecastDayConsumption forecast for the current day todayConsumptionForecast Consumption forecast per hour of the current day (01-24) todayConForecastTillSunset Consumption forecast from current hour to hour before sunset todayDoneAPIcalls the number of radiation data API calls executed on the current day @@ -24425,7 +24717,7 @@ to ensure that the system configuration is correct. The notations are always specified within two curly brackets {...}.

      Note: Values with the units 'Wh' or 'kWh' are automatically converted and displayed according to the setting - automatically converted and displayed according to the setting of attribute + automatically converted and displayed according to the setting of attribute graphicEnergyUnit.

      @@ -24600,6 +24892,43 @@ to ensure that the system configuration is correct. Color of the weather icons for the night hours.

    • + + +
    • plantControl <Schlüssel1=Wert1> <Schlüssel2=Wert2> ...
      + By optionally specifying the 'Key=Value' pairs listed below, various properties of the overall + properties of the overall system can be set.
      + The entry can be made in several lines. +

      + +
        + + + + + + + + + + + + + + + + + + +
        batteryPreferredCharge Consumers with the can mode are only switched on when the specified battery charge (%) is reached.
        Consumers with the must mode do not observe the priority charging of the battery.
        Wert: Integer 0..100, default: 0
        feedinPowerLimit Feed-in limit of the entire system into the public grid in watts.
        SolarForecast does not limit the feed-in, but uses this information
        within the battery charge management to avoid system curtailment.
        Value: Integer, default: unlimited
        consForecastInPlanning The key determines the procedure for scheduling registered consumers.
        0 - the consumers are scheduled on the basis of the PV forecast (default)
        1 - consumers are scheduled on the basis of the PV forecast and the consumption forecast
        showLink Display of a link to the detailed view of the device above the graphics area
        0 - Display off, 1 - Display on, default: 0
        +
      + +
        + Beispiel:
        + attr <name> plantControl feedinPowerLimit=4800 consForecastInPlanning=1 showLink=1 +
      + +
    • +
    • setupBatteryDevXX <Battery Device Name> pin=<Readingname>:<Unit> pout=<Readingname>:<Unit> @@ -24616,23 +24945,23 @@ to ensure that the system configuration is correct. - + - + - - + + - - + + - + - + @@ -24644,8 +24973,7 @@ to ensure that the system configuration is correct. - - + @@ -24874,6 +25202,13 @@ to ensure that the system configuration is correct. the service's website.

      + OpenMeteoDWD_D2-API
      + + Like OpenMeteoDWD-API. However, only the ICON D2 model is used for Central Europe + (Germany, Switzerland, Austria, France, Belgium, Netherlands, Denmark, Czech Republic, Slovenia) is used. + The spatial resolution of this model is 0.02° (approx. 2 km) and a temporal resolution of 15 minutes. +

      + OpenMeteoDWDEnsemble-API
      This Open-Meteo API variant provides access to the DWD's global @@ -25011,6 +25346,13 @@ to ensure that the system configuration is correct. the service's website.

      + OpenMeteoDWD_D2-API
      + + Like OpenMeteoDWD-API. However, only the ICON D2 model is used for Central Europe + (Germany, Switzerland, Austria, France, Belgium, Netherlands, Denmark, Czech Republic, Slovenia) is used. + The spatial resolution of this model is 0.02° (approx. 2 km) and a temporal resolution of 15 minutes. +

      + OpenMeteoDWDEnsemble-API
      This Open-Meteo API variant provides access to the DWD's global @@ -25198,11 +25540,16 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.
      pin Reading which provides the current battery charging power
      pout Reading which provides the current battery discharge rate
      intotal Reading which provides the total battery charge as a continuous counter (optional)
      If the reading violates the specification of a continuously rising counter, SolarForecast handles
      this error and reports the situation that has occurred with a log entry with verbose 2.
      this error and reports the situation that has occurred with a log entry with verbose 2.
      outtotal Reading which provides the total battery discharge as a continuous counter (optional)
      If the reading violates the specification of a continuously rising counter, SolarForecast handles
      this error and reports the situation that has occurred with a log entry with verbose 2.
      this error and reports the situation that has occurred with a log entry with verbose 2.
      cap installed battery capacity. Option can be:
      numerical value - direct specification of the battery capacity in Wh without specifying the unit!
      <Readingname>:<unit> - Reading which provides the capacity and unit (Wh, kWh)
      charge Reading which provides the current state of charge (SOC in percent) (optional)
      Unit the respective unit (W,Wh,kW,kWh)
      icon Icon and/or (only) colour for displaying the battery in the bar chart (optional)
      show Control of the battery display in the bar graph (optional)
      0 - no display of the device (default)
      1[:top|bottom] - Display of the device in the bar chart level 1 (above|below) the beam
      2[:top|bottom] - Display of the device in the bar chart level 2 (above|below) the beam
      1..3[:top|bottom] - display of the device in level 1,2 or 3 (above|below) the bar
      asynchron Data collection mode according to the ctrlInterval setting (synchronous) or additionally by
      event processing (asynchronous).
      - + - -
      addInstAndTrain Die KI wird mit den aktuell vorhandenen PV-, Strahlungs- und Umweltdaten angereichert.
      addInstAndTrain Die KI wird mit den aktuell vorhandenen PV-, Strahlungs- und Umweltdaten angereichert.
      Anschließend wird die KI mit den historischen Daten trainiert.
      Erfolgreich generierte Entscheidungsbäume werden im Filesystem gespeichert.
      addRawData Relevante PV-, Strahlungs- und Umweltdaten werden extrahiert und für die spätere Verwendung gespeichert.
      + + addRawData Relevante PV-, Strahlungs- und Umweltdaten werden extrahiert und für die spätere Verwendung gespeichert. + + rawDataGHIreplace Es werden historische GHI (Global Horizontal Irradiance) Werte vom Open-Meteo Dienst abgerufen und die in aiRawData + (siehe get ... valDecTree aiRawData) vorhanden Werte 'rad1h' ersetzt bzw. + ergänzt wenn sie nicht vorhanden sind. +
@@ -25535,7 +25882,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.
    - + @@ -25930,10 +26277,11 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.
    aiData löscht eine vorhandene KI Instanz inklusive aller Trainingsdaten und initialisiert sie neu
    aiData löscht eine vorhandene KI Instanz inklusive aller Trainings- und Rohdaten und initialisiert sie neu
    batteryTriggerSet löscht die Triggerpunkte des Batteriespeichers
    consumerPlanning löscht die Planungsdaten aller registrierten Verbraucher
    Um die Planungsdaten nur eines Verbrauchers zu löschen verwendet man:
    - - - - + + + + +
    Rad1h wenn vorhanden, erwartete Globalstrahlung (GI) bzw. globale Schräglagenstrahlung (GTI) in kJ/m2
    pv_estimateXX erwartete PV Erzeugung (Wh)
    KI-based erwartete PV Erzeugung (Wh) der VictronKI-API
    KI-based_co erwarteter Energieverbrauch (Wh) der VictronKI-API
    Rad1h erwartete Globalstrahlung (GI) in kJ/m2
    GTIWh erwartete globale Strahlung auf die geneigte Fläche (GTI) in Wh/m2
    pv_estimateXX erwartete PV Erzeugung (Wh)
    KI-based erwartete PV Erzeugung (Wh) der VictronKI-API
    KI-based_co erwarteter Energieverbrauch (Wh) der VictronKI-API
@@ -26100,24 +26448,6 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.

      - -
    • affectBatteryPreferredCharge
      - Es werden Verbraucher mit dem Mode can erst dann eingeschaltet, wenn die angegebene Batterieladung (%) - erreicht ist.
      - Verbraucher mit dem Mode must beachten die Vorrangladung der Batterie nicht.
      - (default: 0) -
    • -
      - - -
    • affectConsForecastInPlanning
      - Wenn gesetzt, wird bei der Einplanung der Consumer zusätzlich zur PV Prognose ebenfalls die Prognose - des Verbrauchs berücksichtigt.
      - Die Standardplanung der Consumer erfolgt lediglich auf Grundlage der PV Prognose.
      - (default: 0) -
    • -
      -
    • affectConsForecastIdentWeekdays
      Wenn gesetzt, werden zur Berechnung der Verbrauchsprognose nur gleiche Wochentage (Mo..So) einbezogen.
      @@ -26154,12 +26484,12 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. (default: 50)

    • - +
    • aiControl <Schlüssel1=Wert1> <Schlüssel2=Wert2> ...
      Durch die optionale Angabe der nachfolgend aufgeführten Schlüssel=Wert Paare können verschiedene Eigenschaften der KI Unterstützung beeinflusst werden.
      - Die KI Unterstützung der PV Prognose Autokorrektur wird mit dem Set-Befehl + Die KI Unterstützung der PV Prognose Autokorrektur wird mit dem Set-Befehl pvCorrectionFactor_Auto eingeschaltet.
      Die Eingabe kann mehrzeilig erfolgen.

      @@ -26186,18 +26516,12 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.
        Beispiel:
        - attr <name> aiControl aiTrainStart=7 aiStorageDuration=3000 aiTreesPV=3 + attr <name> aiControl aiTrainStart=7 aiStorageDuration=3000 aiTreesPV=3

    • - -
    • alias
      - In Verbindung mit "ctrlShowLink" ein beliebiger Anzeigename. -
    • -
      -
    • consumerAdviceIcon
      Definiert die Art der Information über die geplanten Schaltzeiten eines Verbrauchers in der Verbraucherlegende. @@ -26304,7 +26628,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. must - der Verbraucher wird optimiert eingeplant auch wenn wahrscheinlich nicht genügend PV Überschuß vorhanden sein wird            Der Start des Verbrauchers erfolgt auch bei ungenügendem PV-Überschuß sofern eine gesetzte "swoncond" Bedingung erfüllt und "swoffcond" nicht erfüllt ist. - <Device>:<Reading> - Device/Reading Kombination um den Planungsmodus dynamisch ändern zu können. + <Device>:<Reading> - Device/Reading Kombination um den Planungsmodus dynamisch ändern zu können. Das Reading muß 'can' oder 'must' zurückgeben. icon Icon und ggf. dessen Farbe zur Darstellung des Verbrauchers in der Übersichtsgrafik (optional) @@ -26368,7 +26692,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. default - der PV-Überschuß wird aus dem Reading 'Current_Surplus' direkt ausgelesen. (default) median - es wird der Median der letzten PV-Überschuß Messungen (max. 20) verwendet. 2 .. 20 - der verwendete PV-Überschuß wird als Durchschnitt der angegebenen Anzahl Meßwerte gebildet. - <Device>:<Reading> - Device/Reading-Kombination die einen vom Nutzer bestimmten bzw. berechneten + <Device>:<Reading> - Device/Reading-Kombination die einen vom Nutzer bestimmten bzw. berechneten numerischen PV-Überschuß in Watt liefert. spignorecond Bedingung um einen fehlenden PV Überschuß zu ignorieren (optional). Bei erfüllter Bedingung wird der Verbraucher entsprechend @@ -26597,13 +26921,6 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.

    • - -
    • ctrlShowLink
      - Anzeige des Links zur Detailansicht des Device über dem Grafikbereich
      - (default: 1) -
    • -
      -
    • ctrlSolCastAPImaxReq
      (nur bei Verwendung Model SolCastAPI)

      @@ -26653,7 +26970,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. SunMinutes_Remain die verbleibenden Minuten bis Sonnenuntergang des aktuellen Tages SunHours_Remain die verbleibenden Stunden bis Sonnenuntergang des aktuellen Tages todayConsumption der Energieverbrauch des Hauses am aktuellen Tag - todayConsumptionForecastDayVerbrauchsprognose für den aktuellen Tag + todayConsumptionForecastDayVerbrauchsprognose für den aktuellen Tag todayConsumptionForecast Verbrauchsprognose pro Stunde des aktuellen Tages (01-24) todayConForecastTillSunset Verbrauchsprognose von aktueller Stunde bis Stunde vor Sonnenuntergang todayDoneAPIcalls die Anzahl der am aktuellen Tag ausgeführten Strahlungsdaten-API Calls @@ -27084,6 +27401,43 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. Farbe der Wetter-Icons für die Nachtstunden.

    • + + +
    • plantControl <Schlüssel1=Wert1> <Schlüssel2=Wert2> ...
      + Durch die optionale Angabe der nachfolgend aufgeführten 'Schlüssel=Wert' Paare können verschiedene + Eigenschaften der Gesamtanlage eingestellt werden.
      + Die Eingabe kann mehrzeilig erfolgen. +

      + +
        + + + + + + + + + + + + + + + + + + +
        batteryPreferredCharge Verbraucher mit dem Mode can werden erst dann eingeschaltet, wenn die angegebene Batterieladung (%) erreicht ist.
        Verbraucher mit dem Mode must beachten die Vorrangladung der Batterie nicht.
        Wert: Ganzzahl 0..100, default: 0
        feedinPowerLimit Einspeiselimit der Gesamtanlage in das öffentliche Netz in Watt.
        SolarForecast limitiert die Einspeisung nicht, verwendet diese Angabe jedoch
        innerhalb des Batterie-Lademanagements zur Vermeidung einer Anlagenabregelung.
        Wert: Ganzzahl, default: unbegrent
        consForecastInPlanning Der Schlüssel bestimmt die Vorgehensweise bei der Einplanung der registrierten Verbraucher.
        0 - die Einplanung der Verbraucher erfolgt auf Grundlage der PV Prognose (default)
        1 - die Einplanung der Verbraucher erfolgt auf Grundlage der PV Prognose und der Prognose des Verbrauchs
        showLink Anzeige eines Links zur Detailansicht des Device über dem Grafikbereich
        0 - Anzeige aus, 1 - Anzeige an, default: 0
        +
      + +
        + Beispiel:
        + attr <name> plantControl feedinPowerLimit=4800 consForecastInPlanning=1 showLink=1 +
      + +
    • +
    • setupBatteryDevXX <Batterie Device Name> pin=<Readingname>:<Einheit> pout=<Readingname>:<Einheit> @@ -27105,12 +27459,12 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. intotal Reading welches die totale Batterieladung als fortlaufenden Zähler liefert (optional) Sollte des Reading die Vorgabe eines stetig aufsteigenden Zählers verletzen, behandelt - SolarForecast diesen Fehler und meldet die aufgetretene Situation durch einen Logeintrag mit verbose 2. + SolarForecast diesen Fehler und meldet die aufgetretene Situation durch einen Logeintrag mit verbose 2. outtotal Reading welches die totale Batterieentladung als fortlaufenden Zähler liefert (optional) Sollte des Reading die Vorgabe eines stetig aufsteigenden Zählers verletzen, behandelt - SolarForecast diesen Fehler und meldet die aufgetretene Situation durch einen Logeintrag mit verbose 2. - + SolarForecast diesen Fehler und meldet die aufgetretene Situation durch einen Logeintrag mit verbose 2. + cap installierte Batteriekapazität. Option kann sein: numerischer Wert - direkte Angabe der Batteriekapazität in Wh ohne die Einheit anzugeben! <Readingname>:<Einheit> - Reading welches die Kapazität liefert und Einheit (Wh, kWh) @@ -27127,8 +27481,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. show Steuerung der Anzeige der Batterie in der Balkengrafik (optional) 0 - keine Anzeige des Gerätes (default) - 1[:top|bottom] - Anzeige des Gerätes in der Balkengrafik Ebene 1 (über|unter) den Balken - 2[:top|bottom] - Anzeige des Gerätes in der Balkengrafik Ebene 2 (über|unter) den Balken + 1..3[:top|bottom] - Anzeige des Gerätes in der Ebene 1,2 oder 3 (über|unter) den Balken asynchron Modus der Datensammlung entsprechend Einstellung ctrlInterval (synchron) oder zusätzlich durch Eventverarbeitung (asynchron). @@ -27358,6 +27711,13 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. API Dokumentation verfügbar.

      + OpenMeteoDWD_D2-API
      + + Wie OpenMeteoDWD-API. Es wird jedoch nur das Modell ICON D2 für Mitteleuropa + (Deutschland, Schweiz, Österreich, Frankreich, Belgien, Niederlande, Dänemark, Tschechien, Slowenien) verwendet. + Die Raumauflösung dieses Modells beträgt 0,02° (ca. 2 km) und eine zeitliche Auflösung von 15 Minuten. +

      + OpenMeteoDWDEnsemble-API
      Diese Open-Meteo API Variante bietet Zugang zum globalen @@ -27497,6 +27857,13 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden. API Dokumentation verfügbar.

      + OpenMeteoDWD_D2-API
      + + Wie OpenMeteoDWD-API. Es wird jedoch nur das Modell ICON D2 für Mitteleuropa + (Deutschland, Schweiz, Österreich, Frankreich, Belgien, Niederlande, Dänemark, Tschechien, Slowenien) verwendet. + Die Raumauflösung dieses Modells beträgt 0,02° (ca. 2 km) und eine zeitliche Auflösung von 15 Minuten. +

      + OpenMeteoDWDEnsemble-API
      Diese Open-Meteo API Variante bietet Zugang zum globalen