2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-04-22 20:24:36 +00:00

76_SolarForecast: version 1.47.0

git-svn-id: https://svn.fhem.de/fhem/trunk@29711 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
nasseeder1 2025-03-02 19:08:55 +00:00
parent 0cbeacdf82
commit 605a4b2ead

View File

@ -38,7 +38,7 @@ use POSIX;
use GPUtils qw(GP_Import GP_Export); # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt
use Time::HiRes qw(gettimeofday tv_interval);
use Math::Trig;
use List::Util qw(max);
use List::Util qw(max shuffle);
eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; ## no critic 'eval'
eval "use FHEM::Utility::CTZ qw(:all);1;" or my $ctzAbsent = 1; ## no critic 'eval'
@ -159,7 +159,10 @@ BEGIN {
# Versions History intern
my %vNotesIntern = (
"1.46.4" => "23.02.2025 _flowGraphic: fix clculation of node2home (Forum: https://forum.fhem.de/index.php?msg=1334798) ".
"1.47.0" => "02.03.2025 aiInit: change AI init sequence, use Random Forest with Ensemble algorithm ".
"_beamGraphic.*: change decimal places für battery SoC ",
"1.46.5" => "28.02.2025 new ctrlSpecialReadings key todayConsumptionForecastDay ",
"1.46.4" => "25.02.2025 _flowGraphic: fix clculation of node2home (Forum: https://forum.fhem.de/index.php?msg=1334798) ".
"_transferBatteryValues: change Debug Logging ",
"1.46.3" => "22.02.2025 new sub getConsumerMintime, consumer key 'mintime' can handle a device/reading combination that deliver minutes ".
"reports violation of the continuity specification for battery in/out energy ",
@ -402,16 +405,17 @@ use constant {
GMFILERANDOM => 10800, # Random AddOn zu GMFILEREPEAT
IDXLIMIT => 900000, # Notification System: Indexe > IDXLIMIT sind reserviert für Steuerungsaufgaben
AINUMTREES => 10, # Anzahl der Entscheidungsbäume im Ensemble
AITRBLTO => 7200, # KI Training BlockingCall Timeout
AIBCTHHLD => 0.2, # Schwelle der KI Trainigszeit ab der BlockingCall benutzt wird
AITRSTARTDEF => 2, # default Stunde f. Start AI-Training
AISTDUDEF => 1825, # default Haltezeit KI Raw Daten (Tage)
AISPREADUPLIM => 120, # obere Abweichungsgrenze (%) AI 'Spread' von API Prognose
AISPREADLOWLIM => 80, # untere Abweichungsgrenze (%) AI 'Spread' von API Prognose
AIACCUPLIM => 130, # obere Abweichungsgrenze (%) AI 'Accurate' von API Prognose
AIACCLOWLIM => 70, # untere Abweichungsgrenze (%) AI 'Accurate' von API Prognose
AIACCTRNMIN => 5500, # Mindestanzahl KI Trainingssätze für Verwendung "KI Accurate"
AISPREADTRNMIN => 7000, # Mindestanzahl KI Trainingssätze für Verwendung "KI Spreaded"
AIACCUPLIM => 150, # obere Abweichungsgrenze (%) AI 'Accurate' von API Prognose
AIACCLOWLIM => 50, # untere Abweichungsgrenze (%) AI 'Accurate' von API Prognose
AIACCTRNMIN => 2500, # Mindestanzahl KI Trainingssätze für Verwendung "KI Accurate"
AISPREADTRNMIN => 5000, # Mindestanzahl KI Trainingssätze für Verwendung "KI Spreaded"
SOLAPIREPDEF => 3600, # default Abrufintervall SolCast API (s)
FORAPIREPDEF => 900, # default Abrufintervall ForecastSolar API (s)
@ -1241,45 +1245,46 @@ my %hef = (
);
my %hcsr = ( # Funktiontemplate zur Erstellung optionaler Statistikreadings
currentAPIinterval => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => 0 }, # par = Parameter zur spezifischen Verwendung
lastretrieval_time => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => '-' },
lastretrieval_timestamp => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => '-' },
response_message => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => '-' },
todayMaxAPIcalls => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => 'apimaxreq' },
todayDoneAPIcalls => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => 0 },
todayDoneAPIrequests => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => 0 },
todayRemainingAPIcalls => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => 'apimaxreq' },
todayRemainingAPIrequests => { fnr => 1, fn => \&StatusAPIVal, par => '', unit => '', def => 'apimaxreq' },
runTimeCentralTask => { fnr => 2, fn => \&CurrentVal, par => '', unit => ' s', def => '-' },
runTimeLastAPIAnswer => { fnr => 2, fn => \&CurrentVal, par => '', unit => '', def => '-' },
runTimeLastAPIProc => { fnr => 2, fn => \&CurrentVal, par => '', unit => '', def => '-' },
allStringsFullfilled => { fnr => 2, fn => \&CurrentVal, par => '', unit => '', def => 0 },
todayConForecastTillSunset => { fnr => 2, fn => \&CurrentVal, par => 'tdConFcTillSunset', unit => ' Wh', def => 0 },
runTimeTrainAI => { fnr => 3, fn => \&CircularVal, par => 99, unit => ' s', def => '-' },
todayConsumption => { fnr => 3, fn => \&CircularVal, par => 99, unit => ' Wh', def => 0 },
BatPowerIn_Sum => { fnr => 4, fn => \&CurrentVal, par => 'batpowerinsum', unit => ' W', def => '-' },
BatPowerOut_Sum => { fnr => 4, fn => \&CurrentVal, par => 'batpoweroutsum', unit => ' W', def => '-' },
SunHours_Remain => { fnr => 4, fn => \&CurrentVal, par => '', unit => '', def => 0 }, # fnr => 3 -> Custom Calc
SunMinutes_Remain => { fnr => 4, fn => \&CurrentVal, par => '', unit => '', def => 0 },
dayAfterTomorrowPVforecast => { fnr => 4, fn => \&RadiationAPIVal, par => 'pv_estimate50', unit => '', def => 0 },
todayGridFeedIn => { fnr => 4, fn => \&CircularVal, par => 99, unit => '', def => 0 },
todayGridConsumption => { fnr => 4, fn => \&CircularVal, par => 99, unit => '', def => 0 },
todayConsumptionForecast => { fnr => 4, fn => \&NexthoursVal, par => 'confc', unit => ' Wh', def => '-' },
conForecastTillNextSunrise => { fnr => 4, fn => \&NexthoursVal, par => 'confc', unit => ' Wh', def => 0 },
todayBatInSum => { fnr => 4, fn => \&CircularVal, par => 99, unit => ' Wh', def => 0 },
todayBatOutSum => { fnr => 4, fn => \&CircularVal, par => 99, unit => ' Wh', def => 0 },
currentAPIinterval => { fnr => 1, fn => \&StatusAPIVal, par => '', par1 => '', unit => '', def => 0 }, # par = Parameter zur spezifischen Verwendung
lastretrieval_time => { fnr => 1, fn => \&StatusAPIVal, par => '', par1 => '', unit => '', def => '-' },
lastretrieval_timestamp => { fnr => 1, fn => \&StatusAPIVal, par => '', par1 => '', unit => '', def => '-' },
response_message => { fnr => 1, fn => \&StatusAPIVal, par => '', par1 => '', unit => '', def => '-' },
todayMaxAPIcalls => { fnr => 1, fn => \&StatusAPIVal, par => '', par1 => '', unit => '', def => 'apimaxreq' },
todayDoneAPIcalls => { fnr => 1, fn => \&StatusAPIVal, par => '', par1 => '', unit => '', def => 0 },
todayDoneAPIrequests => { fnr => 1, fn => \&StatusAPIVal, par => '', par1 => '', unit => '', def => 0 },
todayRemainingAPIcalls => { fnr => 1, fn => \&StatusAPIVal, par => '', par1 => '', unit => '', def => 'apimaxreq' },
todayRemainingAPIrequests => { fnr => 1, fn => \&StatusAPIVal, par => '', par1 => '', unit => '', def => 'apimaxreq' },
runTimeCentralTask => { fnr => 2, fn => \&CurrentVal, par => '', par1 => '', unit => ' s', def => '-' },
runTimeLastAPIAnswer => { fnr => 2, fn => \&CurrentVal, par => '', par1 => '', unit => '', def => '-' },
runTimeLastAPIProc => { fnr => 2, fn => \&CurrentVal, par => '', par1 => '', unit => '', def => '-' },
allStringsFullfilled => { fnr => 2, fn => \&CurrentVal, par => '', par1 => '', unit => '', def => 0 },
todayConForecastTillSunset => { fnr => 2, fn => \&CurrentVal, par => 'tdConFcTillSunset', par1 => '', unit => ' Wh', def => 0 },
runTimeTrainAI => { fnr => 3, fn => \&CircularVal, par => 99, par1 => '', unit => ' s', def => '-' },
todayConsumption => { fnr => 3, fn => \&CircularVal, par => 99, par1 => '', unit => ' Wh', def => 0 },
todayConsumptionForecastDay => { fnr => 4, fn => \&HistoryVal, par => 99, par1 => 'confc', unit => ' Wh', def => '-' },
BatPowerIn_Sum => { fnr => 5, fn => \&CurrentVal, par => 'batpowerinsum', par1 => '', unit => ' W', def => '-' },
BatPowerOut_Sum => { fnr => 5, fn => \&CurrentVal, par => 'batpoweroutsum', par1 => '', unit => ' W', def => '-' },
SunHours_Remain => { fnr => 5, fn => \&CurrentVal, par => '', par1 => '', unit => '', def => 0 }, # fnr => 3 -> Custom Calc
SunMinutes_Remain => { fnr => 5, fn => \&CurrentVal, par => '', par1 => '', unit => '', def => 0 },
dayAfterTomorrowPVforecast => { fnr => 5, fn => \&RadiationAPIVal, par => 'pv_estimate50', par1 => '', unit => '', def => 0 },
todayGridFeedIn => { fnr => 5, fn => \&CircularVal, par => 99, par1 => '', unit => '', def => 0 },
todayGridConsumption => { fnr => 5, fn => \&CircularVal, par => 99, par1 => '', unit => '', def => 0 },
todayConsumptionForecast => { fnr => 5, fn => \&HistoryVal, par => '', par1 => 'confc', unit => ' Wh', def => '-' },
conForecastTillNextSunrise => { fnr => 5, fn => \&NexthoursVal, par => 'confc', par1 => '', unit => ' Wh', def => 0 },
todayBatInSum => { fnr => 5, fn => \&CircularVal, par => 99, par1 => '', unit => ' Wh', def => 0 },
todayBatOutSum => { fnr => 5, fn => \&CircularVal, par => 99, par1 => '', unit => ' Wh', def => 0 },
);
for my $csr (1..MAXCONSUMER) {
$csr = sprintf "%02d", $csr;
$hcsr{'currentRunMtsConsumer_'.$csr}{fnr} = 4;
$hcsr{'currentRunMtsConsumer_'.$csr}{fnr} = 5;
$hcsr{'currentRunMtsConsumer_'.$csr}{fn} = \&ConsumerVal;
$hcsr{'currentRunMtsConsumer_'.$csr}{par} = 'cycleTime';
$hcsr{'currentRunMtsConsumer_'.$csr}{unit} = ' min';
$hcsr{'currentRunMtsConsumer_'.$csr}{def} = 0;
$hcsr{'runTimeAvgDayConsumer_'.$csr}{fnr} = 4;
$hcsr{'runTimeAvgDayConsumer_'.$csr}{fnr} = 5;
$hcsr{'runTimeAvgDayConsumer_'.$csr}{fn} = \&ConsumerVal;
$hcsr{'runTimeAvgDayConsumer_'.$csr}{par} = 'runtimeAvgDay';
$hcsr{'runTimeAvgDayConsumer_'.$csr}{unit} = ' min';
@ -1289,19 +1294,19 @@ my %hcsr = (
for my $bn (1..MAXBATTERIES) {
$bn = sprintf "%02d", $bn;
$hcsr{'daysUntilBatteryCare_'.$bn}{fnr} = 4;
$hcsr{'daysUntilBatteryCare_'.$bn}{fnr} = 5;
$hcsr{'daysUntilBatteryCare_'.$bn}{fn} = \&CircularVal;
$hcsr{'daysUntilBatteryCare_'.$bn}{par} = 99;
$hcsr{'daysUntilBatteryCare_'.$bn}{unit} = '';
$hcsr{'daysUntilBatteryCare_'.$bn}{def} = '-';
$hcsr{'todayBatIn_'.$bn}{fnr} = 4;
$hcsr{'todayBatIn_'.$bn}{fnr} = 5;
$hcsr{'todayBatIn_'.$bn}{fn} = \&CircularVal;
$hcsr{'todayBatIn_'.$bn}{par} = 99;
$hcsr{'todayBatIn_'.$bn}{unit} = ' Wh';
$hcsr{'todayBatIn_'.$bn}{def} = 0;
$hcsr{'todayBatOut_'.$bn}{fnr} = 4;
$hcsr{'todayBatOut_'.$bn}{fnr} = 5;
$hcsr{'todayBatOut_'.$bn}{fn} = \&CircularVal;
$hcsr{'todayBatOut_'.$bn}{par} = 99;
$hcsr{'todayBatOut_'.$bn}{unit} = ' Wh';
@ -2353,8 +2358,6 @@ sub _setreset { ## no critic "not used"
}
}
aiInit ($paref);
return;
}
@ -5328,22 +5331,32 @@ sub __getaiRuleStrings { ## no critic "not used"
return 'the AI usage is not prepared' if(!isPrepared4AI ($hash));
my $dtree = AiDetreeVal ($hash, 'aitrained', undef);
if (!$dtree) {
return 'AI trained object is missed';
}
my $objref = AiDetreeVal ($hash, 'aitrained', '');
return 'AI trained object is missed or not an ARRAY' if(ref $objref ne 'ARRAY');
my $rs = 'no rules delivered';
my (@rsl, $nodes, $depth);
my (@rsl, %entities);
my $tn = 0;
eval { @rsl = $dtree->rule_statements(); # Returns a list of strings that describe the tree in rule-form
$nodes = $dtree->nodes(); # Returns the number of nodes in the trained decision tree
$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.
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;
}
$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.
1;
}
or do { return $@;
};
}
my $atf = CircularVal ($hash, 99, 'aitrainLastFinishTs', 0);
$atf = '<b>'.$hqtxt{ailatr}{$lang}.' </b>'.($atf ? (timestampToTimestring ($atf, $lang))[0] : '-');
@ -5351,7 +5364,13 @@ sub __getaiRuleStrings { ## no critic "not used"
if (@rsl) {
my $l = scalar @rsl;
$rs = "<b>Number of Rules: $l / Number of Nodes: $nodes / Depth: $depth</b>\n";
$rs = "<b>Trained AI Object contains an Ensemble of $tn trees (only the first Tree is printed out)</b>\n";
for my $tree (1..$tn) {
$rs .= "<b>Tree: $tree / Number of Rules: $entities{$tree}{rules} / Number of Nodes: $entities{$tree}{nodes} / Depth: $entities{$tree}{depth} </b>\n";
}
$rs .= "\n\n";
$rs .= "Rules: ".$hqtxt{airule}{$lang}."\n";
$rs .= "Nodes: ".$hqtxt{ainode}{$lang}."\n";
$rs .= "Depth: ".$hqtxt{aidept}{$lang};
@ -7053,19 +7072,28 @@ sub readCacheFile {
my $hash = $defs{$name};
if ($cachename eq 'aitrained') {
my ($err, $dtree) = fileRetrieve ($file);
my ($err, $objref) = fileRetrieve ($file);
if (!$err && $dtree) {
my $valid = $dtree->isa('AI::DecisionTree');
if (!$err && $objref) {
if (ref $objref ne 'ARRAY') {
return "The file $file was restored but the content is not an ARRAY";
}
for my $obj (@{$objref}) {
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};
if ($valid) {
$data{$name}{aidectree}{aitrained} = $dtree;
$data{$name}{current}{aitrainstate} = 'ok';
Log3 ($name, 3, qq{$name - cached data "$title" restored});
return;
}
}
delete $data{$name}{circular}{99}{aitrainLastFinishTs};
delete $data{$name}{circular}{99}{runTimeTrainAI};
@ -7175,10 +7203,14 @@ sub writeCacheToFile {
my ($error, $err, $lw);
if ($cachename eq 'aitrained') {
my $dtree = AiDetreeVal ($hash, 'aitrained', '');
return if(ref $dtree ne 'AI::DecisionTree');
my $objref = AiDetreeVal ($hash, 'aitrained', '');
return 'trained object is not an ARRAY' if(ref $objref ne 'ARRAY');
$error = fileStore ($dtree, $file);
for my $obj (@{$objref}) {
return 'wrong trained object' if(ref $obj ne 'AI::DecisionTree');
}
$error = fileStore ($objref, $file);
if ($error) {
$err = qq{ERROR while writing AI data to file "$file": $error};
@ -7737,7 +7769,7 @@ sub centralTask {
_calcDataEveryFullHour ($centpars); # Daten berechnen/speichern die nur einmal nach jeder vollen Stunde ermittelt werden
_saveEnergyConsumption ($centpars); # Energie Hausverbrauch speichern
_createSummaries ($centpars); # Zusammenfassungen erstellen
_genSpecialReadings ($centpars); # optionale Statistikreadings erstellen
_genSpecialReadings ($centpars); # optionale Spezialreadings erstellen
userExit ($centpars); # User spezifische Funktionen ausführen
setTimeTracking ($hash, $cst, 'runTimeCentralTask'); # Zyklus-Laufzeit ermitteln
@ -12927,6 +12959,9 @@ sub _genSpecialReadings {
storeReading ($prpo.'_'.$kpi, &{$hcsr{$kpi}{fn}} ($hash, $hcsr{$kpi}{par}, $kpi, $def).$hcsr{$kpi}{unit});
}
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);
my $shr = ($ss - $t) / 3600;
@ -13029,7 +13064,6 @@ sub _genSpecialReadings {
my $dayaftertomorrow = strftime "%Y-%m-%d", localtime($t + 172800); # Datum von Übermorgen
my @allstrings = split ",", AttrVal ($name, 'setupInverterStrings', '');
my $fcsumdat = 0;
my $type = $paref->{type};
for my $strg (@allstrings) {
for my $starttmstr (sort keys %{$data{$name}{solcastapi}{$strg}}) {
@ -13077,8 +13111,16 @@ sub _genSpecialReadings {
}
if ($kpi eq 'todayConsumptionForecast') {
my $type = $paref->{type};
for my $hod (sort keys %{$data{$name}{pvhist}{$day}}) {
next if(!$hod || $hod == 99);
my $confc = &{$hcsr{$kpi}{fn}} ($hash, $day, $hod, $hcsr{$kpi}{par1}, $def);
storeReading ($prpo.'_'.$kpi.'_'.$hod, $confc.$hcsr{$kpi}{unit});
}
}
if ($kpi eq 'todayConsumptionForecastNh') {
for my $idx (sort keys %{$data{$name}{nexthours}}) {
my $istoday = NexthoursVal ($hash, $idx, 'today', 0);
last if(!$istoday);
@ -13091,7 +13133,6 @@ sub _genSpecialReadings {
}
if ($kpi eq 'conForecastTillNextSunrise') {
my $type = $paref->{type};
my $confc = 0;
my $dono = 1;
my $hrs = 0;
@ -14796,7 +14837,7 @@ sub _beamGraphicFirstHour {
$beam1cont eq 'energycosts' ? $val6 :
$beam1cont eq 'gridfeedin' ? $val7 :
$beam1cont eq 'feedincome' ? $val8 :
$beam1cont =~ /batsocforecast_/xs ? $hbsocs->{0}{(split '_', $beam1cont)[1]} :
$beam1cont =~ /batsocforecast_/xs ? ($hbsocs->{0}{(split '_', $beam1cont)[1]} < 100 ? sprintf "%.1f", $hbsocs->{0}{(split '_', $beam1cont)[1]} : 100) :
undef;
$hfcg->{0}{beam2} = $beam2cont eq 'pvForecast' ? $val1 :
@ -14807,7 +14848,7 @@ sub _beamGraphicFirstHour {
$beam2cont eq 'energycosts' ? $val6 :
$beam2cont eq 'gridfeedin' ? $val7 :
$beam2cont eq 'feedincome' ? $val8 :
$beam2cont =~ /batsocforecast_/xs ? $hbsocs->{0}{(split '_', $beam2cont)[1]} :
$beam2cont =~ /batsocforecast_/xs ? ($hbsocs->{0}{(split '_', $beam2cont)[1]} < 100 ? sprintf "%.1f", $hbsocs->{0}{(split '_', $beam2cont)[1]} : 100) :
undef;
$hfcg->{0}{beam1} //= 0;
@ -14955,7 +14996,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]} < 100 ? sprintf "%.1f", $hbsocs->{$i}{(split '_', $beam1cont)[1]} : 100) :
undef;
$hfcg->{$i}{beam2} = $beam2cont eq 'pvForecast' ? $val1 :
@ -14966,7 +15007,7 @@ sub _beamGraphicRemainingHours {
$beam2cont eq 'energycosts' ? $val6 :
$beam2cont eq 'gridfeedin' ? $val7 :
$beam2cont eq 'feedincome' ? $val8 :
$beam2cont =~ /batsocforecast_/xs ? $hbsocs->{$i}{(split '_', $beam2cont)[1]} :
$beam2cont =~ /batsocforecast_/xs ? ($hbsocs->{$i}{(split '_', $beam2cont)[1]} < 100 ? sprintf "%.1f", $hbsocs->{$i}{(split '_', $beam2cont)[1]} : 100) :
undef;
$hfcg->{$i}{time_str} = sprintf ('%02d', $hfcg->{$i}{time}-1).$hourstyle;
@ -15763,11 +15804,9 @@ sub _flowGraphic {
######################################################
my $pnodesum = __normDecPlaces ($ppall + $pv2node); # Erzeugung Summe im Knoten
$node2bat -= $pv2bat; # Knoten-Bat -> abzüglich Direktladung (pv2bat)
#Log3 ($name, 1, "$name - pv2bat: $pv2bat, node2bat:$node2bat ");
$pnodesum += $node2bat < 0 ? abs $node2bat : 0; # V 1.46.4 - Batterie ist voll und SolarLader liefert an Knoten
#my $node2home = __normDecPlaces ($cself + $ppall); # Energiefluß vom Knoten zum Haus: Selbstverbrauch + alle Producer (Batterie-In/Solar-Ladegeräte sind nicht in SelfConsumtion enthalten)
my $node2home = __normDecPlaces ($pnodesum - $node2grid - ($node2bat > 0 ? $node2bat : 0)); # V 1.46.4 - Energiefluß vom Knoten zum Haus
my $node2home = $pnodesum - $node2grid - ($node2bat > 0 ? $node2bat : 0); # V 1.46.4 - Energiefluß vom Knoten zum Haus
$node2home = __normDecPlaces ($node2home); # V 1.46.4
## SVG Box initialisieren mit Grid-Icon
#########################################
@ -17001,6 +17040,105 @@ sub outputMessages {
return $out;
}
################################################################
# KI Instanz(en) aus Raw Daten Hash erzeugen
# mit Ensemble-Algorithmus
################################################################
sub aiAddInstancePV {
my $paref = shift;
my $name = $paref->{name};
my $taa = $paref->{taa}; # do train after add
my $hash = $defs{$name};
return if(!isPrepared4AI ($hash));
sub sample_data { # Hilfsfunktion zum Erstellen einer Stichprobe der Daten
my $data = shift;
my @shuffled = shuffle @$data;
my $sample_size = int (scalar (@$data) * 0.8);
return @shuffled[0 .. $sample_size - 1];
}
my @data;
for my $idx (sort keys %{$data{$name}{aidectree}{airaw}}) {
next if(!$idx);
my $pvrl = AiRawdataVal ($hash, $idx, 'pvrl', undef);
next if(!defined $pvrl);
my $hod = AiRawdataVal ($hash, $idx, 'hod', undef);
next if(!defined $hod);
my $rad1h = AiRawdataVal ($hash, $idx, 'rad1h', 0);
next if($rad1h <= 0);
my $temp = AiRawdataVal ($hash, $idx, 'temp', undef);
my $wcc = AiRawdataVal ($hash, $idx, 'wcc', undef);
my $rr1c = AiRawdataVal ($hash, $idx, 'rr1c', undef);
my $sunalt = AiRawdataVal ($hash, $idx, 'sunalt', 0);
my $tbin = temp2bin ($temp) if(defined $temp);
my $cbin = cloud2bin ($wcc) if(defined $wcc);
my $sabin = sunalt2bin ($sunalt);
push @data, { rad1h => $rad1h, temp => $tbin, wcc => $cbin, rr1c => $rr1c, sunalt => $sunalt, hod => $hod, pvrl => $pvrl };
}
return if(!scalar @data);
for my $tn (1 .. AINUMTREES) { # Trainiere mehrere Entscheidungsbäume auf unterschiedlichen Stichproben
my @sampled_data = sample_data (\@data);
my ($err, $dtree) = aiInit ($paref);
return if($err);
my $aiAddedToTrain = 0;
for my $instance (@sampled_data) {
eval { $dtree->add_instance (attributes => { rad1h => $instance->{rad1h},
temp => $instance->{temp},
wcc => $instance->{wcc},
rr1c => $instance->{rr1c},
sunalt => $instance->{sunalt},
hod => $instance->{hod}
},
result => $instance->{pvrl}
);
1;
}
or do { Log3 ($name, 1, "$name - aiAddInstancePV ERROR: $@");
$data{$name}{current}{aiaddistate} = $@;
return;
};
$aiAddedToTrain++;
debugLog ($paref, 'aiProcess', "AI Instance added Tree $tn - ".
"hod: $instance->{hod}, ".
"sunalt: $instance->{sunalt}, ".
"rad1h: $instance->{rad1h}, pvrl: instance->{pvrl}, ".
"wcc: ".(defined $instance->{wcc} ? $instance->{wcc} : '-').", ".
"rr1c: ".(defined $instance->{rr1c} ? $instance->{rr1c} : '-').", ".
"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;
}
$data{$name}{current}{aiaddistate} = 'ok';
if ($taa) {
manageTrain ($paref);
}
return;
}
###############################################################
# Eintritt in den KI Train Prozess normal/Blocking
###############################################################
@ -17043,6 +17181,117 @@ sub manageTrain {
return;
}
################################################################
# KI trainieren
################################################################
sub aiTrain {
my $paref = shift;
my $name = $paref->{name};
my $block = $paref->{block} // 0;
my $hash = $defs{$name};
my ($serial, $err);
if (!isPrepared4AI ($hash)) {
$err = CurrentVal ($hash, 'aicanuse', '');
$serial = encode_base64 (Serialize ( { name => $name,
aitrainstate => "Train: not performed -> $err",
aicanuse => $err
}
), "");
$block ? return ($serial) : return \&finishTrain ($serial);
}
my $cst = [gettimeofday]; # Train Startzeit
my $object = AiDetreeVal ($hash, 'object', undef);
if (!$object) {
$err = 'no AI::DecisionTree object present';
$serial = encode_base64 (Serialize ( {name => $name,
aitrainstate => "Train: not performed -> $err",
aiinitstate => "Init: $err",
aicanuse => 'ok'
}
), "");
$block ? return ($serial) : return \&finishTrain ($serial);
}
my @ensemble; # Erstelle das Ensemble
my %entities;
for my $tn (1 .. AINUMTREES) { # 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;
}
or do { Log3 ($name, 1, "$name - aiTrain ERROR: $@");
$err = (split / at/, $@)[0];
$serial = encode_base64 (Serialize ( {name => $name,
aitrainstate => "Train: $err",
aicanuse => 'ok'
}
), "");
$block ? return ($serial) : return \&finishTrain ($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);
my $rn;
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{AI trained and saved data into file: }.$aitrained.$name);
debugLog ($paref, 'aiProcess', qq{Training instances and their associated information where purged from the AI object});
}
else {
$serial = encode_base64 (Serialize ( {name => $name,
aitrainstate => "Train performed but not written -> $err",
aicanuse => 'ok'
}
), "");
$block ? return ($serial) : return \&finishTrain ($serial);
}
setTimeTracking ($hash, $cst, 'runTimeTrainAI'); # Zyklus-Laufzeit ermitteln
$serial = encode_base64 (Serialize ( {name => $name,
aitrainstate => 'ok',
runTimeTrainAI => CurrentVal ($hash, 'runTimeTrainAI', ''),
aitrainLastFinishTs => int time,
aiRulesNumber => $rn, # Returns a list of strings that describe the tree in rule-form
aicanuse => 'ok'
}
)
, "");
delete $data{$name}{current}{runTimeTrainAI};
$block ? return ($serial) : return \&finishTrain ($serial);
return;
}
###############################################################
# Restaufgaben nach AI Train
###############################################################
@ -17062,6 +17311,7 @@ sub finishTrain {
my $aiRulesNumber = $paref->{aiRulesNumber};
delete $data{$name}{circular}{99}{aiRulesNumber};
delete $data{$name}{aidectree}{object};
$data{$name}{current}{aiAddedToTrain} = 0;
$data{$name}{current}{aicanuse} = $aicanuse;
@ -17106,6 +17356,7 @@ sub abortTrain {
Log3 ($name, 1, "$name -> BlockingCall $hash->{HELPER}{AIBLOCKRUNNING}{fn} pid:$hash->{HELPER}{AIBLOCKRUNNING}{pid} aborted: $cause");
delete($hash->{HELPER}{AIBLOCKRUNNING});
delete $data{$name}{aidectree}{object};
$data{$name}{current}{aitrainstate} = 'Traing (Child) process timed out';
$data{$name}{current}{aiAddedToTrain} = 0;
@ -17113,157 +17364,6 @@ sub abortTrain {
return;
}
################################################################
# KI Instanz(en) aus Raw Daten Hash erzeugen
################################################################
sub aiAddInstancePV { ## no critic "not used"
my $paref = shift;
my $name = $paref->{name};
my $type = $paref->{type};
my $taa = $paref->{taa}; # do train after add
my $hash = $defs{$name};
return if(!isPrepared4AI ($hash));
my $err = aiInit ($paref);
return if($err);
my $dtree = AiDetreeVal ($hash, 'object', undef);
$data{$name}{current}{aiAddedToTrain} = 0;
for my $idx (sort keys %{$data{$name}{aidectree}{airaw}}) {
next if(!$idx);
my $pvrl = AiRawdataVal ($hash, $idx, 'pvrl', undef);
next if(!defined $pvrl);
my $hod = AiRawdataVal ($hash, $idx, 'hod', undef);
next if(!defined $hod);
my $rad1h = AiRawdataVal ($hash, $idx, 'rad1h', 0);
next if($rad1h <= 0);
my $temp = AiRawdataVal ($hash, $idx, 'temp', undef);
my $wcc = AiRawdataVal ($hash, $idx, 'wcc', undef);
my $rr1c = AiRawdataVal ($hash, $idx, 'rr1c', undef);
my $sunalt = AiRawdataVal ($hash, $idx, 'sunalt', 0);
my $tbin = temp2bin ($temp) if(defined $temp);
my $cbin = cloud2bin ($wcc) if(defined $wcc);
my $sabin = sunalt2bin ($sunalt);
eval { $dtree->add_instance (attributes => { rad1h => $rad1h,
temp => $tbin,
wcc => $cbin,
rr1c => $rr1c,
sunalt => $sunalt,
hod => $hod
},
result => $pvrl
);
1;
}
or do { Log3 ($name, 1, "$name - aiAddInstancePV ERROR: $@");
$data{$name}{current}{aiaddistate} = $@;
return;
};
$data{$name}{current}{aiAddedToTrain}++;
debugLog ($paref, 'aiProcess', "AI Instance added $idx - hod: $hod, sunalt: $sunalt, rad1h: $rad1h, pvrl: $pvrl, wcc: ".(defined $wcc ? $wcc : '-').", rr1c: ".(defined $rr1c ? $rr1c : '-').", temp: ".(defined $temp ? $temp : '-'), 4);
}
debugLog ($paref, 'aiProcess', "AI Instance add - ".$data{$name}{current}{aiAddedToTrain}." entities added for training ".(AttrVal ($name, 'verbose', 3) != 4 ? '(set verbose 4 for output more detail)' : ''));
$data{$name}{aidectree}{object} = $dtree;
$data{$name}{current}{aiaddistate} = 'ok';
if ($taa) {
manageTrain ($paref);
}
return;
}
################################################################
# KI trainieren
################################################################
sub aiTrain { ## no critic "not used"
my $paref = shift;
my $name = $paref->{name};
my $block = $paref->{block} // 0;
my $hash = $defs{$name};
my ($serial, $err);
if (!isPrepared4AI ($hash)) {
$err = CurrentVal ($hash, 'aicanuse', '');
$serial = encode_base64 (Serialize ( { name => $name,
aitrainstate => "Train: not performed -> $err",
aicanuse => $err
}
), "");
$block ? return ($serial) : return \&finishTrain ($serial);
}
my $cst = [gettimeofday]; # Zyklus-Startzeit
my $dtree = AiDetreeVal ($hash, 'object', undef);
if (!$dtree) {
$err = 'no AI::DecisionTree object present';
$serial = encode_base64 (Serialize ( {name => $name,
aitrainstate => "Train: not performed -> $err",
aiinitstate => "Init: $err",
aicanuse => 'ok'
}
), "");
$block ? return ($serial) : return \&finishTrain ($serial);
}
eval { $dtree->train();
1;
}
or do { Log3 ($name, 1, "$name - aiTrain ERROR: $@");
$err = (split / at/, $@)[0];
$serial = encode_base64 (Serialize ( {name => $name,
aitrainstate => "Train: $err",
aicanuse => 'ok'
}
), "");
$block ? return ($serial) : return \&finishTrain ($serial);
};
$data{$name}{aidectree}{aitrained} = $dtree;
$err = writeCacheToFile ($hash, 'aitrained', $aitrained.$name);
if (!$err) {
debugLog ($paref, 'aiProcess', qq{AI trained number of entities: }. $data{$name}{current}{aiAddedToTrain});
debugLog ($paref, 'aiProcess', qq{AI trained and saved data into file: }.$aitrained.$name);
debugLog ($paref, 'aiProcess', qq{Training instances and their associated information where purged from the AI object});
}
setTimeTracking ($hash, $cst, 'runTimeTrainAI'); # Zyklus-Laufzeit ermitteln
$serial = encode_base64 (Serialize ( {name => $name,
aitrainstate => 'ok',
runTimeTrainAI => CurrentVal ($hash, 'runTimeTrainAI', ''),
aitrainLastFinishTs => int time,
aiRulesNumber => scalar $dtree->rule_statements(), # Returns a list of strings that describe the tree in rule-form
aicanuse => 'ok'
}
)
, "");
delete $data{$name}{current}{runTimeTrainAI};
$block ? return ($serial) : return \&finishTrain ($serial);
return;
}
################################################################
# AI Ergebnis für ermitteln
################################################################
@ -17278,11 +17378,8 @@ sub aiGetResult {
return 'AI usage is not prepared' if(!isPrepared4AI ($hash, 'full'));
my $dtree = AiDetreeVal ($hash, 'aitrained', undef);
if (!$dtree) {
return 'AI trained object is missed';
}
my $objref = AiDetreeVal ($hash, 'aitrained', '');
return 'AI trained object is missed or not an ARRAY' if(ref $objref ne 'ARRAY');
my $rad1h = NexthoursVal ($hash, $nhtstr, 'rad1h', 0);
return "no rad1h for hod: $hod" if($rad1h <= 0);
@ -17299,48 +17396,86 @@ sub aiGetResult {
my $cbin = cloud2bin ($wcc);
my $sabin = sunalt2bin ($sunalt);
my $pvaifc;
eval { $pvaifc = $dtree->get_result (attributes => { rad1h => $rad1h,
temp => $tbin,
wcc => $cbin,
rr1c => $rr1c,
sunalt => $sabin,
sunaz => $sunaz,
hod => $hod
}
);
};
if ($@) {
Log3 ($name, 1, "$name - aiGetResult ERROR: $@");
return $@;
}
if (defined $pvaifc) {
debugLog ($paref, 'aiData', qq{AI accurate result found: pvaifc: $pvaifc (hod: $hod, sunaz: $sunaz, sunalt: $sabin, Rad1h: $rad1h, wcc: $wcc, rr1c: $rr1c, temp: $tbin)});
return ('accurate', $pvaifc);
}
(my $msg, $pvaifc) = _aiGetSpread ( { name => $name,
type => $type,
my $new_data = { # Prognose für neue Daten
rad1h => $rad1h,
temp => $tbin,
wcc => $cbin,
rr1c => $rr1c,
sunalt => $sabin,
sunaz => $sunaz,
hod => $hod,
dtree => $dtree,
debug => $paref->{debug}
}
);
hod => $hod
};
if (defined $pvaifc) {
return ($msg, $pvaifc);
## 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;
}
or do { Log3 ($name, 1, "$name - aiGetResult ERROR: $@");
return $@;
};
debugLog ($paref, 'aiData', "got AI result from Tree number $tn: $res") if(defined $res);
}
return 'No AI decition delivered';
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,
# wcc => $cbin,
# rr1c => $rr1c,
# sunalt => $sabin,
# sunaz => $sunaz,
# hod => $hod,
# dtree => $dtree,
# debug => $paref->{debug}
# }
# );
# push @total_prediction, $res if($msg eq 'spreaded');
# }
# my $sprnum = scalar @total_prediction;
# 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);
# }
return 'No AI decision delivered';
}
################################################################
@ -17363,9 +17498,6 @@ sub _aiGetSpread {
my ($pos, $neg, $p, $n);
debugLog ($paref, 'aiData', qq{AI no accurate result found with initial value "Rad1h: $rad1h" (hod: $hod)});
debugLog ($paref, 'aiData', qq{AI test Rad1h variance "$dtn" and positive/negative spread with step size "$step"});
my $gra = {
temp => $temp,
wcc => $wcc,
@ -17382,11 +17514,10 @@ sub _aiGetSpread {
debugLog ($paref, 'aiData', qq{AI positive test value "Rad1h: $p"});
eval { $pos = $dtree->get_result (attributes => $gra);
};
if ($@) {
return $@;
1;
}
or do { return $@;
};
if ($pos) {
debugLog ($paref, 'aiData', qq{AI positive tolerance hit: $pos Wh});
@ -17402,11 +17533,10 @@ sub _aiGetSpread {
debugLog ($paref, 'aiData', qq{AI negative test value "Rad1h: $n"});
eval { $neg = $dtree->get_result (attributes => $gra);
};
if ($@) {
return $@;
1;
}
or do { return $@;
};
if ($neg) {
debugLog ($paref, 'aiData', qq{AI negative tolerance hit: $neg Wh});
@ -17414,14 +17544,13 @@ sub _aiGetSpread {
}
}
my $pvaifc = $pos && $neg ? sprintf "%.0f", (($pos + $neg) / 2) : undef;
my $result = $pos && $neg ? sprintf "%.0f", (($pos + $neg) / 2) : undef;
if (defined $pvaifc) {
debugLog ($paref, 'aiData', qq{AI determined average result: pvaifc: $pvaifc Wh (hod: $hod, sunaz: $sunaz, sunalt: $sunalt, wcc: $wcc, rr1c: $rr1c, temp: $temp)});
return ('spreaded', $pvaifc);
if (defined $result) {
return ('spreaded', $result);
}
return 'No AI decition delivered';
return 'No AI decision delivered';
}
################################################################
@ -17430,8 +17559,6 @@ return 'No AI decition delivered';
sub aiInit { ## no critic "not used"
my $paref = shift;
my $name = $paref->{name};
my $type = $paref->{type};
my $hash = $defs{$name};
if (!isPrepared4AI ($hash)) {
@ -17444,17 +17571,15 @@ sub aiInit { ## no critic "not used"
debugLog ($paref, 'aiProcess', $err);
$data{$name}{current}{aiinitstate} = $err;
return $err;
return ($err);
}
my $dtree = new AI::DecisionTree ( verbose => 0, noise_mode => 'pick_best' );
$data{$name}{aidectree}{object} = $dtree;
$data{$name}{current}{aiinitstate} = 'ok';
Log3 ($name, 3, "$name - AI::DecisionTree initialized");
return;
return ('', $dtree);
}
################################################################
@ -21536,6 +21661,7 @@ return;
# dayname - Tagesname (Kürzel)
# csmt${c} - Totalconsumption Consumer $c (1..MAXCONSUMER)
# csme${c} - Consumption Consumer $c (1..MAXCONSUMER) in $hod
# minutescsm${c} - Laufzeit des Consumers in Minuten in $hod
# sunaz - Azimuth der Sonne (in Dezimalgrad)
# sunalt - Höhe der Sonne (in Dezimalgrad)
# $def: Defaultwert
@ -23797,6 +23923,7 @@ to ensure that the system configuration is correct.
<tr><td> <b>SunMinutes_Remain</b> </td><td>the remaining minutes until sunset of the current day </td></tr>
<tr><td> <b>SunHours_Remain</b> </td><td>the remaining hours until sunset of the current day </td></tr>
<tr><td> <b>todayConsumption</b> </td><td>the energy consumption of the house on the current day </td></tr>
<tr><td> <b>todayConsumptionForecastDay</b></td><td>Consumption forecast for the current day </td></tr>
<tr><td> <b>todayConsumptionForecast</b> </td><td>Consumption forecast per hour of the current day (01-24) </td></tr>
<tr><td> <b>todayConForecastTillSunset</b> </td><td>Consumption forecast from current hour to hour before sunset </td></tr>
<tr><td> <b>todayDoneAPIcalls</b> </td><td>the number of radiation data API calls executed on the current day </td></tr>
@ -26301,6 +26428,7 @@ die ordnungsgemäße Anlagenkonfiguration geprüft werden.
<tr><td> <b>SunMinutes_Remain</b> </td><td>die verbleibenden Minuten bis Sonnenuntergang des aktuellen Tages </td></tr>
<tr><td> <b>SunHours_Remain</b> </td><td>die verbleibenden Stunden bis Sonnenuntergang des aktuellen Tages </td></tr>
<tr><td> <b>todayConsumption</b> </td><td>der Energieverbrauch des Hauses am aktuellen Tag </td></tr>
<tr><td> <b>todayConsumptionForecastDay</b></td><td>Verbrauchsprognose für den aktuellen Tag </td></tr>
<tr><td> <b>todayConsumptionForecast</b> </td><td>Verbrauchsprognose pro Stunde des aktuellen Tages (01-24) </td></tr>
<tr><td> <b>todayConForecastTillSunset</b> </td><td>Verbrauchsprognose von aktueller Stunde bis Stunde vor Sonnenuntergang </td></tr>
<tr><td> <b>todayDoneAPIcalls</b> </td><td>die Anzahl der am aktuellen Tag ausgeführten Strahlungsdaten-API Calls </td></tr>