
Zitat von: dvdjimmy am 04 Januar 2026, 17:01:32Bzgl. Abtauvorgang kann man vermutlich nicht viel einstellen, oder?Nö, wie auch? Du müsstest "irgendwie" das Vereisen verhindern, z.B. die angesaugte Luft trocknen ...
Zitat von: DS_Starter am 04 Januar 2026, 14:56:14Ein interessanter Artikel zum Thema WP und Energievorhersage und ziemlich vollständig das beschreibt was auch im SF umgesetzt ist:Interessant an den Ergebnissen ist, dass die Vorhersagen bei nicht monotonem Verhalten stark fehlerbehaftet waren. Und dabei hat man sich ausschließlich auf eine Korrelation zwischen Energieverbrauch, Außentemperatur und die zeitliche Abhängigkeit konzentriert.
https://medium.com/@omegaxp3/predicting-future-energy-consumption-using-neural-network-based-on-historical-data-and-temperature-819028398301
sub myUtils_heatingEnergyDemand {
my ($hash, $name, $consumerNum, $timeOffset) = @_;
...
return ($energy, $setpointTemp);
}
Aktuell berechne ich die Energie / h mit nachfolgender Routine und schreibe den aktuellen Wert und die Prognosen in zusätzliche SolarForecast Readings. Die Funktion myUtils_heatingEnergyDemand wird in der ctrlUserExitFn zusammen mit anderen Hilfsfunktionen aufgerufen und stündlich ausgeführt:sub myUtils_ctrlUserExitFn {
my ($hash, $name, $wrName, $hpName) = @_;
myUtils_buildGridTariffArray($hash, $name);
myUtils_buildSpotTariffHash($hash, $name);
myUtils_consumerEnergyLastHour($hash, $name,'01');
my ($sec, $min, $hour) = localtime;
# Berechne den aktuellen 15-Minuten-Intervall-Start (z.B. 08:15, 08:30, ...)
my $intervalMin = int($min / 15) * 15;
my $intervalTag = sprintf("%02d:%02d", $hour, $intervalMin);
# Ausfuehrung jeweils einmal zu Beginn eines 15 min Intervalls
if (!exists($hash->{helper}{ctrlUserExitFn}{lastInterval}) || $hash->{helper}{ctrlUserExitFn}{lastInterval} ne $intervalTag) {
$hash->{helper}{ctrlUserExitFn}{lastInterval} = $intervalTag;
readingsBeginUpdate($hash);
readingsBulkUpdateIfChanged($hash, 'gridTariff', myUtils_getCurrentGridTariff($hash, $name, sprintf("%02d:%02d", $hour, $min)), 1);
readingsBulkUpdateIfChanged($hash, 'gridEnergyFixedPrice', myUtils_getCurrentTotalEnergyCosts($hash, $name, sprintf("%02d:%02d", $hour, $min)), 1);
readingsBulkUpdateIfChanged($hash, 'gridEnergySpotPrice', myUtils_GetCurrentSpotEnergyCosts($hash, $name, sprintf("%02d:%02d", $hour, $min)), 1);
readingsEndUpdate($hash, 1);
myUtils_pvBatChargeRequest($hash, $name, $wrName);
myUtils_heatingEnergyDemand($hash, $name, '01');
myUtils_HPHcCharge($hash, $name, $hpName, $intervalTag);
myUtils_HPHwcCharge($hash, $name, $hpName, $intervalTag);
}
return;
}
Die Funktion myUtils_heatingEnergyDemand holt sich u. a. die Sollinnentemperatur unmittelbar aus der Wärmepumpe. Die Windgeschwindigkeit aus dem Modul DWD_OpenData.# Schaetzung des Energiebedarfs faer Heizung abhaengig vom Verlustkoeffizient aus Heizlastberechnung (DIN EN 12831) und der Aussentemperatur
sub myUtils_heatingEnergyDemand {
my ($hash, $name, $consumerNum) = @_;
# Initialisiere Helper-Struktur bei Bedarf
$hash->{helper}{ctrlUserExitFn} //= {};
# Zeitabfrage
my ($sec, $min, $hour, $mday, $mon, $year) = localtime;
my $nowEpoch = time;
my $lastRunHour = $hash->{helper}{ctrlUserExitFn}{lastRunHour} // -1;
return undef if ($lastRunHour == $hour);
# Konstanten
my $copMax = 5;
my $copMin = 2.5;
my $correctionFactor = 0.8;
my $energyDiffMax = 0.3; # max. dynamischer Energieaufschlag
my $energyHwcHour = 0; # Energiebedarf / h fuer Warmwasser
my $heatCapacity = 16; # Waermekapazität des Gebaeudes C [Wh/K]
my $heatingOutdoorDesignTemp = -8.5; # Auslegungstemperatur
my $heatingLimitTemp = ReadingsVal('Heizung_ctlv3', 'ctlv3_Hc1SummerTempLimit', 16);
my $helperKey = 'consumer' . $consumerNum;
my $lossCoeff = 300; # Verlustkoeffizient H [W/K] aus Heizlastberechnung (DIN EN 12831)
my $setpointSlotTemp = 20;
my $setpointBackTemp = ReadingsVal('Heizung_ctlv3', 'ctlv3_z1SetBackTemp', 10);
my $quickVetoTemp = ReadingsVal('Heizung_ctlv3', 'ctlv3_z1QuickVetoTemp', 20);
my $periodSlotTemp = "04:30-21:00";
my $z1Status = ReadingsVal('Heizung_ctlv3', 'ctlv3_z1Status', 'auto');
# Endzeitpunkte parsen
my ($holidayEndDay, $holidayEndMonth, $holidayEndYear) = split(/\./, ReadingsVal('Heizung_ctlv3', 'ctlv3_z1HolidayEndDate', '01.01.2000'));
my ($holidayEndHour, $holidayEndMin, $holidayEndSec) = split(/:/, ReadingsVal('Heizung_ctlv3', 'ctlv3_z1HolidayEndTime', '00:00:00'));
my ($quickVetoEndDay, $quickVetoEndMonth, $quickVetoEndYear) = split(/\./, ReadingsVal('Heizung_ctlv3', 'ctlv3_z1QuickVetoEndDate', '01.01.2000'));
my ($quickVetoEndHour, $quickVetoEndMin, $quickVetoEndSec) = split(/:/, ReadingsVal('Heizung_ctlv3', 'ctlv3_z1QuickVetoEndTime', '00:00:00'));
my $holidayEpoch = timelocal($holidayEndSec, $holidayEndMin, $holidayEndHour, $holidayEndDay, $holidayEndMonth - 1, $holidayEndYear);
my $quickVetoEpoch = timelocal($quickVetoEndSec, $quickVetoEndMin, $quickVetoEndHour, $quickVetoEndDay, $quickVetoEndMonth - 1, $quickVetoEndYear);
# Slot-Zeitraum parsen
my ($startSlot, $endSlot) = split /-/, $periodSlotTemp;
my ($startHour, $startMin) = split /:/, $startSlot;
my ($endHour, $endMin) = split /:/, $endSlot;
# Hilfsfunktion: Slot-Temperatur
sub getSetpointTempForHour {
my ($h, $startHour, $startMin, $endHour, $endMin, $slotTemp, $backTemp) = @_;
return $slotTemp if (
($h > $startHour && $h < $endHour) or
($h == $startHour && $startMin == 0) or
($h == $endHour && $endMin > 0)
);
return $backTemp;
}
# Hilfsfunktion: Sonderstatus prüfen
sub overrideSetpointIfActive {
my ($status, $epochNow, $holidayEpoch, $quickVetoEpoch, $backTemp, $vetoTemp) = @_;
return $backTemp if ($status eq 'holiday_away' && $epochNow < $holidayEpoch);
return $vetoTemp if ($status eq 'quick_veto' && $epochNow < $quickVetoEpoch);
return undef;
}
# Energiebedarf / h (statisch / dynamisch)
sub heatingCalcDynamic {
my ($lossCoeff, $copMax, $copMin, $correctionFactor, $heatCapacity, $energyDiffMax, $energyHwcHour, $setpointTemp, $setpointTempLast, $tempOutside, $heatingOutdoorDesignTemp, $heatingLimitTemp, $wind) = @_;
return (0, 0, 0) if ($tempOutside >= $setpointTemp || $tempOutside >= $heatingLimitTemp);
# COP Wert in Abhaengigkeit der Aussentemperatut (lineare Interpolation)
my $cop = $copMin + ($copMax - $copMin) * (($tempOutside - $heatingOutdoorDesignTemp) / ($heatingLimitTemp - $heatingOutdoorDesignTemp));
# Falls unterhalb der Auslegungstemperatur, COP bleibt minimal
$cop = $copMin if ($tempOutside <= $heatingOutdoorDesignTemp);
# Falls oberhalb der maximalen Temperatur, COP bleibt maximal
$cop = $copMax if ($tempOutside >= $heatingLimitTemp);
# Basislast: Verlustkoeffizient * Temperaturdifferenz
my $energyBase = $lossCoeff / $cop * $correctionFactor * ($setpointTemp - $tempOutside);
# Windkorrekturfaktor (Annahme: ab 20 km/h Wind steigt Infiltration linear um bis zu +20 %)
my $windFactor = 1.0;
if (defined $wind && $wind > 20) {
# Skaliere zwischen 20 km/h und 60 km/h
my $windGradient = ($wind - 20) / 40; # 0 bis 1
$windGradient = 1 if $windGradient > 1; # Deckelung
$windFactor += 0.2 * $windGradient; # max. +20 %
}
$energyBase *= $windFactor;
# Änderung der Solltemperatur pro Stunde
my $deltaTsoll = $setpointTemp - $setpointTempLast;
# Dynamische Zusatzlast: Gebaeudekapazität * Temperaturänderung / 1h
# (1h = 3600s, hier vereinfachend pro Stunde gerechnet)
my $energyDyn = $heatCapacity * $deltaTsoll;
# Begrenzung der Zusatzlast auf Prozentsatz der Basislast
my $energyDynMax = $energyBase * $energyDiffMax;
$energyDyn = $energyDynMax if $energyDyn > $energyDynMax;
$energyDyn = 0 if $energyDyn < 0; # keine negative Zusatzlast
# Gesamtenergie inkl Energie fuer Warmwasser / h
$energyBase += $energyHwcHour;
my $energyTotal = $energyBase + $energyDyn;
return ($energyBase, $energyDyn, $energyTotal, $cop);
}
sub toDayHour {
my ($value) = @_;
# Ganze Tage = Division durch 24
my $day = int($value / 24);
# Reststunden = Modulo 24
my $hour = $value % 24;
return ($day, $hour);
}
readingsBeginUpdate($hash);
# Aktuelle Stunde
my $setpointTemp = overrideSetpointIfActive($z1Status, $nowEpoch, $holidayEpoch, $quickVetoEpoch, $setpointBackTemp, $quickVetoTemp);
$setpointTemp //= getSetpointTempForHour($hour, $startHour, $startMin, $endHour, $endMin, $setpointSlotTemp, $setpointBackTemp);
my $setpointTempLast = $hash->{helper}{$helperKey}{setpointTemp}{current} // $setpointTemp;
my $tempHour = FHEM::SolarForecast::CurrentVal($name, 'temp', $heatingLimitTemp);
my ($energyBase, $energyDyn, $energyTotal, $cop) = heatingCalcDynamic ($lossCoeff, $copMax, $copMin, $correctionFactor, $heatCapacity, $energyDiffMax,
$energyHwcHour,
$setpointTemp, $setpointTempLast,
$tempHour, $heatingOutdoorDesignTemp, $heatingLimitTemp,
ReadingsVal("Wetter_DWD", "fc0_" . $hour . "_FF", 0));
$hash->{helper}{$helperKey}{setpointTemp}{current} = $setpointTemp;
readingsBulkUpdateIfChanged($hash, 'consumer' . $consumerNum . '_consumptionCalc_Current', int($energyBase) . " Wh", 1);
readingsBulkUpdateIfChanged($hash, 'consumer' . $consumerNum . '_consumptionCalcDyn_Current', int($energyTotal) . " Wh", 1);
readingsBulkUpdateIfChanged($hash, 'consumer' . $consumerNum . '_copCalc_Current', sprintf('%0.1f', $cop), 1);
# Folge-Stunden
for my $indexHour (0 .. 23) {
my $futureEpoch = $nowEpoch + ($indexHour + 1) * 3600;
my ($futureDay, $futureHour) = toDayHour ($hour + $indexHour + 1);
$setpointTemp = overrideSetpointIfActive($z1Status, $futureEpoch, $holidayEpoch, $quickVetoEpoch, $setpointBackTemp, $quickVetoTemp);
$setpointTemp //= getSetpointTempForHour($futureHour, $startHour, $startMin, $endHour, $endMin, $setpointSlotTemp, $setpointBackTemp);
$hash->{helper}{$helperKey}{setpointTemp}{$indexHour} = $setpointTemp;
$setpointTempLast = $indexHour == 0 ? $hash->{helper}{$helperKey}{setpointTemp}{current} // $setpointTemp : $hash->{helper}{$helperKey}{setpointTemp}{$indexHour - 1} // $setpointTemp;
$tempHour = FHEM::SolarForecast::NexthoursVal($name, 'NextHour' . sprintf('%02d', $indexHour), 'temp', $heatingLimitTemp);
($energyBase, $energyDyn, $energyTotal, $cop) = heatingCalcDynamic ($lossCoeff, $copMax, $copMin, $correctionFactor, $heatCapacity, $energyDiffMax,
$energyHwcHour,
$setpointTemp, $setpointTempLast,
$tempHour, $heatingOutdoorDesignTemp, $heatingLimitTemp,
ReadingsVal("Wetter_DWD", "fc" . $futureDay . "_" . $futureHour . "_FF", 0));
readingsBulkUpdateIfChanged($hash, 'consumer' . $consumerNum . '_consumptionCalc_NextHour' . sprintf('%02d', $indexHour), int($energyBase) . " Wh", 1);
readingsBulkUpdateIfChanged($hash, 'consumer' . $consumerNum . '_consumptionCalcDyn_NextHour' . sprintf('%02d', $indexHour), int($energyTotal) . " Wh", 1);
}
readingsEndUpdate($hash, 1);
# Merke letzte Ausführungsstunde
$hash->{helper}{ctrlUserExitFn}{lastRunHour} = $hour;
return undef;
}
attr >Forecast< ctrlDebug aiProcess
!!hinzufügen !!attr <Forecast> aiConActivate=1
set Forecast aiDecTree runConTrain.....und das Logbuch beobachten....

set-executionpolicy remotesigned einmalig als administrator in der powershell ausführen.Add-Type -AssemblyName System.Speech
$synth = New-Object -TypeName System.Speech.Synthesis.SpeechSynthesizer
$synth.Speak($args[0])als tts.ps1 speicherndefmod say cmdalias say .* AS {system('cmd', '/c','start','/B','""','c:\Program Files\PowerShell\7\pwsh.exe','-ExecutionPolicy','Bypass','c:\fhem\tts.ps1',$EVENT);;;;return''}say mein text ist sehr einfallsreichzur sprachausgabe bewegen.