diff --git a/fhem/contrib/DS_Starter/76_SolarForecast.pm b/fhem/contrib/DS_Starter/76_SolarForecast.pm index 9fe2d9b06..44c66a76e 100644 --- a/fhem/contrib/DS_Starter/76_SolarForecast.pm +++ b/fhem/contrib/DS_Starter/76_SolarForecast.pm @@ -160,8 +160,9 @@ BEGIN { # Versions History intern my %vNotesIntern = ( - "1.58.6" => "25.09.2025 __batChargeMgmt code changed, new sub ___batChargeSaveResults, remove reading Battery_ChargeRecommended_XX ". - "_calcReadingsTomorrowPVFc: bugfix generating readings of tomorrow ", + "1.58.6" => "01.10.2025 __batChargeMgmt code changed, new sub ___batChargeSaveResults, remove reading Battery_ChargeRecommended_XX ". + "_calcReadingsTomorrowPVFc: bugfix generating readings of tomorrow ". + "__batChargeOptTargetPower: complete rework, Attr ctrlBatSocManagementXX new key 'loadStrategy' ", "1.58.5" => "24.09.2025 __batChargeOptTargetPower: fix if battery load control is deactivated ", "1.58.4" => "23.09.2025 __batChargeOptTargetPower: user a better surplus value, excess based on average removed & some other code optimization ", "1.58.3" => "17.09.2025 __batChargeOptTargetPower: minor code change, consider bpinmax & lcintime ", @@ -1566,10 +1567,15 @@ my %hfspvh = ( $hfspvh{'batprogsoc'.$bn}{validkey} = undef; $hfspvh{'batprogsoc'.$bn}{fpar} = undef; - $hfspvh{'lcintimebat'.$bn}{fn} = \&_storeVal; # Ladesteurung der Batterie In Time, d.h. war sie aktiv? (1 - Ja, 0 - Nein) + $hfspvh{'lcintimebat'.$bn}{fn} = \&_storeVal; # Ladesteuerung der Batterie In Time, d.h. war sie aktiv? (1 - Ja, 0 - Nein) $hfspvh{'lcintimebat'.$bn}{storname} = 'lcintimebat'.$bn; $hfspvh{'lcintimebat'.$bn}{validkey} = undef; $hfspvh{'lcintimebat'.$bn}{fpar} = undef; + + $hfspvh{'strategybat'.$bn}{fn} = \&_storeVal; # Ladestrategie der Batterie + $hfspvh{'strategybat'.$bn}{storname} = 'strategybat'.$bn; + $hfspvh{'strategybat'.$bn}{validkey} = undef; + $hfspvh{'strategybat'.$bn}{fpar} = undef; $hfspvh{'batmaxsoc'.$bn}{fn} = \&_storeVal; # max. erreichter SOC des Tages $hfspvh{'batmaxsoc'.$bn}{storname} = 'batmaxsoc'.$bn; @@ -7519,6 +7525,7 @@ sub _attrBatSocManagement { ## no critic "not used" careCycle => { comp => '\d+', must => 0, act => 0 }, loadAbort => { comp => '(?:100|[1-9]?[0-9]):\d+(?::(?:100|[1-9]?[0-9]))?', must => 0, act => 0 }, safetyMargin => { comp => '(?:100|[1-9]?\d)(?::(?:100|[1-9]?\d))?', must => 0, act => 0 }, + loadStrategy => { comp => '(loadRelease|optPower)', must => 0, act => 0 }, }; my ($a, $h) = parseParams ($aVal); @@ -11532,14 +11539,15 @@ sub __parseAttrBatSoc { ($urMargin, $otpMargin) = split ':', $ph->{safetyMargin} if(defined $ph->{safetyMargin}); my $parsed = { - lowSoc => $ph->{lowSoc}, - upSoc => $ph->{upSoC}, - maxSoc => $ph->{maxSoC} // MAXSOCDEF, # optional (default: MAXSOCDEF) - careCycle => $ph->{careCycle} // CARECYCLEDEF, # Ladungszyklus (Maintenance) für maxSoC in Tagen - lcslot => $ph->{lcSlot}, - loadAbort => $ph->{loadAbort}, - urMargin => $urMargin, - otpMargin => $otpMargin, + lowSoc => $ph->{lowSoc}, + upSoc => $ph->{upSoC}, + maxSoc => $ph->{maxSoC} // MAXSOCDEF, # optional (default: MAXSOCDEF) + careCycle => $ph->{careCycle} // CARECYCLEDEF, # Ladungszyklus (Maintenance) für maxSoC in Tagen + lcslot => $ph->{lcSlot}, + loadAbort => $ph->{loadAbort}, + loadStrategy => $ph->{loadStrategy}, + urMargin => $urMargin, + otpMargin => $otpMargin, }; return $parsed; @@ -11603,7 +11611,8 @@ sub _batChargeMgmt { my $hsurp = {}; # Hashreferenz Überschuß my $hsoc = {}; # Hashreferenz my $values = {}; # Hashreferenz - + my $progsoc; + ## Inverter Limits ermitteln ############################## for my $in (1..MAXINVERTER) { @@ -11647,22 +11656,25 @@ sub _batChargeMgmt { my $batoptsoc = ReadingsNum ($name, 'Battery_OptimumTargetSoC_'.$bn, 0); # aktueller optimierter SoC my $confcss = CurrentVal ($name, 'tdConFcTillSunset', 0); # Verbrauchsprognose bis Sonnenuntergang my $csoc = BatteryVal ($name, $bn, 'bcharge', 0); # aktuelle Ladung in % + my $csocwh = BatteryVal ($name, $bn, 'bchargewh', 0); # aktuelle Ladung in Wh my $bpinmax = BatteryVal ($name, $bn, 'bpinmax', INFINITE); # max. mögliche Ladeleistung W my $bpoutmax = BatteryVal ($name, $bn, 'bpoutmax', INFINITE); # max. mögliche Entladeleistung W my $bpowerin = BatteryVal ($name, $bn, 'bpowerin', INFINITE); # aktuelle Ladeleistung W my $cgbt = AttrVal ($name, 'ctrlBatSocManagement'.$bn, undef); my $sf = __batCapShareFactor ($hash, $bn); # Anteilsfaktor der Batterie XX Kapazität an Gesamtkapazität + my $strategy = 'loadRelease'; # 'loadRelease' oder 'optPower' my $lowSoc = 0; my $loadAbort = ''; my ($lcslot, $urMargin, $otpMargin); if ($cgbt) { - my $parsed = __parseAttrBatSoc ($name, $cgbt); - $lowSoc = $parsed->{lowSoc} // 0; - $lcslot = $parsed->{lcslot}; - $loadAbort = $parsed->{loadAbort}; - $urMargin = $parsed->{urMargin}; - $otpMargin = $parsed->{otpMargin}; + my $parsed = __parseAttrBatSoc ($name, $cgbt); + $lowSoc = $parsed->{lowSoc} // 0; + $lcslot = $parsed->{lcslot}; + $loadAbort = $parsed->{loadAbort}; + $urMargin = $parsed->{urMargin}; + $otpMargin = $parsed->{otpMargin}; + $strategy = $parsed->{loadStrategy} // $strategy; } my $margin = defined $urMargin ? $urMargin : SFTYMARGIN_50; # Sicherheitszuschlag (%) @@ -11717,6 +11729,8 @@ sub _batChargeMgmt { my $nhr = sprintf "%02d", $num; my $hod = NexthoursVal ($name, 'NextHour'.$nhr, 'hourofday', undef); my $nhstt = NexthoursVal ($name, 'NextHour'.$nhr, 'starttime', undef); + my $stt = (split /[-:]/, $nhstt)[2]; + $stt =~ s/\s/\//; next if(!defined ($hod) || !defined ($nhstt)); @@ -11725,8 +11739,8 @@ sub _batChargeMgmt { my $pvfc = NexthoursVal ($name, 'NextHour'.$nhr, 'pvfc', 0); if ($fd == 2 && $fh == 0) { - $tompvfc = CurrentVal ($name, 'dayAfterTomorrowPVfc', 0); # PV Prognose übernächster Tag - $tomconfc = CurrentVal ($name, 'dayAfterTomorrowConfc', 0); # Verbrauchsprognose übernächster Tag + $tompvfc = CurrentVal ($name, 'dayAfterTomorrowPVfc', 0); # PV Prognose übernächster Tag + $tomconfc = CurrentVal ($name, 'dayAfterTomorrowConfc', 0); # Verbrauchsprognose übernächster Tag } ## Zeitfenster für aktives Lademanagement anwenden @@ -11779,48 +11793,49 @@ sub _batChargeMgmt { if ( !$cgbt ) {$crel = 1} # Ladefreigabe wenn kein BatSoc-Management if ( !$lcintime ) {$crel = 1} # Ladefreigabe wenn nicht innerhalb Zeitslot für Ladesteuerung if ( $labortCond ) {$crel = 0} # keine Ladefreigabe bei genereller Abbruchbedingung - + # Steuerhash für optimimierte Ladeleistung erstellen ###################################################### my $surplswh = max (0, $surpls); - if ($today) { # nur Heute - $hsurp->{$hod}{hod} = $hod; - $hsurp->{$hod}{nhr} = $nhr; - $hsurp->{$hod}{surpls} = $lcintime ? $surplswh.'.'.$hod : 0; # Überschuß in Wh der Stunde mit Sortierhilfe - $hsurp->{$hod}{$bn}{spday} = $spday; # (Rest)PV-Überschuß am laufenden Tag - $hsurp->{$hod}{$bn}{initsocwh} = $socwh; - $hsurp->{$hod}{$bn}{batinstcap} = $batinstcap; - $hsurp->{$hod}{$bn}{bpinmax} = $bpinmax; # max. mögliche Ladeleistung - $hsurp->{$hod}{$bn}{otpMargin} = $otpMargin; # Sicherheitszuschlag für Berechnungen - $hsurp->{$hod}{$bn}{lcintime} = $lcintime; # Ladesteuerung "In Time" oder "nicht In Time" + if ($strategy eq 'optPower' || $strategy eq 'loadRelease' && $today) { # bei loadRelease' nur den aktuellen Tag betrachten + $hsurp->{$fd}{$hod}{nhr} = $nhr; + $hsurp->{$fd}{$hod}{speff} = $surpls; # effektiver PV Überschuß bzw. effektiver Verbrauch wenn < 0 + $hsurp->{$fd}{$hod}{surplswh} = $surplswh.'.'.$hod; # absoluter Überschuß in Wh der Stunde mit Sortierhilfe + $hsurp->{$fd}{$hod}{$bn}{spday} = $spday; # (Rest)PV-Überschuß am laufenden Tag + $hsurp->{$fd}{$hod}{$bn}{initsocwh} = $socwh; + $hsurp->{$fd}{$hod}{$bn}{batinstcap} = $batinstcap; # installierte Batteriekapazität (Wh) + $hsurp->{$fd}{$hod}{$bn}{bpinmax} = $bpinmax; # max. mögliche Ladeleistung + $hsurp->{$fd}{$hod}{$bn}{bpoutmax} = $bpoutmax; # max. mögliche Entladeleistung + $hsurp->{$fd}{$hod}{$bn}{lowSocwh} = $lowSocwh; # eingestellter lowSoC in Wh + $hsurp->{$fd}{$hod}{$bn}{csocwh} = $csocwh; # aktueller SoC in Wh + $hsurp->{$fd}{$hod}{$bn}{otpMargin} = $otpMargin; # Sicherheitszuschlag für Berechnungen + $hsurp->{$fd}{$hod}{$bn}{lcintime} = $lcintime; # Ladesteuerung "In Time" oder "nicht In Time" + $hsurp->{$fd}{$hod}{$bn}{stt} = $stt; # Day/Time für Debuglog + $hsurp->{$fd}{$hod}{$bn}{strategy} = $strategy; # Ladestrategie } - ## SOC-Prognose - ################# - my $fceff = $surpls; # effektiver PV Überschuß bzw. effektiver Verbrauch wenn < 0 + ## SOC-Prognose LR + #################### + my $speff = $surpls; # effektiver PV Überschuß bzw. effektiver Verbrauch wenn < 0 - $fceff = $fceff > 0 ? ($fceff >= $bpinmax ? $bpinmax : $fceff) : - $fceff < 0 ? ($fceff <= $bpoutmax * -1 ? $bpoutmax * -1 : $fceff) : - $fceff; + $speff = $speff > 0 ? ($speff >= $bpinmax ? $bpinmax : $speff) : + $speff < 0 ? ($speff <= $bpoutmax * -1 ? $bpoutmax * -1 : $speff) : + $speff; - $socwh += $crel ? ($fceff > 0 ? $fceff * STOREFFDEF : $fceff / STOREFFDEF) : - ($fceff > 0 ? 0 : $fceff / STOREFFDEF); # PV Überschuß (d.h. Aufladung) nur einbeziehen wenn Ladefreigabe + $socwh += $crel ? ($speff > 0 ? $speff * STOREFFDEF : $speff / STOREFFDEF) : + ($speff > 0 ? 0 : $speff / STOREFFDEF); # PV Überschuß (d.h. Aufladung) nur einbeziehen wenn Ladefreigabe $socwh = $socwh < $lowSocwh ? $lowSocwh : $socwh < $batoptsocwh ? $batoptsocwh : # SoC Prognose in Wh $socwh > $batinstcap ? $batinstcap : $socwh; - $socwh = sprintf "%.0f", $socwh; - my $progsoc = sprintf "%.1f", (100 * $socwh / $batinstcap); # Prognose SoC in % - + $socwh = sprintf "%.0f", $socwh; + $progsoc = sprintf "%.1f", (100 * $socwh / $batinstcap); # Prognose SoC in % ## Debuglog LR ################ - my $stt = (split /[-:]/, $nhstt)[2]; - $stt =~ s/\s/\//; - 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, inTime: ".($cgbt ? $lcintime : '-'); if ($num) { @@ -11831,67 +11846,98 @@ sub _batChargeMgmt { } } - debugLog ($paref, 'batteryManagement', "Bat $bn ChargeLR $stt -> $crel ($msg)"); + debugLog ($paref, 'batteryManagement', "Bat $bn ChargeLR $stt - lc: $crel, $msg"); ## Fortschreibung ################### $whneed = $batinstcap - $socwh; - ## Speicherung und Readings erstellen - ####################################### + ## Speicherung und Readings erstellen LR + ########################################## $values = { hsoc => $hsoc, - loop => 'LR', - num => $num, - crel => $crel, bn => $bn, - labortCond => $labortCond, - loadAbort => $loadAbort, nhr => $nhr, progsoc => $progsoc, - lcintime => $lcintime, - cgbt => $cgbt, socwh => $socwh, today => $today, hod => $hod, + loopid => 'LR', + strategy => $strategy, + crel => $crel, + labortCond => $labortCond, + loadAbort => $loadAbort, + cgbt => $cgbt, + lcintime => $lcintime, }; ___batChargeSaveResults ($paref, $values); + + $values = {}; } } - # leistungsoptimierte Beladungssteuerung - ########################################## - $paref->{hsurp} = $hsurp; - my ($hopt, $otp) = __batChargeOptTargetPower ($paref); - delete $paref->{hsurp}; - - ## Debuglog OTP - ################# - if ($paref->{debug} =~ /batteryManagement/) { - Log3 ($name, 1, "$name DEBUG> ChargeOTP - The limit for grid feed-in is $feedinlim W"); - Log3 ($name, 1, "$name DEBUG> ChargeOTP - NOTE: The hours listed below are the estimated number of hours remaining on the current day with at least the respective PV surplus."); - - for my $k (sort { $a <=> $b } keys %{$hopt}) { - my @batteries = grep { !/^(?:hod|surpls|nhr)$/xs } keys %{$hsurp->{24}}; - for my $bat (sort @batteries) { - my $sphrs = defined $hopt->{$k}{$bat}{sphrs} ? - '('.($hopt->{$k}{$bat}{sphrs} ? $hopt->{$k}{$bat}{sphrs} : 1).' hrs)' : - ''; - - my $ssocwh = $hopt->{$k}{$bat}{runwh} // '-'; - my $otpMargin = $hopt->{$k}{$bat}{otpMargin}; - my $margin = defined $otpMargin ? $otpMargin : SFTYMARGIN_20; - my $spls = int $hopt->{$k}{surpls}; - my $needmin = max (BatteryVal ($name, $bat, 'bpinreduced', 0), $hopt->{$k}{$bat}{pneedmin} // 0); + # leistungsoptimierte (optPower) Beladungssteuerung + ##################################################### + for my $lfd (0..max (0, keys %{$hsurp})) { + $paref->{hsurp} = $hsurp->{$lfd}; + my ($hopt, $otp) = __batChargeOptTargetPower ($paref); + delete $paref->{hsurp}; + + ## Debuglog OTP + ################# + if ($paref->{debug} =~ /batteryManagement/ && !$lfd) { + Log3 ($name, 1, "$name DEBUG> ChargeOTP - The limit for grid feed-in is $feedinlim W"); + } + + ## Speicherung und Readings erstellen OTP + ########################################### + for my $shod (sort { $a <=> $b } keys %{$hopt}) { + my $nhr = $hopt->{$shod}{nhr}; + my @batteries = grep { !/^(?:fd|speff|surplswh|nhr)$/xs } keys %{$hopt->{24}}; + + for my $bat (sort @batteries) { + my $ssocwh = $hopt->{$shod}{$bat}{runwh} // '-'; + my $fcendwh = $hopt->{$shod}{$bat}{fcendwh} // 0; + $progsoc = sprintf "%.1f", (100 * $fcendwh / $hopt->{$shod}{$bat}{batinstcap}); # Prognose SoC in % + + ## Speicherung und Readings erstellen OTP + ########################################## + $values = { hsoc => $hsoc, + otp => $otp, + bn => $bat, + nhr => $nhr, + progsoc => $progsoc, + socwh => $fcendwh, + hod => $shod, + loopid => 'OTP', + strategy => $hopt->{$shod}{$bat}{strategy}, + }; + + ___batChargeSaveResults ($paref, $values); - if ($hopt->{$k}{nhr} eq '00') { # Target für aktuelle Stunde - $needmin = $otp->{$bat}{target} // 0; + ## Debuglog OTP + ################# + if ($paref->{debug} =~ /batteryManagement/) { + my $lcintime = $hopt->{$shod}{$bat}{lcintime}; + my $spls = int $hopt->{$shod}{surplswh}; + my $pneedmin = $hopt->{$shod}{$bat}{pneedmin}; + my $ttt = $hopt->{$shod}{$bat}{stt}; + + if ($nhr eq '00') { + $pneedmin = $otp->{$bat}{target} // 0; + my $achievable = $hopt->{$shod}{$bat}{achievable}; + my $otpMargin = $hopt->{$shod}{$bat}{otpMargin}; + my $margin = defined $otpMargin ? $otpMargin : SFTYMARGIN_20; + Log3 ($name, 1, "$name DEBUG> Bat $bat ChargeOTP - used safety margin: $margin %"); + Log3 ($name, 1, "$name DEBUG> Bat $bat ChargeOTP - is the charging goal likely to be achieved? - $achievable"); + } + + Log3 ($name, 1, "$name DEBUG> Bat $bat ChargeOTP $ttt - hod: $shod / $nhr, lc: $lcintime, SoC S/E: $ssocwh / $fcendwh Wh, Surplus: $spls Wh, OTP: $pneedmin W"); } - - Log3 ($name, 1, "$name DEBUG> Bat $bat ChargeOTP - hod: $k, Start SoC: $ssocwh Wh, Surplus: $spls Wh $sphrs, OTP: $needmin W, safety: $margin %"); } } } + # prognostizierten SOC über alle Batterien speichern ###################################################### @@ -11941,102 +11987,142 @@ return; sub __batChargeOptTargetPower { my $paref = shift; my $name = $paref->{name}; - my $hsurp = $paref->{hsurp} // return; # Hashref Überschußhash + my $hsurp = $paref->{hsurp}; # Hashref Überschußhash - my $fipl = CurrentVal ($name, 'feedinPowerLimit', INFINITE); - my @sorted = sort { $hsurp->{$a}{surpls} <=> $hsurp->{$b}{surpls} } keys %{$hsurp}; - my $sphrs = scalar grep { int( $hsurp->{$_}{surpls} ) >= 1 } @sorted; # Stunden mit Überschuß >= 1 - my @batteries = grep { !/^(?:hod|surpls|nhr)$/xs } keys %{$hsurp->{24}}; + my $fipl = CurrentVal ($name, 'feedinPowerLimit', INFINITE); + my @sortedhods = sort { $hsurp->{$a}{surplswh} <=> $hsurp->{$b}{surplswh} } keys %{$hsurp}; # Stunden aufsteigend nach PV-Überschuß sortiert + my @batteries = grep { !/^(?:fd|speff|surplswh|nhr)$/xs } keys %{$hsurp->{24}}; my $otp; - for my $shod (@sorted) { - my $spls = int $hsurp->{$shod}{surpls}; - my $newshod = sprintf "%02d", (int $shod + 1); + for my $hod (sort { $a <=> $b } keys %{$hsurp}) { + my $spls = int $hsurp->{$hod}{surplswh}; + my $newshod = sprintf "%02d", (int $hod + 1); + my $nhr = $hsurp->{$hod}{nhr}; - for my $sbn (sort @batteries) { # jede Batterie - my $bpinmax = $hsurp->{$shod}{$sbn}{bpinmax}; # Bat max. mögliche Ladelesitung - - if (!$hsurp->{$shod}{$sbn}{lcintime}) { # Ladesteuerung nicht "In Time" - $hsurp->{$shod}{$sbn}{pneedmin} = $bpinmax; + my @remaining_hods = grep { int $_ >= int $hod } @sortedhods; + + for my $sbn (sort { $a <=> $b } @batteries) { # jede Batterie + my $bpinmax = $hsurp->{$hod}{$sbn}{bpinmax}; # Bat max. mögliche Ladelesitung + my $sbatinstcap = $hsurp->{$hod}{$sbn}{batinstcap}; # Kapa dieser Batterie + my $lowSocwh = $hsurp->{$hod}{$sbn}{lowSocwh}; # eingestellter lowSoc in Wh + my $csocwh = $hsurp->{$hod}{$sbn}{csocwh}; # aktueller SoC in Wh + my $bpinreduced = BatteryVal ($name, $sbn, 'bpinreduced', 0); # Standardwert wenn z.B. kein Überschuß oder Zwangsladung vom Grid + + my $runwh = defined $hsurp->{$hod}{$sbn}{fcnextwh} ? # Auswahl des zu verwendenden Prognose-SOC (Wh) + $hsurp->{$hod}{$sbn}{fcnextwh} : + ( $nhr eq '00' ? + $csocwh : + $hsurp->{$hod}{$sbn}{initsocwh} + ); + + $runwh = min ($runwh, $sbatinstcap); + $hsurp->{$hod}{$sbn}{runwh} = sprintf "%.0f", $runwh; + + if (!$spls || !$hsurp->{$hod}{$sbn}{lcintime}) { # Ladesteuerung nicht "In Time" + $hsurp->{$hod}{$sbn}{achievable} = 'undetermined'; + $hsurp->{$hod}{$sbn}{pneedmin} = $bpinmax; + $hsurp->{$hod}{$sbn}{fcendwh} = sprintf "%.0f", $runwh; - if ($hsurp->{$shod}{nhr} eq '00') { - $otp->{$sbn}{target} = $bpinmax; - storeReading ('Battery_ChargeOptTargetPower_'.$sbn, $bpinmax.' W'); + if ($nhr eq '00') { + $otp->{$sbn}{target} = $csocwh <= $lowSocwh ? $bpinreduced : $bpinmax; } next; - } - - my $bpinreduced = BatteryVal ($name, $sbn, 'bpinreduced', 0); # Standardwert wenn z.B. kein Überschuß oder Zwangsladung vom Grid - my $crgwh = BatteryVal ($name, $sbn, 'bchargewh', 0); # aktueller Ladezustand Batterie - my $runwh = defined $hsurp->{$shod}{$sbn}{fcnextwh} ? # Auswahl des zu verwendenden Prognose-SOC (Wh) - $hsurp->{$shod}{$sbn}{fcnextwh} : - ( $hsurp->{$shod}{nhr} eq '00' ? - $crgwh : - $hsurp->{$shod}{$sbn}{initsocwh} - ); + } - my $otpMargin = $hsurp->{$shod}{$sbn}{otpMargin}; - my $margin = defined $otpMargin ? $otpMargin : SFTYMARGIN_20; + my $otpMargin = $hsurp->{$hod}{$sbn}{otpMargin}; + my $margin = defined $otpMargin ? $otpMargin : SFTYMARGIN_20; + my $runwhneed = $sbatinstcap - $runwh; + my $fref = ___batFindMinPhWh ($hsurp, \@remaining_hods, $runwhneed); + my $needraw = min ($fref->{ph}, $spls); # Ladeleistung auf Surplus begrenzen + + $needraw *= 1 + ($margin / 100); # 1. Sicherheitsaufschlag + $needraw = sprintf "%.0f", $needraw; + my $pneedmin = max ($needraw, $bpinreduced); # Mindestladeleistung bpinreduced sicherstellen - if (!$spls) { # auf kleine Sollladeleistung setzen wenn kein Überschuß - $hsurp->{$shod}{$sbn}{pneedmin} = $bpinreduced; - - if ($hsurp->{$shod}{nhr} eq '00') { - $otp->{$sbn}{target} = $bpinreduced; - storeReading ('Battery_ChargeOptTargetPower_'.$sbn, $bpinreduced.' W'); - } - - next; - } - - my $sbatinstcap = $hsurp->{$shod}{$sbn}{batinstcap}; # Kapa dieser Batterie - $runwh = min ($runwh, $sbatinstcap); - my $runwhneed = $sbatinstcap - $runwh; - my $needraw = $sphrs ? $runwhneed / $sphrs : $runwhneed; # Ladeleistung initial - $needraw *= 1 + ($margin / 100); # Sicherheitsaufschlag - - $needraw = max (0, $needraw); - $hsurp->{$shod}{$sbn}{runwh} = sprintf "%.0f", $runwh; - $hsurp->{$shod}{$sbn}{sphrs} = $sphrs; # Reststunden mit diesem Überschuß - my $pneedmin = sprintf "%.0f", $spls > $needraw ? # Mindestladeleistung bzw. Energie bei 1h (Wh) - $needraw ? $needraw : $bpinreduced : - $spls; - $hsurp->{$shod}{$sbn}{pneedmin} = min ($pneedmin, $hsurp->{$shod}{$sbn}{bpinmax}); # Begrenzung auf max. mögliche Batterieleistung - - if (defined $hsurp->{$newshod}) { - $hsurp->{$newshod}{$sbn}{fcnextwh} = min ($sbatinstcap, $runwh + $hsurp->{$shod}{$sbn}{pneedmin}); - } - - if ($hsurp->{$shod}{nhr} eq '00') { - my $target = max ($bpinreduced, $hsurp->{$shod}{$sbn}{pneedmin}); - - if (NexthoursVal ($name, 'NextHour00', 'DoN', 0)) { - $target *= 1 + ($margin / 100); # 2. Sicherheitsaufschlag - } + $hsurp->{$hod}{$sbn}{achievable} = 'goal: '.(sprintf "%.0f", $runwhneed).' Wh -> '.($fref->{achievable} ? 'yes' : 'no'); + $hsurp->{$hod}{$sbn}{pneedmin} = min ($pneedmin, $hsurp->{$hod}{$sbn}{bpinmax}); # Begrenzung auf max. mögliche Batterieleistung + + if ($nhr eq '00') { + my $target = max ($bpinreduced, $hsurp->{$hod}{$sbn}{pneedmin}); + $target *= 1 + ($margin / 100); # 2. Sicherheitsaufschlag my $gfeedin = CurrentVal ($name, 'gridfeedin', 0); # aktuelle Netzeinspeisung my $bpin = CurrentVal ($name, 'batpowerinsum', 0); # aktuelle Batterie Ladeleistung (Summe über alle Batterien) my $inc = 0; - if ( !$bpin && $gfeedin > $fipl ) {$inc = $gfeedin - $fipl} # Ladefreigabe wenn akt. keine Bat-Ladung UND akt. Einspeisung > Einspeiselimit der Anlage - if ( $bpin && ($gfeedin - $bpin) > $fipl ) {$inc = $bpin + (($gfeedin - $bpin) - $fipl)} # Ladefreigabe wenn akt. Bat-Ladung UND Eispeisung - Bat-Ladung > Einspeiselimit der Anlage - - + if ( !$bpin && $gfeedin > $fipl ) {$inc = $gfeedin - $fipl} # Ladeleistung wenn akt. keine Bat-Ladung UND akt. Einspeisung > Einspeiselimit der Anlage + if ( $bpin && ($gfeedin - $bpin) > $fipl ) {$inc = $bpin + (($gfeedin - $bpin) - $fipl)} # Ladeleistung wenn akt. Bat-Ladung UND Eispeisung - Bat-Ladung > Einspeiselimit der Anlage + $target = sprintf "%.0f", max ($target, $inc); # Einspeiselimit berücksichtigen - $target = min ($bpinmax, $target); # 2. Begrenzung auf max. mögliche Batterieleistung + $target = min (($csocwh <= $lowSocwh ? $bpinreduced : $bpinmax), $target); # 2. Begrenzung auf max. mögliche Batterieleistung bzw. bpinreduced bei Unterschreitung lowSoc $otp->{$sbn}{target} = $target; - - storeReading ('Battery_ChargeOptTargetPower_'.$sbn, $target.' W'); - } + } + + $hsurp->{$hod}{$sbn}{fcendwh} = sprintf "%.0f", min ($sbatinstcap, $runwh + ($nhr eq '00' ? # Endwert Prognose aktuelle Stunde + $otp->{$sbn}{target} : + $hsurp->{$hod}{$sbn}{pneedmin} + )); + + $hsurp->{$newshod}{$sbn}{fcnextwh} = $hsurp->{$hod}{$sbn}{fcendwh}; # Startwert kommende Stunde } - - $sphrs-- if($spls); # Reststunden mit Überschuß } return ($hsurp, $otp); } +############################################################################################### +# Errechnet minimal benötigte konstante Ladeleistung: $ph Wh via Binärsuche Iteration +# +# - Wenn die Summe aller surplswh geringer ist als der Bedarf, wird Ereq automatisch auf +# diesen Maximalwert gesetzt und liefert so die tatsächlich erreichbare Energie. +# - gewichtete Stundenkapazität @hods enthält die Stunden-Keys sortiert von der niedrigsten +# bis zur höchsten Leistung. In jeder Binärsuche-Iteration addiert das Skript +# min(ph, surplswh) für jede Stunde, wodurch die konstant gewählte Leistung ph gemäß der +# verfügbaren Kapazität gewichtet wird. +# - Rückgabe Nach X Iterationen steht $high als kleinstmöglicher Wert für ph bereit. Er +# garantiert entweder das Erreichen von Ereq Wh oder – falls das Ziel unerreichbar war – +# die vollständige Ausnutzung der vorhandenen Kapazität. +############################################################################################### +sub ___batFindMinPhWh { + my ($hsurp, $aref, $runwhneed) = @_; + my @hods = @$aref; + + my $Ereq = $runwhneed; # 1. Benötigte Energie (Wh) bestimmen + my $achievable = 1; + my $total = 0; # 2. Gesamtkapazität aller Stunden mit PV-Überschuß ermitteln + $total += $hsurp->{$_}{surplswh} for @hods; + + if ($total < $Ereq) { + $achievable = 0; + #$Ereq = $total; # 3. Wenn Ziel nicht erreichbar: Ereq auf Maximum setzen + } + + my $low = 0; # 4. Binärsuche für konstante Ladeleistung ph (Wh/h) + my $high = max map { $hsurp->{$_}{surplswh} } @hods; + my $eps = 0.5; # minimale Genauigkeit in Wh (1e-3) + my $max_iter = 100; # Zwangsabbruch nach X Durchläufen + my $loop = 0; + + while (($high - $low) > $eps) { + last if ++$loop > $max_iter; + + my $mid = ($low + $high) / 2; + my $charged = 0; + + for my $hod (@hods) { + my $cap = $hsurp->{$hod}{surplswh}; + $charged += $mid < $cap ? $mid : $cap; + } + + $charged >= $Ereq ? ($high = $mid) : ($low = $mid); + } + + $high = max (0, $high); + +return { ph => (sprintf "%.0f", $high), iterations => $loop, blur => (sprintf "%.4f", ($high - $low)), achievable => $achievable }; +} + ################################################################ # Speicherung Ergebnisse aus Batterie Lademanagement ################################################################ @@ -12046,14 +12132,16 @@ sub ___batChargeSaveResults { my $name = $paref->{name}; my $hsoc = $values->{hsoc}; # Referenz SoC-Hash + my $otp = $values->{otp}; # Referenz OTP-Hash my $bn = $values->{bn}; # Batterie Nummer - my $num = $values->{num}; # Nexthours Schleife (0..MAXNEXTHOURS) my $nhr = $values->{nhr}; # zweistellige lfd. Nexthour my $progsoc = $values->{progsoc}; # Prognose-SoC in % my $socwh = $values->{socwh}; # Prognose-SoC in Wh my $today = $values->{today}; # Statusbit aktueller Tag my $hod = $values->{hod}; # Stunde des Tages - my $loop = $values->{loop}; # in welcher Loop gestartet? + my $loopid = $values->{loopid}; # in welcher Loop ist sub aufgerufen? + my $strategy = $values->{strategy}; # welche Lade-Strategie wird verwendet + my $crel = $values->{crel}; # nur in Schleife 'loadRelease' mitgeben my $labortCond = $values->{labortCond}; # nur in Schleife 'loadRelease' mitgeben my $loadAbort = $values->{loadAbort}; # nur in Schleife 'loadRelease' mitgeben @@ -12062,36 +12150,50 @@ sub ___batChargeSaveResults { ## in Schleife 'loadRelease' setzen ##################################### - if ($loop eq 'LR') { # nur in Schleife 'loadRelease' setzen + if ($loopid eq 'LR') { # nur in Schleife 'loadRelease' setzen $data{$name}{nexthours}{'NextHour'.$nhr}{'rcdchargebat'.$bn} = $crel; $data{$name}{nexthours}{'NextHour'.$nhr}{'lcintimebat'.$bn} = $lcintime if($cgbt); # nur einmal bei 'loadRelease' setzen -> Ladesteuerung "In Time", "nicht In Time" oder nicht verwendet + $data{$name}{nexthours}{'NextHour'.$nhr}{'strategybat'.$bn} = $strategy; - if (!$num) { + if ($nhr eq '00') { storeReading ('Battery_ChargeUnrestricted_'.$bn, $crel); storeReading ('Battery_ChargeAbort_'.$bn, $labortCond) if ($loadAbort); # Ladeabbruchbedingung } if ($today && $hod) { - writeToHistory ( { paref => $paref, key => 'lcintimebat'.$bn, val => $lcintime, hour => $hod } ) if($cgbt); # nur einmal bei 'loadRelease' setzen + writeToHistory ( { paref => $paref, key => 'lcintimebat'.$bn, val => $lcintime, hour => $hod } ) if($cgbt); + writeToHistory ( { paref => $paref, key => 'strategybat'.$bn, val => $strategy, hour => $hod } ); } } - - if ($today && $hod) { - writeToHistory ( { paref => $paref, key => 'batprogsoc'.$bn, val => $progsoc, hour => $hod } ); + ## in Schleife 'optPower' setzen + ################################## + if ($loopid eq 'OTP') { + if ($nhr eq '00') { # Target für aktuelle Stunde + my $needmin = $otp->{$bn}{target} // 0; + storeReading ('Battery_ChargeOptTargetPower_'.$bn, $needmin.' W'); + } } - __createNextHoursSFCReadings ( {name => $name, - nhr => $nhr, - bn => $bn, - progsoc => $progsoc - } - ); # Readings NextHourXX_Bat_XX_ChargeForecast erstellen + ## abhängig von Strategie in entsprechender Schleife setzen + ############################################################# + if (($loopid eq 'LR' && $strategy eq 'loadRelease') || ($loopid eq 'OTP' && $strategy eq 'optPower')) { + if ($today && $hod) { + writeToHistory ( { paref => $paref, key => 'batprogsoc'.$bn, val => $progsoc, hour => $hod } ); + } + + __createNextHoursSFCReadings ( {name => $name, + nhr => $nhr, + bn => $bn, + progsoc => $progsoc + } + ); # Readings NextHourXX_Bat_XX_ChargeForecast erstellen - $data{$name}{nexthours}{'NextHour'.$nhr}{'soc'.$bn} = $progsoc; + $data{$name}{nexthours}{'NextHour'.$nhr}{'soc'.$bn} = $progsoc; + + $hsoc->{$nhr}{socprogwhsum} += $socwh; # Hilfshash Aufsummierung SoC-Prognose (Wh) über alle Batterien + } - $hsoc->{$nhr}{socprogwhsum} += $socwh; # Hilfshash Aufsummierung SoC-Prognose (Wh) über alle Batterien - return; } @@ -12211,13 +12313,12 @@ sub _createSummaries { $tdConFcTillSunset -= ($confc / 60) * $diflasth; } } - else { - $tomorrowSum->{PV} += $pvfc if(int($nhday) == int($tmoday)); - - if (int($nhday) == int($datmoday)) { - $daftertomSum->{PV} += $pvfc; - $daftertomSum->{Consumption} += $confc; - } + elsif ($nhday eq $tmoday) { + $tomorrowSum->{PV} += $pvfc; + } + elsif ($nhday eq $datmoday) { + $daftertomSum->{PV} += $pvfc; + $daftertomSum->{Consumption} += $confc; } } @@ -17535,6 +17636,7 @@ sub _beamFillupBatValues { $hh->{$day_str}{$time_str}{'rcdchargebat'.$bn} = $rcdc; $hh->{$day_str}{$time_str}{'lcintimebat'.$bn} = NexthoursVal ($name, $idx, 'lcintimebat'.$bn, undef); + $hh->{$day_str}{$time_str}{'strategybat'.$bn} = NexthoursVal ($name, $idx, 'strategybat'.$bn, undef); $hh->{$day_str}{$time_str}{'soc'.$bn} = NexthoursVal ($name, $idx, 'soc'.$bn, undef); } } @@ -17557,6 +17659,7 @@ sub _beamFillupBatValues { ##################################### $hfcg->{$kdx}{'rcdchargebat'.$bn} = $hh->{$ds}{$ts}{'rcdchargebat'.$bn} if(defined $hh->{$ds}{$ts}{'rcdchargebat'.$bn}); $hfcg->{$kdx}{'lcintimebat'.$bn} = $hh->{$ds}{$ts}{'lcintimebat'.$bn} if(defined $hh->{$ds}{$ts}{'lcintimebat'.$bn}); + $hfcg->{$kdx}{'strategybat'.$bn} = $hh->{$ds}{$ts}{'strategybat'.$bn} if(defined $hh->{$ds}{$ts}{'strategybat'.$bn}); $hfcg->{$kdx}{'soc'.$bn} = $hh->{$ds}{$ts}{'soc'.$bn} if(defined $hh->{$ds}{$ts}{'soc'.$bn}); ## Auffüllen mit History Werten (Achtung: Stundenverschieber relativ zu Nexthours) @@ -17564,10 +17667,12 @@ sub _beamFillupBatValues { if (!defined $hh->{$ds}{$ts}{'rcdchargebat'.$bn}) { my $histsoc = HistoryVal ($hash, $ds, (sprintf "%02d", $ts+1), 'batsoc'.$bn, undef); my $lcintime = HistoryVal ($hash, $ds, (sprintf "%02d", $ts+1), 'lcintimebat'.$bn, undef); + my $strategy = HistoryVal ($hash, $ds, (sprintf "%02d", $ts+1), 'strategybat'.$bn, undef); if (defined $histsoc) { $hfcg->{$kdx}{'rcdchargebat'.$bn} = 'hist'; $hfcg->{$kdx}{'lcintimebat'.$bn} = $lcintime; + $hfcg->{$kdx}{'strategybat'.$bn} = $strategy; $hfcg->{$kdx}{'soc'.$bn} = $histsoc; } } @@ -18161,6 +18266,11 @@ sub __batteryOnBeam { $time_str = (split ":", $time_str)[0]; # Forum: https://forum.fhem.de/index.php?msg=1332721 my $soc = $hfcg->{$i}{'soc'.$bn}; my $lcintime = $hfcg->{$i}{'lcintimebat'.$bn}; # Lademanagement für Batterie XX ist aktiviert + my $strategy = $hfcg->{$i}{'strategybat'.$bn} // '-'; # Ladestrategie + + my $stysymbol = $strategy eq 'loadRelease' ? 'ldreleas' : + $strategy eq 'optPower' ? 'optchpow' : + 'norate'; my ($bpower, $currsoc); @@ -18178,7 +18288,7 @@ sub __batteryOnBeam { flag => $hfcg->{$i}{'rcdchargebat'.$bn}, msg1 => $balias, msg2 => $lcintime, - msg3 => $htitles{ldreleas}{$lang}, + msg3 => $htitles{$stysymbol}{$lang}, soc => $soc, pcurr => $bpower, lang => $lang @@ -20972,7 +21082,7 @@ sub _listDataPoolPvHist { $prdl .= "pprl${pn}: $pprl"; } - my ($btotin, $batin, $btotout, $batout, $batmsoc, $batssoc, $batprogsoc, $batsoc, $lcintime); + my ($btotin, $batin, $btotout, $batout, $batmsoc, $batssoc, $batprogsoc, $batsoc, $lcintime, $lcstrategy); for my $bn (1..MAXBATTERIES) { # + alle Batterien $bn = sprintf "%02d", $bn; my $hbtotin = HistoryVal ($name, $day, $key, 'batintotal'.$bn, '-'); @@ -20984,6 +21094,7 @@ sub _listDataPoolPvHist { my $hbatprogsoc = HistoryVal ($name, $day, $key, 'batprogsoc'.$bn, '-'); my $hbatsoc = HistoryVal ($name, $day, $key, 'batsoc'.$bn, '-'); my $intime = HistoryVal ($name, $day, $key, 'lcintimebat'.$bn, '-'); + my $strategy = HistoryVal ($name, $day, $key, 'strategybat'.$bn, '-'); if ($export eq 'csv') { $hexp->{$day}{$key}{"BatteryInTotal${bn}"} = $hbtotin; @@ -20995,6 +21106,7 @@ sub _listDataPoolPvHist { $hexp->{$day}{$key}{"BatteryProgSoc${bn}"} = $hbatprogsoc; $hexp->{$day}{$key}{"BatterySoc${bn}"} = $hbatsoc; $hexp->{$day}{$key}{"BatteryLCinTime${bn}"} = $intime; + $hexp->{$day}{$key}{"BatteryStrategy${bn}"} = $strategy; } $btotin .= ', ' if($btotin); @@ -21015,6 +21127,8 @@ sub _listDataPoolPvHist { $batsoc .= "batsoc${bn}: $hbatsoc"; $lcintime .= ', ' if($lcintime); $lcintime .= "lcintimebat${bn}: $intime"; + $lcstrategy .= ', ' if($lcstrategy); + $lcstrategy .= "strategybat${bn}: $strategy"; } $ret .= "\n " if($ret); @@ -21047,7 +21161,9 @@ sub _listDataPoolPvHist { $ret .= "\n " if($key ne '99'); $ret .= $lcintime if($key ne '99'); $ret .= "\n " if($key ne '99'); - + $ret .= $lcstrategy if($key ne '99'); + $ret .= "\n " if($key ne '99'); + $ret .= $batin; $ret .= "\n "; $ret .= $batout; @@ -21448,16 +21564,19 @@ sub _listDataPoolNextHours { my $socprgs = NexthoursVal ($name, $idx, 'socprogwhsum', '-'); my $dinrang = NexthoursVal ($name, $idx, 'DaysInRange', '-'); - my ($rcdbat, $socs, $lcintime); + my ($rcdbat, $socs, $lcintime, $lcstrategy); for my $bn (1..MAXBATTERIES) { # alle Batterien $bn = sprintf "%02d", $bn; my $rcdcharge = NexthoursVal ($name, $idx, 'rcdchargebat'.$bn, '-'); my $intime = NexthoursVal ($name, $idx, 'lcintimebat'.$bn, '-'); + my $strategy = NexthoursVal ($name, $idx, 'strategybat'.$bn, '-'); my $socxx = NexthoursVal ($name, $idx, 'soc'.$bn, '-'); $rcdbat .= ', ' if($rcdbat); $rcdbat .= "rcdchargebat${bn}: $rcdcharge"; $lcintime .= ', ' if($lcintime); $lcintime .= "lcintimebat${bn}: $intime"; + $lcstrategy .= ', ' if($lcstrategy); + $lcstrategy .= "strategybat${bn}: $strategy"; $socs .= ', ' if($socs); $socs .= "soc${bn}: $socxx"; } @@ -21479,6 +21598,8 @@ sub _listDataPoolNextHours { $sq .= $rcdbat; $sq .= "\n "; $sq .= $lcintime; + $sq .= "\n "; + $sq .= $lcstrategy; } return $sq; @@ -26283,6 +26404,7 @@ to ensure that the system configuration is correct.