diff --git a/fhem/contrib/DS_Starter/76_SolarForecast.pm b/fhem/contrib/DS_Starter/76_SolarForecast.pm
index 716bdd918..2397ffdc7 100644
--- a/fhem/contrib/DS_Starter/76_SolarForecast.pm
+++ b/fhem/contrib/DS_Starter/76_SolarForecast.pm
@@ -160,11 +160,12 @@ BEGIN {
# Versions History intern
my %vNotesIntern = (
- "1.49.0" => "19.03.2025 _listDataPoolApiData: fix warning item1, new option OpenMeteoDWD_D2-API with preparation for satellite support ".
+ "1.49.0" => "22.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 ",
+ "_batChargeRecmd: fix calc if more than one batteries are installed, set aiDecTree: new option rawDataGHIreplace ".
+ "new Attr plantControl with key feedinPowerLimit ",
"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 ".
@@ -391,6 +392,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
@@ -567,14 +569,13 @@ my @rconfigs = qw( pvCorrectionFactor_Auto
# Anlagenkonfiguration: maßgebliche Attribute
my @aconfigs = qw( affectBatteryPreferredCharge affectConsForecastIdentWeekdays
affectConsForecastInPlanning affectSolCastPercentile
- aiControl
+ aiControl
consumerLegend consumerAdviceIcon consumerLink
ctrlBackupFilesKeep
ctrlConsRecommendReadings ctrlGenPVdeviation ctrlInterval
ctrlLanguage ctrlNextDayForecastReadings ctrlNextHoursSoCForecastReadings
ctrlShowLink ctrlSolCastAPImaxReq
ctrlSolCastAPIoptimizeReq ctrlSpecialReadings ctrlUserExitFn
- setupWeatherDev1 setupWeatherDev2 setupWeatherDev3
disable
flowGraphicControl graphicBeamWidth
graphicBeamHeightLevel1 graphicBeamHeightLevel2 graphicBeamHeightLevel3
@@ -585,7 +586,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
);
@@ -699,6 +702,7 @@ my %hattr = ( # H
setupRoofTops => { fn => \&_attrRoofTops },
flowGraphicControl => { fn => \&_attrflowGraphicControl },
aiControl => { fn => \&_attraiControl },
+ plantControl => { fn => \&_attrplantControl },
);
for my $bn (1..MAXBATTERIES) {
@@ -1567,6 +1571,7 @@ sub Initialize {
"graphicSpaceSize ".
"graphicWeatherColor:colorpicker,RGB ".
"graphicWeatherColorNight:colorpicker,RGB ".
+ "plantControl:textField-long ".
"setupInverterStrings ".
"setupMeterDev:textField-long ".
"setupWeatherDev1 ".
@@ -1598,9 +1603,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'
@@ -4266,7 +4270,7 @@ sub __getopenMeteoData {
my $allstrings = AttrVal ($name, 'setupInverterStrings', '');
#if ($reqm eq 'MODEL' && $submodel eq 'OpenMeteoDWDD2API') { # Satellitenunterstützung dazuladen
- # $allstrings .= ',Special_SatelliteRadiation,'.$allstrings;
+ # $allstrings .= ',SatelliteRadiation,'.$allstrings;
#}
$paref->{callequivalent} = $submodel eq 'OpenMeteoDWDEnsembleAPI' ? 20 : 1;
@@ -4365,7 +4369,7 @@ sub __openMeteoDWD_ApiRequest {
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);
@@ -4377,8 +4381,8 @@ sub __openMeteoDWD_ApiRequest {
my ($string, $err);
($string, $allstrings) = split ",", $allstrings, 2;
- if ($string eq 'Special_SatelliteRadiation') { # Trenner-String: ab jetzt Satelliten Abfrage
- $submodel = 'Special_SatelliteRadiation';
+ if ($string eq 'SatelliteRadiation') { # Trenner-String: ab jetzt Satelliten Abfrage
+ $submodel = 'SatelliteRadiation';
$paref->{submodel} = $submodel;
($string, $allstrings) = split ",", $allstrings, 2;
}
@@ -4542,7 +4546,7 @@ sub __openMeteoDWD_ApiResponse {
#########################
my ($curwid, $currain, $curwcc, $curtmp, $curstr);
- if ($submodel ne 'Special_SatelliteRadiation') {
+ if ($submodel ne 'SatelliteRadiation') {
if (defined $jdata->{current}{time}) {
($err, $curstr) = timestringUTCtoLocal ($name, $jdata->{current}{time}, '%Y-%m-%dT%H:%M');
@@ -4630,7 +4634,7 @@ sub __openMeteoDWD_ApiResponse {
## Wetterdaten
################
- if ($submodel ne 'Special_SatelliteRadiation' && $submodel ne 'HistoricalData') {
+ 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
@@ -4679,7 +4683,7 @@ sub __openMeteoDWD_ApiResponse {
## Tageswerte (Sonnenauf- und untergang)
##########################################
- if ($submodel ne 'Special_SatelliteRadiation') {
+ if ($submodel ne 'SatelliteRadiation') {
$k = 0;
while ($jdata->{daily}{time}[$k]) {
@@ -4721,13 +4725,13 @@ sub __openMeteoDWD_ApiResponse {
## 15 Minuten Werte
#####################
- if ($submodel ne 'Special_SatelliteRadiation') {
+ 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 ($requestmode eq 'MODEL' && $haggr) {
+ 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);
@@ -4735,7 +4739,7 @@ sub __openMeteoDWD_ApiResponse {
#$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");
+ #debugLog ($paref, 'apiProcess', "Open-Meteo API - do 15 min Aggr $tmstr - GTIWh: $gtiwh, PV estimate: $pv Wh");
}
}
@@ -4827,7 +4831,7 @@ sub ___createOpenMeteoURL {
$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=global_tilted_irradiance,shortwave_radiation";
+ $url .= "&minutely_15=rain,global_tilted_irradiance,shortwave_radiation";
$url .= "&daily=sunrise,sunset";
$url .= "&forecast_hours=48";
$url .= "&forecast_days=2";
@@ -4835,7 +4839,7 @@ sub ___createOpenMeteoURL {
$url .= "&azimuth=".$az;
}
- if ($submodel eq 'Special_SatelliteRadiation') {
+ if ($submodel eq 'SatelliteRadiation') {
$url = "https://satellite-api.open-meteo.com/v1/archive?";
$url .= "models=satellite_radiation_seamless";
$url .= "&latitude=".$lat;
@@ -6336,6 +6340,46 @@ 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( feedinPowerLimit
+ ) ) {
+
+ delete $data{$name}{current}{$av};
+ }
+
+ if ($cmd eq 'set') {
+ my $valid = {
+ feedinPowerLimit => '\d+',
+ };
+
+ 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
################################################################
@@ -7933,25 +7977,9 @@ sub centralTask {
### nicht mehr benötigte Daten verarbeiten - Bereich kann später wieder raus !!
##########################################################################################################################
- delete $data{$name}{circular}{'00'}; # 04.02.2025
+ #delete $data{$name}{circular}{'00'}; # 04.02.2025
readingsDelete ($hash, '.migrated'); # 01.02.25
- 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});
- }
-
- 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});
- }
- }
-
my $n = 0; # 01.02.25 -> Datenmigration pvrlsum, pvfcsum, dnumsum in pvrl_*, pvfc_*
for my $hh (1..24) {
$hh = sprintf "%02d", $hh;
@@ -10446,10 +10474,11 @@ sub _batChargeRecmd {
return if(!isBatteryUsed ($name));
- my $hash = $defs{$name};
- 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
##############################
@@ -10489,10 +10518,10 @@ sub _batChargeRecmd {
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 $confcss = CurrentVal ($hash, 'tdConFcTillSunset', 0); # Verbrauchsprognose bis Sonnenuntergang
my $tomconfc = ReadingsNum ($name, 'Tomorrow_ConsumptionForecast', 0); # Verbrauchsprognose nächster Tag
- my $csoc = BatteryVal ($hash, $bn, 'bcharge', 0); # aktuelle Ladung in %
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;
@@ -10558,12 +10587,13 @@ sub _batChargeRecmd {
$spday = 0 if($spday < 0); # PV Überschuß Prognose bis Sonnenuntergang
## Ladefreigabe
- #################
- 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 ( !$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
@@ -24861,6 +24891,32 @@ 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.
+
+
+
+
+
+ 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 |
+ | |
+
+
+
+
+ Beispiel:
+ attr <name> plantControl feedinPowerLimit=4800
+
+
+
+
setupBatteryDevXX <Battery Device Name> pin=<Readingname>:<Unit> pout=<Readingname>:<Unit>
@@ -27364,6 +27420,32 @@ 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.
+
+
+
+
+
+ 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 |
+ | |
+
+
+
+
+ Beispiel:
+ attr <name> plantControl feedinPowerLimit=4800
+
+
+
+
setupBatteryDevXX <Batterie Device Name> pin=<Readingname>:<Einheit> pout=<Readingname>:<Einheit>