2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-02-26 10:34:52 +00:00

76_SolarForecast: Consumer planning can now react dynamically to changing or time-shifting PV generation, some more minor changes and fixes

git-svn-id: https://svn.fhem.de/fhem/trunk@28392 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
nasseeder1 2024-01-18 21:21:35 +00:00
parent 42a3a7d7e5
commit 6384245583
2 changed files with 514 additions and 409 deletions

View File

@ -1,5 +1,8 @@
# Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # 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. # Do not insert empty lines here, update check depends on it.
- feature: 76_SolarForecast: Consumer planning can now react dynamically to
changing or time-shifting PV generation,
some more minor changes
- bugfix: 72_FRITZBOX: fast alle Sub in der Namensgebung vereinheitlich - bugfix: 72_FRITZBOX: fast alle Sub in der Namensgebung vereinheitlich
um das Modul wartbarer zu machen. um das Modul wartbarer zu machen.
die Fehlerbehandlung vereinheitlicht und hoffentlich besser die Fehlerbehandlung vereinheitlicht und hoffentlich besser

View File

@ -157,7 +157,13 @@ BEGIN {
# Versions History intern # Versions History intern
my %vNotesIntern = ( my %vNotesIntern = (
"1.6.5" => "10.01.2024 new function runCtHourly in ReadyFn to run centralTask definitely at end/begin of an hour ", "1.7.0" => "18.01.2024 Changeover Start centralTask completely to runCentralTask, ".
"aiAddRawData: Weekday from pvHistory not taken into account greater than current day ".
"__reviewSwitchTime: new function for review consumer planning state ".
"___switchonTimelimits: The current time is taken into account during planning ".
"take info-tag into consumerxx Reading ".
"fix deletion of currentBatteryDev, currentInverterDev, currentMeterDev ",
"1.6.5" => "10.01.2024 new function runCentralTask in ReadyFn to run centralTask definitely at end/begin of an hour ",
"1.6.4" => "09.01.2024 fix get Automatic State, use key switchdev for auto-Reading if switchdev is set in consumer attr ", "1.6.4" => "09.01.2024 fix get Automatic State, use key switchdev for auto-Reading if switchdev is set in consumer attr ",
"1.6.3" => "08.01.2024 optimize battery management once more ", "1.6.3" => "08.01.2024 optimize battery management once more ",
"1.6.2" => "07.01.2024 optimize battery management ", "1.6.2" => "07.01.2024 optimize battery management ",
@ -592,7 +598,7 @@ my %hqtxt = (
DE => qq{bis zum Sonnenuntergang warten} }, DE => qq{bis zum Sonnenuntergang warten} },
snbefb => { EN => qq{Should not be empty. Maybe the device has just been redefined.}, snbefb => { EN => qq{Should not be empty. Maybe the device has just been redefined.},
DE => qq{Sollte nicht leer sein. Vielleicht wurde das Device erst neu definiert.} }, DE => qq{Sollte nicht leer sein. Vielleicht wurde das Device erst neu definiert.} },
scnp => { EN => qq{The scheduling of the consumer is not provided}, scnp => { EN => qq{Scheduling of the consumer is not provided},
DE => qq{Die Einplanung des Verbrauchers ist nicht vorgesehen} }, DE => qq{Die Einplanung des Verbrauchers ist nicht vorgesehen} },
vrmcr => { EN => qq{Please set the Victron VRM Portal credentials with "set LINK vrmCredentials".}, vrmcr => { EN => qq{Please set the Victron VRM Portal credentials with "set LINK vrmCredentials".},
DE => qq{Bitte setzen sie die Victron VRM Portal Zugangsdaten mit "set LINK vrmCredentials". } }, DE => qq{Bitte setzen sie die Victron VRM Portal Zugangsdaten mit "set LINK vrmCredentials". } },
@ -884,7 +890,7 @@ sub Initialize {
$hash->{DbLog_splitFn} = \&DbLogSplit; $hash->{DbLog_splitFn} = \&DbLogSplit;
$hash->{AttrFn} = \&Attr; $hash->{AttrFn} = \&Attr;
$hash->{NotifyFn} = \&Notify; $hash->{NotifyFn} = \&Notify;
$hash->{ReadyFn} = \&runCtHourly; $hash->{ReadyFn} = \&runCentralTask;
$hash->{AttrList} = "affect70percentRule:1,dynamic,0 ". $hash->{AttrList} = "affect70percentRule:1,dynamic,0 ".
"affectBatteryPreferredCharge:slider,0,1,100 ". "affectBatteryPreferredCharge:slider,0,1,100 ".
"affectConsForecastIdentWeekdays:1,0 ". "affectConsForecastIdentWeekdays:1,0 ".
@ -991,8 +997,6 @@ sub Define {
createAssociatedWith ($hash); createAssociatedWith ($hash);
$readyfnlist{$name} = $hash;
$params->{file} = $pvhcache.$name; # Cache File PV History einlesen wenn vorhanden $params->{file} = $pvhcache.$name; # Cache File PV History einlesen wenn vorhanden
$params->{cachename} = 'pvhist'; $params->{cachename} = 'pvhist';
_readCacheFile ($params); _readCacheFile ($params);
@ -1019,7 +1023,7 @@ sub Define {
singleUpdateState ( {hash => $hash, state => 'initialized', evt => 1} ); singleUpdateState ( {hash => $hash, state => 'initialized', evt => 1} );
centralTask ($hash); # Einstieg in Abfrage $readyfnlist{$name} = $hash; # Registrierung in Ready-Schleife
InternalTimer (gettimeofday()+$whistrepeat, "FHEM::SolarForecast::periodicWriteCachefiles", $hash, 0); # Einstieg periodisches Schreiben historische Daten InternalTimer (gettimeofday()+$whistrepeat, "FHEM::SolarForecast::periodicWriteCachefiles", $hash, 0); # Einstieg periodisches Schreiben historische Daten
return; return;
@ -2113,6 +2117,7 @@ sub _setreset { ## no critic "not used"
if ($prop eq 'currentMeterSet') { if ($prop eq 'currentMeterSet') {
readingsDelete ($hash, "Current_GridConsumption"); readingsDelete ($hash, "Current_GridConsumption");
readingsDelete ($hash, "Current_GridFeedIn"); readingsDelete ($hash, "Current_GridFeedIn");
readingsDelete ($hash, 'currentMeterDev');
delete $data{$type}{$name}{circular}{'99'}{initdayfeedin}; delete $data{$type}{$name}{circular}{'99'}{initdayfeedin};
delete $data{$type}{$name}{circular}{'99'}{gridcontotal}; delete $data{$type}{$name}{circular}{'99'}{gridcontotal};
delete $data{$type}{$name}{circular}{'99'}{initdaygcon}; delete $data{$type}{$name}{circular}{'99'}{initdaygcon};
@ -2132,6 +2137,7 @@ sub _setreset { ## no critic "not used"
readingsDelete ($hash, 'Current_PowerBatIn'); readingsDelete ($hash, 'Current_PowerBatIn');
readingsDelete ($hash, 'Current_PowerBatOut'); readingsDelete ($hash, 'Current_PowerBatOut');
readingsDelete ($hash, 'Current_BatCharge'); readingsDelete ($hash, 'Current_BatCharge');
readingsDelete ($hash, 'currentBatteryDev');
deleteReadingspec ($hash, 'Battery_.*'); deleteReadingspec ($hash, 'Battery_.*');
undef @{$data{$type}{$name}{current}{socslidereg}}; undef @{$data{$type}{$name}{current}{socslidereg}};
delete $data{$type}{$name}{circular}{'99'}{lastTsMaxSocRchd}; delete $data{$type}{$name}{circular}{'99'}{lastTsMaxSocRchd};
@ -2150,6 +2156,7 @@ sub _setreset { ## no critic "not used"
if ($prop eq 'currentInverterSet') { if ($prop eq 'currentInverterSet') {
undef @{$data{$type}{$name}{current}{genslidereg}}; undef @{$data{$type}{$name}{current}{genslidereg}};
readingsDelete ($hash, "Current_PV"); readingsDelete ($hash, "Current_PV");
readingsDelete ($hash, 'currentInverterDev');
deleteReadingspec ($hash, ".*_PVreal" ); deleteReadingspec ($hash, ".*_PVreal" );
writeCacheToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben writeCacheToFile ($hash, "plantconfig", $plantcfg.$name); # Anlagenkonfiguration File schreiben
} }
@ -4182,8 +4189,6 @@ sub Attr {
unless ($aVal =~ /^[0-9]+$/x) { unless ($aVal =~ /^[0-9]+$/x) {
return qq{The value for $aName is not valid. Use only figures 0-9 !}; return qq{The value for $aName is not valid. Use only figures 0-9 !};
} }
InternalTimer(gettimeofday()+1.0, "FHEM::SolarForecast::centralTask", $hash, 0);
} }
if ($aName eq 'ctrlAIdataStorageDuration') { if ($aName eq 'ctrlAIdataStorageDuration') {
@ -4503,59 +4508,6 @@ sub Notify {
return; return;
} }
################################################################
# centralTask kurz vor und kurz nach einer vollen Stunde
# starten um möglichst genaue Stundenwerte zu ermitteln
################################################################
sub runCtHourly {
my $hash = shift;
return if(!$init_done);
my $name = $hash->{NAME};
return if(InternalVal($name, 'MODE', '') =~ /Manual/xs || CurrentVal ($hash, 'ctrunning', 0));
my $debug;
my $t = time;
my $second = int (strftime "%S", localtime(time)); # aktuelle Sekunde (00-61)
my $minute = int (strftime "%M", localtime($t)); # aktuelle Minute (00-59)
if ($minute == 59 && $second > 48 && $second < 58) {
if (!exists $hash->{HELPER}{S58DONE}) {
$debug = getDebug ($hash);
$hash->{HELPER}{S58DONE} = 1;
if ($debug =~ /collectData/x) {
Log3 ($name, 1, "$name DEBUG> Start of unscheduled data collection at the end of an hour");
}
centralTask ($hash, 1);
}
}
else {
delete $hash->{HELPER}{S58DONE};
}
if ($minute == 0 && $second > 3 && $second < 20) {
if (!exists $hash->{HELPER}{S20DONE}) {
$debug = getDebug ($hash);
$hash->{HELPER}{S20DONE} = 1;
if ($debug =~ /collectData/x) {
Log3 ($name, 1, "$name DEBUG> Start of unscheduled data collection at the beginning of an hour");
}
centralTask ($hash, 1);
}
}
else {
delete $hash->{HELPER}{S20DONE};
}
return;
}
############################################################### ###############################################################
# DbLog_splitFn # DbLog_splitFn
############################################################### ###############################################################
@ -4777,6 +4729,79 @@ sub _savePlantConfig {
return @pvconf; return @pvconf;
} }
################################################################
# centralTask Start Management
################################################################
sub runCentralTask {
my $hash = shift;
return if(!$init_done);
my $name = $hash->{NAME};
my $type = $hash->{TYPE};
return if(CurrentVal ($hash, 'ctrunning', 0));
my $debug;
my $t = time;
my $second = int (strftime "%S", localtime($t)); # aktuelle Sekunde (00-61)
my $minute = int (strftime "%M", localtime($t)); # aktuelle Minute (00-59)
my $interval = controlParams ($name);
if (!$interval) {
$hash->{MODE} = 'Manual';
storeReading ('nextCycletime', 'Manual');
return;
}
my $nct = CurrentVal ($hash, 'nextCycleTime', 0); # gespeicherte nächste CyleTime
if(!IsDisabled($name) && $t >= $nct) {
my $new = $t + $interval; # nächste Wiederholungszeit
$hash->{MODE} = "Automatic - next Cycletime: ".FmtTime($new);
$data{$type}{$name}{current}{nextCycleTime} = $new;
storeReading ('nextCycletime', FmtTime($new));
centralTask ($hash, 1);
}
if ($minute == 59 && $second > 48 && $second < 58) {
if (!exists $hash->{HELPER}{S58DONE}) {
$debug = getDebug ($hash);
$hash->{HELPER}{S58DONE} = 1;
if ($debug =~ /collectData/x) {
Log3 ($name, 1, "$name DEBUG> Start of unscheduled data collection at the end of an hour");
}
centralTask ($hash, 1);
}
}
else {
delete $hash->{HELPER}{S58DONE};
}
if ($minute == 0 && $second > 3 && $second < 20) {
if (!exists $hash->{HELPER}{S20DONE}) {
$debug = getDebug ($hash);
$hash->{HELPER}{S20DONE} = 1;
if ($debug =~ /collectData/x) {
Log3 ($name, 1, "$name DEBUG> Start of unscheduled data collection at the beginning of an hour");
}
centralTask ($hash, 1);
}
}
else {
delete $hash->{HELPER}{S20DONE};
}
return;
}
################################################################ ################################################################
# Zentraler Datenabruf # Zentraler Datenabruf
################################################################ ################################################################
@ -4807,54 +4832,39 @@ sub centralTask {
### nicht mehr benötigte Readings/Daten löschen - Bereich kann später wieder raus !! ### nicht mehr benötigte Readings/Daten löschen - Bereich kann später wieder raus !!
########################################################################################## ##########################################################################################
for my $i (keys %{$data{$type}{$name}{nexthours}}) { #for my $i (keys %{$data{$type}{$name}{nexthours}}) {
delete $data{$type}{$name}{nexthours}{$i}{Rad1h}; # delete $data{$type}{$name}{nexthours}{$i}{Rad1h};
} #}
for my $c (keys %{$data{$type}{$name}{consumers}}) { #for my $c (keys %{$data{$type}{$name}{consumers}}) {
delete $data{$type}{$name}{consumers}{$c}{epiecEstart}; # delete $data{$type}{$name}{consumers}{$c}{epiecEstart};
delete $data{$type}{$name}{consumers}{$c}{epiecStart}; # delete $data{$type}{$name}{consumers}{$c}{epiecStart};
delete $data{$type}{$name}{consumers}{$c}{epiecStartEnergy}; # delete $data{$type}{$name}{consumers}{$c}{epiecStartEnergy};
} #}
for my $k (sort keys %{$data{$type}{$name}{circular}}) { #for my $k (sort keys %{$data{$type}{$name}{circular}}) {
my $val = $data{$type}{$name}{circular}{$k}{pvcorrf}{percentile}; # my $val = $data{$type}{$name}{circular}{$k}{pvcorrf}{percentile};
$data{$type}{$name}{circular}{$k}{pvcorrf}{percentile} = 1 if($val && $val >= 10); # $data{$type}{$name}{circular}{$k}{pvcorrf}{percentile} = 1 if($val && $val >= 10);
} #}
my $fcdev = ReadingsVal ($name, "currentForecastDev", undef); #my $fcdev = ReadingsVal ($name, "currentForecastDev", undef);
if ($fcdev) { #if ($fcdev) {
readingsSingleUpdate ($hash, "currentWeatherDev", $fcdev, 0); # readingsSingleUpdate ($hash, "currentWeatherDev", $fcdev, 0);
deleteReadingspec ($hash, "currentForecastDev"); # deleteReadingspec ($hash, "currentForecastDev");
} #}
my $rdev = ReadingsVal ($name, "currentRadiationDev", undef); #my $rdev = ReadingsVal ($name, "currentRadiationDev", undef);
if ($rdev) { #if ($rdev) {
readingsSingleUpdate ($hash, "currentRadiationAPI", $rdev, 0); # readingsSingleUpdate ($hash, "currentRadiationAPI", $rdev, 0);
deleteReadingspec ($hash, "currentRadiationDev"); # deleteReadingspec ($hash, "currentRadiationDev");
} #}
############################################################################################ ############################################################################################
if ($init_done == 1) { return if(!$init_done);
my $interval = controlParams ($name);
setModel ($hash); # Model setzen setModel ($hash); # Model setzen
if (!$interval) {
$hash->{MODE} = 'Manual';
storeReading ('nextCycletime', 'Manual');
}
else {
my $new = gettimeofday() + $interval;
InternalTimer($new, "FHEM::SolarForecast::centralTask", $hash, 0); # Wiederholungsintervall
if(!IsDisabled($name)) {
$hash->{MODE} = "Automatic - next Cycletime: ".FmtTime($new);
storeReading ('nextCycletime', FmtTime($new));
}
}
return if(IsDisabled($name)); return if(IsDisabled($name));
if (CurrentVal ($hash, 'ctrunning', 0)) { if (CurrentVal ($hash, 'ctrunning', 0)) {
@ -4923,7 +4933,7 @@ sub centralTask {
_transferBatteryValues ($centpars); # Batteriewerte einsammeln _transferBatteryValues ($centpars); # Batteriewerte einsammeln
_batSocTarget ($centpars); # Batterie Optimum Ziel SOC berechnen _batSocTarget ($centpars); # Batterie Optimum Ziel SOC berechnen
_createSummaries ($centpars); # Zusammenfassungen erstellen _createSummaries ($centpars); # Zusammenfassungen erstellen
_manageConsumerData ($centpars); # Consumerdaten sammeln und planen _manageConsumerData ($centpars); # Consumer Daten sammeln und Zeiten planen
_estConsumptionForecast ($centpars); # Verbrauchsprognose erstellen _estConsumptionForecast ($centpars); # Verbrauchsprognose erstellen
_evaluateThresholds ($centpars); # Schwellenwerte bewerten und signalisieren _evaluateThresholds ($centpars); # Schwellenwerte bewerten und signalisieren
_calcReadingsTomorrowPVFc ($centpars); # zusätzliche Readings Tomorrow_HourXX_PVforecast berechnen _calcReadingsTomorrowPVFc ($centpars); # zusätzliche Readings Tomorrow_HourXX_PVforecast berechnen
@ -4953,10 +4963,6 @@ sub centralTask {
} }
$data{$type}{$name}{current}{ctrunning} = 0; $data{$type}{$name}{current}{ctrunning} = 0;
}
else {
InternalTimer(gettimeofday()+5, "FHEM::SolarForecast::centralTask", $hash, 0);
}
return; return;
} }
@ -6808,10 +6814,11 @@ sub _manageConsumerData {
__getAutomaticState ($paref); # Automatic Status des Consumers abfragen __getAutomaticState ($paref); # Automatic Status des Consumers abfragen
__calcEnergyPieces ($paref); # Energieverbrauch auf einzelne Stunden für Planungsgrundlage aufteilen __calcEnergyPieces ($paref); # Energieverbrauch auf einzelne Stunden für Planungsgrundlage aufteilen
__planSwitchTimes ($paref); # Consumer Switch Zeiten planen __planInitialSwitchTime ($paref); # Consumer Switch Zeiten planen
__setTimeframeState ($paref); # Timeframe Status ermitteln __setTimeframeState ($paref); # Timeframe Status ermitteln
__setConsRcmdState ($paref); # Consumption Recommended Status setzen __setConsRcmdState ($paref); # Consumption Recommended Status setzen
__switchConsumer ($paref); # Consumer schalten __switchConsumer ($paref); # Consumer schalten
__reviewSwitchTime ($paref); # Planungsdaten überprüfen und ggf. neu planen
__remainConsumerTime ($paref); # Restlaufzeit Verbraucher ermitteln __remainConsumerTime ($paref); # Restlaufzeit Verbraucher ermitteln
__setPhysSwState ($paref); # physischen Schaltzustand festhalten __setPhysSwState ($paref); # physischen Schaltzustand festhalten
@ -6825,7 +6832,9 @@ sub _manageConsumerData {
my ($pstate,$starttime,$stoptime,$supplmnt) = __getPlanningStateAndTimes ($paref); my ($pstate,$starttime,$stoptime,$supplmnt) = __getPlanningStateAndTimes ($paref);
my ($iilt,$rlt) = isInLocktime ($paref); # Sperrzeit Status ermitteln my ($iilt,$rlt) = isInLocktime ($paref); # Sperrzeit Status ermitteln
my $constate = "name='$alias' state='$costate' planningstate='$pstate'"; my $mode = ConsumerVal ($hash, $c, 'mode', 'can');
my $constate = "name='$alias' state='$costate'";
$constate .= " mode='$mode' planningstate='$pstate'";
$constate .= " remainLockTime='$rlt'" if($rlt); $constate .= " remainLockTime='$rlt'" if($rlt);
$constate .= " info='$supplmnt'" if($supplmnt); $constate .= " info='$supplmnt'" if($supplmnt);
@ -7075,15 +7084,10 @@ return;
################################################################### ###################################################################
# Consumer Schaltzeiten planen # Consumer Schaltzeiten planen
#
# ToDo: bei mode=can ->
# die $epieceX aller bereits geplanten
# Consumer der entsprechenden Stunde XX von $surplus
# abziehen weil schon "verplant"
#
################################################################### ###################################################################
sub __planSwitchTimes { sub __planInitialSwitchTime {
my $paref = shift; my $paref = shift;
my $hash = $paref->{hash}; my $hash = $paref->{hash};
my $name = $paref->{name}; my $name = $paref->{name};
my $c = $paref->{consumer}; my $c = $paref->{consumer};
@ -7097,6 +7101,7 @@ sub __planSwitchTimes {
qq{ alias: }.ConsumerVal ($hash, $c, 'alias', '')); qq{ alias: }.ConsumerVal ($hash, $c, 'alias', ''));
Log3 ($name, 4, qq{$name DEBUG> Planning consumer "$c" - $dnp}); Log3 ($name, 4, qq{$name DEBUG> Planning consumer "$c" - $dnp});
} }
return; return;
} }
@ -7114,158 +7119,7 @@ sub __planSwitchTimes {
return; return;
} }
my $type = $paref->{type}; ___doPlanning ($paref);
my $lang = $paref->{lang};
my $nh = $data{$type}{$name}{nexthours};
my $maxkey = (scalar keys %{$data{$type}{$name}{nexthours}}) - 1;
my $cicfip = AttrVal ($name, 'affectConsForecastInPlanning', 0); # soll Consumption Vorhersage in die Überschußermittlung eingehen ?
debugLog ($paref, "consumerPlanning", qq{consumer "$c" - Consider consumption forecast in consumer planning: }.($cicfip ? 'yes' : 'no'));
my %max;
my %mtimes;
## max. Überschuß 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
my $spexp = $pvfc - ($cicfip ? $confcex : 0); # prognostizierter Energieüberschuß (kann negativ sein)
my ($hour) = $idx =~ /NextHour(\d+)/xs;
$max{$spexp}{starttime} = NexthoursVal ($hash, $idx, "starttime", "");
$max{$spexp}{today} = NexthoursVal ($hash, $idx, "today", 0);
$max{$spexp}{nexthour} = int ($hour);
}
my $order = 1;
for my $k (reverse sort{$a<=>$b} keys %max) {
$max{$order}{spexp} = $k;
$max{$order}{starttime} = $max{$k}{starttime};
$max{$order}{nexthour} = $max{$k}{nexthour};
$max{$order}{today} = $max{$k}{today};
my $ts = timestringToTimestamp ($max{$k}{starttime});
$mtimes{$ts}{spexp} = $k;
$mtimes{$ts}{starttime} = $max{$k}{starttime};
$mtimes{$ts}{nexthour} = $max{$k}{nexthour};
$mtimes{$ts}{today} = $max{$k}{today};
delete $max{$k};
$order++;
}
my $epiece1 = (~0 >> 1);
my $epieces = ConsumerVal ($hash, $c, "epieces", "");
if (ref $epieces eq "HASH") {
$epiece1 = $data{$type}{$name}{consumers}{$c}{epieces}{1};
}
else {
return;
}
debugLog ($paref, "consumerPlanning", qq{consumer "$c" - epiece1: $epiece1});
my $mode = ConsumerVal ($hash, $c, "mode", "can");
my $calias = ConsumerVal ($hash, $c, "alias", "");
my $mintime = ConsumerVal ($hash, $c, "mintime", $defmintime); # Einplanungsdauer
debugLog ($paref, "consumerPlanning", qq{$name DEBUG> consumer "$c" - mode: $mode, mintime: $mintime, relevant method: surplus});
if (isSunPath ($hash, $c)) { # SunPath ist in mintime gesetzt
my ($riseshift, $setshift) = sunShift ($hash, $c);
my $tdiff = (CurrentVal ($hash, 'sunsetTodayTs', 0) + $setshift) -
(CurrentVal ($hash, 'sunriseTodayTs', 0) + $riseshift);
$mintime = $tdiff / 60;
if ($debug =~ /consumerPlanning/x) {
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - Sunrise is shifted by >}.($riseshift / 60).'< minutes');
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - Sunset is shifted by >}. ($setshift / 60).'< minutes');
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - mintime calculated: }.$mintime.' minutes');
}
}
my $stopdiff = $mintime * 60;
$paref->{maxref} = \%max;
$paref->{mintime} = $mintime;
$paref->{stopdiff} = $stopdiff;
if ($mode eq "can") { # Verbraucher kann geplant werden
if ($debug =~ /consumerPlanning/x) {
for my $m (sort{$a<=>$b} keys %mtimes) {
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - surplus expected: $mtimes{$m}{spexp}, }.
qq{starttime: }.$mtimes{$m}{starttime}.", ".
qq{nexthour: $mtimes{$m}{nexthour}, today: $mtimes{$m}{today}});
}
}
for my $ts (sort{$a<=>$b} keys %mtimes) {
if ($mtimes{$ts}{spexp} >= $epiece1) { # die früheste Startzeit sofern Überschuß größer als Bedarf
my $starttime = $mtimes{$ts}{starttime};
$paref->{starttime} = $starttime;
$starttime = ___switchonTimelimits ($paref);
delete $paref->{starttime};
my $startts = timestringToTimestamp ($starttime); # Unix Timestamp für geplanten Switch on
$paref->{ps} = "planned:";
$paref->{startts} = $startts;
$paref->{stopts} = $startts + $stopdiff;
___setConsumerPlanningState ($paref);
___saveEhodpieces ($paref);
delete $paref->{ps};
delete $paref->{startts};
delete $paref->{stopts};
last;
}
else {
$paref->{ps} = "no planning: the max expected surplus is less $epiece1";
___setConsumerPlanningState ($paref);
delete $paref->{ps};
}
}
}
else { # Verbraucher _muß_ geplant werden
if ($debug =~ /consumerPlanning/x) {
for my $o (sort{$a<=>$b} keys %max) {
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - surplus: $max{$o}{spexp}, }.
qq{starttime: }.$max{$o}{starttime}.", ".
qq{nexthour: $max{$o}{nexthour}, today: $max{$o}{today}});
}
}
for my $o (sort{$a<=>$b} keys %max) {
next if(!$max{$o}{today}); # der max-Wert ist _nicht_ heute
$paref->{elem} = $o;
___planMust ($paref);
last;
}
if (!ConsumerVal ($hash, $c, "planstate", undef)) { # es konnte keine Planung mit max für den aktuellen Tag erstellt werden -> Zwangsplanung mit ersten Wert
my $p = (sort{$a<=>$b} keys %max)[0];
$paref->{elem} = $p;
___planMust ($paref);
}
}
my $planstate = ConsumerVal ($hash, $c, "planstate", "");
if ($planstate) {
Log3 ($name, 3, qq{$name - Consumer "$calias" $planstate});
}
writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben
___setPlanningDeleteMeth ($paref);
return; return;
} }
@ -7303,6 +7157,232 @@ sub ___noPlanRelease {
return $dnp; return $dnp;
} }
###################################################################
# Consumer Review Schaltzeiten und neu planen wenn der
# Consumer noch nicht in Operation oder finished ist
# (nach Consumer Schaltung)
###################################################################
sub __reviewSwitchTime {
my $paref = shift;
my $hash = $paref->{hash};
my $c = $paref->{consumer};
my $pstate = ConsumerVal ($hash, $c, 'planstate', '');
my $plswon = ConsumerVal ($hash, $c, 'planswitchon', 0); # bisher geplante Switch on Zeit
my $simpCstat = simplifyCstate ($pstate);
my $t = $paref->{t};
if ($simpCstat =~ /planned|suspended/xs) {
if ($t < $plswon || $t > $plswon + 300) { # geplante Switch-On Zeit ist 5 Min überschritten und immer noch "planned"
my $minute = $paref->{minute};
for my $m (qw(15 45)) {
if (int $minute >= $m) {
if (!exists $hash->{HELPER}{$c.'M'.$m.'DONE'}) {
my $name = $paref->{name};
$hash->{HELPER}{$c.'M'.$m.'DONE'} = 1;
debugLog ($paref, "consumerPlanning", qq{consumer "$c" - Review switch time planning name: }.ConsumerVal ($hash, $c, 'name', '').
qq{ alias: }.ConsumerVal ($hash, $c, 'alias', ''));
___doPlanning ($paref);
}
}
else {
delete $hash->{HELPER}{$c.'M'.$m.'DONE'};
}
}
}
}
else {
delete $hash->{HELPER}{$c.'M15DONE'};
delete $hash->{HELPER}{$c.'M45DONE'};
}
return;
}
###################################################################
# Consumer Planung ausführen
###################################################################
sub ___doPlanning {
my $paref = shift;
my $hash = $paref->{hash};
my $name = $paref->{name};
my $c = $paref->{consumer};
my $debug = $paref->{debug};
my $type = $paref->{type};
my $nh = $data{$type}{$name}{nexthours};
my $cicfip = AttrVal ($name, 'affectConsForecastInPlanning', 0); # soll Consumption Vorhersage in die Überschußermittlung eingehen ?
debugLog ($paref, "consumerPlanning", qq{consumer "$c" - Consider consumption forecast in consumer planning: }.($cicfip ? 'yes' : 'no'));
my %max;
my %mtimes;
## max. Überschuß 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
my $spexp = $pvfc - ($cicfip ? $confcex : 0); # prognostizierter Energieüberschuß (kann negativ sein)
my ($hour) = $idx =~ /NextHour(\d+)/xs;
$max{$spexp}{starttime} = NexthoursVal ($hash, $idx, "starttime", "");
$max{$spexp}{today} = NexthoursVal ($hash, $idx, "today", 0);
$max{$spexp}{nexthour} = int ($hour);
}
my $order = 1;
for my $k (reverse sort{$a<=>$b} keys %max) {
my $ts = timestringToTimestamp ($max{$k}{starttime});
$max{$order}{spexp} = $k;
$max{$order}{ts} = $ts;
$max{$order}{starttime} = $max{$k}{starttime};
$max{$order}{nexthour} = $max{$k}{nexthour};
$max{$order}{today} = $max{$k}{today};
$mtimes{$ts}{spexp} = $k;
$mtimes{$ts}{starttime} = $max{$k}{starttime};
$mtimes{$ts}{nexthour} = $max{$k}{nexthour};
$mtimes{$ts}{today} = $max{$k}{today};
delete $max{$k};
$order++;
}
my $epiece1 = (~0 >> 1);
my $epieces = ConsumerVal ($hash, $c, "epieces", "");
if (ref $epieces eq "HASH") {
$epiece1 = $data{$type}{$name}{consumers}{$c}{epieces}{1};
}
else {
return;
}
debugLog ($paref, "consumerPlanning", qq{consumer "$c" - epiece1: $epiece1});
my $mode = ConsumerVal ($hash, $c, 'mode', 'can');
my $calias = ConsumerVal ($hash, $c, 'alias', '');
my $mintime = ConsumerVal ($hash, $c, 'mintime', $defmintime); # Einplanungsdauer
debugLog ($paref, "consumerPlanning", qq{consumer "$c" - mode: $mode, mintime: $mintime, relevant method: surplus});
if (isSunPath ($hash, $c)) { # SunPath ist in mintime gesetzt
my ($riseshift, $setshift) = sunShift ($hash, $c);
my $tdiff = (CurrentVal ($hash, 'sunsetTodayTs', 0) + $setshift) -
(CurrentVal ($hash, 'sunriseTodayTs', 0) + $riseshift);
$mintime = $tdiff / 60;
if ($debug =~ /consumerPlanning/x) {
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - Sunrise is shifted by >}.($riseshift / 60).'< minutes');
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - Sunset is shifted by >}. ($setshift / 60).'< minutes');
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - mintime calculated: }.$mintime.' minutes');
}
}
my $stopdiff = $mintime * 60;
$paref->{maxref} = \%max;
$paref->{mintime} = $mintime;
$paref->{stopdiff} = $stopdiff;
if ($mode eq "can") { # Verbraucher kann geplant werden
if ($debug =~ /consumerPlanning/x) {
for my $m (sort{$a<=>$b} keys %mtimes) {
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - surplus expected: $mtimes{$m}{spexp}, }.
qq{starttime: }.$mtimes{$m}{starttime}.", ".
qq{nexthour: $mtimes{$m}{nexthour}, today: $mtimes{$m}{today}});
}
}
for my $ts (sort{$a<=>$b} keys %mtimes) {
if ($mtimes{$ts}{spexp} >= $epiece1) { # die früheste Startzeit sofern Überschuß größer als Bedarf
my $starttime = $mtimes{$ts}{starttime};
$paref->{starttime} = $starttime;
$starttime = ___switchonTimelimits ($paref);
delete $paref->{starttime};
my $startts = timestringToTimestamp ($starttime); # Unix Timestamp für geplanten Switch on
$paref->{ps} = 'planned:';
$paref->{startts} = $startts;
$paref->{stopts} = $startts + $stopdiff;
___setConsumerPlanningState ($paref);
___saveEhodpieces ($paref);
delete $paref->{ps};
delete $paref->{startts};
delete $paref->{stopts};
last;
}
else {
$paref->{supplement} = "expected max surplus less $epiece1";
$paref->{ps} = 'suspended:';
___setConsumerPlanningState ($paref);
delete $paref->{ps};
delete $paref->{supplement};
}
}
}
else { # Verbraucher _muß_ geplant werden
if ($debug =~ /consumerPlanning/x) {
for my $o (sort{$a<=>$b} keys %max) {
Log3 ($name, 1, qq{$name DEBUG> consumer "$c" - surplus: $max{$o}{spexp}, }.
qq{starttime: }.$max{$o}{starttime}.", ".
qq{nexthour: $max{$o}{nexthour}, today: $max{$o}{today}});
}
}
my $done;
for my $o (sort{$a<=>$b} keys %max) {
next if(!$max{$o}{today}); # der max-Wert von heute ist auszuwählen
$paref->{elem} = $o;
___planMust ($paref);
delete $paref->{elem};
$done = 1;
last;
}
if (!$done) {
$paref->{supplement} = 'no max surplus found for current day';
$paref->{ps} = 'suspended:';
___setConsumerPlanningState ($paref);
delete $paref->{ps};
delete $paref->{supplement};
}
}
my $planstate = ConsumerVal ($hash, $c, 'planstate', '');
my $planspmlt = ConsumerVal ($hash, $c, 'planSupplement', '');
if ($planstate) {
Log3 ($name, 3, qq{$name - Consumer "$calias" $planstate $planspmlt});
}
writeCacheToFile ($hash, "consumers", $csmcache.$name); # Cache File Consumer schreiben
___setPlanningDeleteMeth ($paref);
return;
}
################################################################ ################################################################
# die geplanten EIN-Stunden des Tages mit den dazu gehörigen # die geplanten EIN-Stunden des Tages mit den dazu gehörigen
# Consumer spezifischen epieces im Consumer-Hash speichern # Consumer spezifischen epieces im Consumer-Hash speichern
@ -7446,10 +7526,13 @@ sub ___switchonTimelimits {
my $date = $paref->{date}; my $date = $paref->{date};
my $starttime = $paref->{starttime}; my $starttime = $paref->{starttime};
my $lang = $paref->{lang}; my $lang = $paref->{lang};
my $t = $paref->{t};
my $startts;
if (isSunPath ($hash, $c)) { # SunPath ist in mintime gesetzt if (isSunPath ($hash, $c)) { # SunPath ist in mintime gesetzt
my ($riseshift, $setshift) = sunShift ($hash, $c); my ($riseshift, $setshift) = sunShift ($hash, $c);
my $startts = CurrentVal ($hash, 'sunriseTodayTs', 0) + $riseshift; $startts = CurrentVal ($hash, 'sunriseTodayTs', 0) + $riseshift;
$starttime = (timestampToTimestring ($startts, $lang))[3]; $starttime = (timestampToTimestring ($startts, $lang))[3];
debugLog ($paref, "consumerPlanning", qq{consumer "$c" - starttime is set to >$starttime< due to >SunPath< is used}); debugLog ($paref, "consumerPlanning", qq{consumer "$c" - starttime is set to >$starttime< due to >SunPath< is used});
@ -7458,26 +7541,31 @@ sub ___switchonTimelimits {
my $origtime = $starttime; my $origtime = $starttime;
my $notbefore = ConsumerVal ($hash, $c, "notbefore", 0); my $notbefore = ConsumerVal ($hash, $c, "notbefore", 0);
my $notafter = ConsumerVal ($hash, $c, "notafter", 0); my $notafter = ConsumerVal ($hash, $c, "notafter", 0);
my ($starthour) = $starttime =~ /\s(\d{2}):/xs;
my $change = q{}; my $change = q{};
if ($t > timestringToTimestamp ($starttime)) {
$starttime = (timestampToTimestring ($t, $lang))[3];
$change = 'current time';
}
my ($starthour) = $starttime =~ /\s(\d{2}):/xs;
if ($notbefore && int $starthour < int $notbefore) { if ($notbefore && int $starthour < int $notbefore) {
$starthour = $notbefore; $starthour = sprintf("%02d", $notbefore);
$change = "notbefore"; $starttime =~ s/\s(\d{2}):/ $starthour:/x;
$change = 'notbefore';
} }
if ($notafter && int $starthour > int $notafter) { if ($notafter && int $starthour > int $notafter) {
$starthour = $notafter; $starthour = sprintf("%02d", $notafter);
$change = "notafter";
}
$starthour = sprintf("%02d", $starthour);
$starttime =~ s/\s(\d{2}):/ $starthour:/x; $starttime =~ s/\s(\d{2}):/ $starthour:/x;
$change = 'notafter';
}
if ($change) { if ($change) {
my $cname = ConsumerVal ($hash, $c, "name", ""); my $cname = ConsumerVal ($hash, $c, "name", "");
Log3 ($name, 3, qq{$name - Planned starttime of "$cname" changed from "$origtime" to "$starttime" due to $change condition}); debugLog ($paref, "consumerPlanning", qq{consumer "$c" - Planned starttime of "$cname" changed from "$origtime" to "$starttime" due to $change condition});
} }
return $starttime; return $starttime;
@ -7654,12 +7742,20 @@ sub ___switchConsumerOn {
my $isintable = isInterruptable ($hash, $c, 0, 1); # mit Ausgabe Interruptable Info im Debug my $isintable = isInterruptable ($hash, $c, 0, 1); # mit Ausgabe Interruptable Info im Debug
my $isConsRcmd = isConsRcmd ($hash, $c); my $isConsRcmd = isConsRcmd ($hash, $c);
$paref->{supplement} = 'swoncond not met' if(!$swoncond);
$paref->{supplement} = 'swoffcond met' if($swoffcond);
if ($paref->{supplement}) {
___setConsumerPlanningState ($paref);
delete $paref->{supplement};
}
if ($auto && $oncom && $swoncond && !$swoffcond && !$iilt && # kein Einschalten wenn zusätzliche Switch off Bedingung oder Sperrzeit zutrifft if ($auto && $oncom && $swoncond && !$swoffcond && !$iilt && # kein Einschalten wenn zusätzliche Switch off Bedingung oder Sperrzeit zutrifft
$simpCstat =~ /planned|priority|starting/xs && $isInTime) { # Verbraucher Start ist geplant && Startzeit überschritten $simpCstat =~ /planned|priority|starting/xs && $isInTime) { # Verbraucher Start ist geplant && Startzeit überschritten
my $mode = ConsumerVal ($hash, $c, "mode", $defcmode); # Consumer Planungsmode my $mode = ConsumerVal ($hash, $c, "mode", $defcmode); # Consumer Planungsmode
my $enable = ___enableSwitchByBatPrioCharge ($paref); # Vorrangladung Batterie ? my $enable = ___enableSwitchByBatPrioCharge ($paref); # Vorrangladung Batterie ?
debugLog ($paref, "consumerSwitching", qq{$name DEBUG> Consumer switch enabled by battery: $enable}); debugLog ($paref, "consumerSwitching", qq{$name DEBUG> Consumer switch enable by battery state: $enable});
if ($mode eq "can" && !$enable) { # Batterieladung - keine Verbraucher "Einschalten" Freigabe if ($mode eq "can" && !$enable) { # Batterieladung - keine Verbraucher "Einschalten" Freigabe
$paref->{ps} = "priority charging battery"; $paref->{ps} = "priority charging battery";
@ -8994,12 +9090,12 @@ sub _checkSetupNotComplete {
### nicht mehr benötigte Readings/Daten löschen - Bereich kann später wieder raus !! ### nicht mehr benötigte Readings/Daten löschen - Bereich kann später wieder raus !!
########################################################################################## ##########################################################################################
my $fcdev = ReadingsVal ($name, "currentForecastDev", undef); #my $fcdev = ReadingsVal ($name, "currentForecastDev", undef);
if ($fcdev) { #if ($fcdev) {
readingsSingleUpdate ($hash, "currentWeatherDev", $fcdev, 0); # readingsSingleUpdate ($hash, "currentWeatherDev", $fcdev, 0);
deleteReadingspec ($hash, "currentForecastDev"); # deleteReadingspec ($hash, "currentForecastDev");
} #}
########################################################################################## ##########################################################################################
my $is = ReadingsVal ($name, 'inverterStrings', undef); # String Konfig my $is = ReadingsVal ($name, 'inverterStrings', undef); # String Konfig
@ -12137,11 +12233,12 @@ return;
################################################################ ################################################################
# Daten der Raw Datensammlung hinzufügen # Daten der Raw Datensammlung hinzufügen
################################################################ ################################################################
sub aiAddRawData { ## no critic "not used" sub aiAddRawData {
my $paref = shift; my $paref = shift;
my $hash = $paref->{hash}; my $hash = $paref->{hash};
my $name = $paref->{name}; my $name = $paref->{name};
my $type = $paref->{type}; my $type = $paref->{type};
my $day = $paref->{day} // strftime "%d", localtime(time); # aktueller Tag (range 01 to 31)
my $ood = $paref->{ood} // 0; # only one (current) day my $ood = $paref->{ood} // 0; # only one (current) day
my $rho = $paref->{rho}; # only this hour of day my $rho = $paref->{rho}; # only this hour of day
@ -12151,10 +12248,13 @@ sub aiAddRawData { ## no critic "not used"
for my $pvd (sort keys %{$data{$type}{$name}{pvhist}}) { for my $pvd (sort keys %{$data{$type}{$name}{pvhist}}) {
next if(!$pvd); next if(!$pvd);
if ($ood) { if ($ood) {
next if($pvd ne $paref->{day}); next if($pvd ne $paref->{day});
} }
last if(int $pvd > int $day);
for my $hod (sort keys %{$data{$type}{$name}{pvhist}{$pvd}}) { for my $hod (sort keys %{$data{$type}{$name}{pvhist}{$pvd}}) {
next if(!$hod || $hod eq '99' || ($rho && $hod ne $rho)); next if(!$hod || $hod eq '99' || ($rho && $hod ne $rho));
@ -14422,7 +14522,7 @@ sub simplifyCstate {
my $ps = shift; my $ps = shift;
$ps = $ps =~ /planned/xs ? 'planned' : $ps = $ps =~ /planned/xs ? 'planned' :
$ps =~ /no\splanning/xs ? 'suspended' : $ps =~ /suspended/xs ? 'suspended' :
$ps =~ /switching\son/xs ? 'starting' : $ps =~ /switching\son/xs ? 'starting' :
$ps =~ /switched\son/xs ? 'started' : $ps =~ /switched\son/xs ? 'started' :
$ps =~ /switching\soff/xs ? 'stopping' : $ps =~ /switching\soff/xs ? 'stopping' :
@ -16476,7 +16576,8 @@ to ensure that the system configuration is correct.
<tr><td> </td><td><b>can</b> - Scheduling takes place at the time when there is probably enough PV surplus available (default). </td></tr> <tr><td> </td><td><b>can</b> - Scheduling takes place at the time when there is probably enough PV surplus available (default). </td></tr>
<tr><td> </td><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; The consumer is not started at the time of planning if the PV surplus is insufficient. </td></tr> <tr><td> </td><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; The consumer is not started at the time of planning if the PV surplus is insufficient. </td></tr>
<tr><td> </td><td><b>must</b> - The consumer is optimally planned, even if there will probably not be enough PV surplus. </td></tr> <tr><td> </td><td><b>must</b> - The consumer is optimally planned, even if there will probably not be enough PV surplus. </td></tr>
<tr><td> </td><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; The consumer is started even if there is insufficient PV surplus. </td></tr> <tr><td> </td><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; The load is started even if there is insufficient PV surplus, provided that
a set "swoncond" condition is met and "swoffcond" is not met. </td></tr>
<tr><td> </td><td> </td></tr> <tr><td> </td><td> </td></tr>
<tr><td> <b>icon</b> </td><td>Icon to represent the consumer in the overview graphic (optional) </td></tr> <tr><td> <b>icon</b> </td><td>Icon to represent the consumer in the overview graphic (optional) </td></tr>
<tr><td> </td><td> </td></tr> <tr><td> </td><td> </td></tr>
@ -18511,7 +18612,8 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.
<tr><td> </td><td><b>can</b> - Die Einplanung erfolgt zum Zeitpunkt mit wahrscheinlich genügend verfügbaren PV Überschuß (default) </td></tr> <tr><td> </td><td><b>can</b> - Die Einplanung erfolgt zum Zeitpunkt mit wahrscheinlich genügend verfügbaren PV Überschuß (default) </td></tr>
<tr><td> </td><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Der Start des Verbrauchers zum Planungszeitpunkt unterbleibt bei ungenügendem PV-Überschuß. </td></tr> <tr><td> </td><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Der Start des Verbrauchers zum Planungszeitpunkt unterbleibt bei ungenügendem PV-Überschuß. </td></tr>
<tr><td> </td><td><b>must</b> - der Verbraucher wird optimiert eingeplant auch wenn wahrscheinlich nicht genügend PV Überschuß vorhanden sein wird </td></tr> <tr><td> </td><td><b>must</b> - der Verbraucher wird optimiert eingeplant auch wenn wahrscheinlich nicht genügend PV Überschuß vorhanden sein wird </td></tr>
<tr><td> </td><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Der Start des Verbrauchers erfolgt auch bei ungenügendem PV-Überschuß. </td></tr> <tr><td> </td><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Der Start des Verbrauchers erfolgt auch bei ungenügendem PV-Überschuß sofern eine
gesetzte "swoncond" Bedingung erfüllt und "swoffcond" nicht erfüllt ist. </td></tr>
<tr><td> </td><td> </td></tr> <tr><td> </td><td> </td></tr>
<tr><td> <b>icon</b> </td><td>Icon zur Darstellung des Verbrauchers in der Übersichtsgrafik (optional) </td></tr> <tr><td> <b>icon</b> </td><td>Icon zur Darstellung des Verbrauchers in der Übersichtsgrafik (optional) </td></tr>
<tr><td> </td><td> </td></tr> <tr><td> </td><td> </td></tr>