Die Erfassung der jeweiligen Parameter erfolgt in FHEM. Die eigentliche Herausforderung war es, die Parameter außerhalb des FHEM Eco-Systems zu speichern, um sie für die PV-Prognose in einer flachen Struktur verfügbar zu haben.
Lesen Sie hier die Details dazu.
Die Parameter, die für die PV-Ertragsprognose verwendet werden, sind entweder schon langjährig in FHEM als history vorhanden oder wurden über neue Devices gesammelt. Ziel ist es dabei, die Parameter letztendlich in einem stündlichen Intervall in einer separaten Tabelle (flache Struktur) zu speichern. Wie bereits erwähnt, hat mein Betrachtungstag 20 Stunden. Somit kommen je Tag 20 neue Datensätze hinzu, was über ein Jahr ca. 7000 neue Sätze ergibt.
Im Folgenden sind die wesentlichen Auszüge der FHEM-Devices gelistet.
Der PV-Ertrag wird von den Wechselrichtern mit SBFSpot gelesen und per MQTT-Protokoll an FHEM in ein MQTT-Device übertragen. Hier beispielhaft der relevante Wert eines Wechselrichters.
Internals:
DEF SB5000
IODev mq_Server
NAME KG_ws_WR_2
TYPE MQTT2_DEVICE
READINGS:
2024-10-21 18:15:13 EToday 17.882
Attributes:
readingList inverter:2100522126:.* { json2nameValue($EVENT, '', $JSONMAP) }
Da ich zwei Wechselrichter im Einsatz habe, werden die Werte der beiden Wechselrichter in einem Dummy-Device addiert. Innerhalb des Dummies wird der Ertrag stündlich betrachtet. Hier der wesentliche Ausschnitt:
Internals:
NAME du_KG_ws_WR_Ges
TYPE dummy
READINGS:
2024-10-21 20:02:00 EHour 0.000
2024-10-21 20:02:00 EHourLast 25.285
2024-10-21 20:02:00 EToday 25.285
Attributes:
event-on-change-reading EToday
userReadings
EHour {round(ReadingsVal("du_KG_ws_WR_Ges","EToday",0) - ReadingsVal("du_KG_ws_WR_Ges","EHourLast",0),3)},
EToday {ReadingsVal("KG_ws_WR_1","EToday",0) + ReadingsVal("KG_ws_WR_2","EToday",0)}
Die Helligkeit und Temperatur liefert ein Multisensor von Eltako. Hier ist es wichtig, mit Mittelwerten und nicht mit Momentanwerten zu agieren. Wenn z.B. von 11:00 Uhr bis 11:55 Uhr die Sonne scheint und sich um 12:00 Uhr eine größere Wolke vor die Sonne schiebt, dann ist es wenig sinnvoll, den Wert von 12:00 Uhr 1:1 zu übernehmen. Das würde die gesamte Stunde verfälschen. Hier ist der Mittelwert über die Stunde sinnvoller. So können punktuelle Ausreißer vermieden werden. Der Mittelwert für jeweils eine Stunde wurde mit der FHEM Subroutine movingAverage berechnet.
Internals:
NAME EG_au_MS_1
TYPE EnOcean
READINGS:
2024-10-21 20:35:25 brightness 0
2024-10-21 20:35:25 brightnessAV60m 0
2024-10-21 20:35:25 temperature 12.7
2024-10-21 20:35:25 temperatureAV60m 13.6
Attributes:
eep A5-13-01
subType environmentApp
userReadings
temperatureAV60m {round(myUtils_movingAverageT("EG_au_MS_1", "temperature", 3600),1)},
brightnessAV60m {round(myUtils_movingAverageT("EG_au_MS_1", "brightness", 3600),0)}
Die Sonnenwinkel (SunAlt, SunAz) werden mit dem FHEM AstroModul zu Beginn des Tages einmalig ermittelt und in ein Dummy-Device geschrieben. Angestoßen wir die Berechnung mit einem AT-Befehl. Es wird dabei der Sonnenstand für den aktuellen Tag (fc0_...) und den Folgetag (fc1_...) unter Berücksichtigung des Azimut der Anlage berechnet. Die Routine, die das Dummy füllt, finden Sie am Ende dieser Seite.
Internals:
NAME du_SunPos
TYPE dummy
READINGS:
2024-03-05 19:16:30 azimut -19.5
2024-10-21 04:02:00 fc0_10_SunAlt 14.5
2024-10-21 04:02:00 fc0_10_SunAz 146
2024-10-21 04:02:00 fc0_11_SunAlt 21.1
2024-10-21 04:02:00 fc0_11_SunAz 159.6
:
:
2024-10-21 04:02:00 fc0_Date 2024-10-21
2024-10-21 04:02:01 fc1_10_SunAlt 14.2
2024-10-21 04:02:01 fc1_10_SunAz 146.3
2024-10-21 04:02:01 fc1_11_SunAlt 20.8
2024-10-21 04:02:01 fc1_11_SunAz 159.8
:
:
2024-10-21 04:02:01 fc1_Date 2024-10-22
Die Wetterdaten werden mit dem FHEM-Modul DWD_OpenData ebenfalls in ein FHEM-Device übernommen. Hier die wesentlichen Readings und Attribute:
NAME WetterFc
STATE forecast updated
TYPE DWD_OpenData
READINGS:
2024-10-21 12:00:07 fc0_10_Neff 75
2024-10-21 12:00:07 fc0_10_RRad1 24.00
2024-10-21 12:00:07 fc0_10_Rad1h 260.00
2024-10-21 12:00:07 fc0_10_SunD1 480.00
2024-10-21 12:00:07 fc0_10_TTT 12.1
:
:
2024-10-21 21:00:06 fc1_10_Neff 80
2024-10-21 21:00:06 fc1_10_RRad1 25.00
2024-10-21 21:00:06 fc1_10_Rad1h 270.00
2024-10-21 21:00:06 fc1_10_SunD1 480.00
2024-10-21 21:00:06 fc1_10_TTT 13
:
:
Attributes:
forecastDays 1
forecastProperties Neff, Rad1h, RRad1, SunD1, TTT
forecastPruning 1
forecastResolution 1
forecastStation 10763
Hinzu kommt noch das Attribute CloudCover vom FHEM-Modul OpenWeather:
API OpenWeatherMapAPI
DEF API=OpenWeatherMapAPI,cachemaxage:900,version:3.0 apikey=key interval=900
MODEL OpenWeatherMapAPI
NAME WetterFc2
TYPE Weather
READINGS:
2024-12-09 18:57:33 hfc1_CloudCover 100
2024-12-09 18:57:33 hfc2_CloudCover 85
:
:
Attributes:
forecast hourly
forecastLimit 44
FHEM kennt nur die beiden Tabellen current und history.
Da über die Jahre erhebliche Datenmengen entstehen werden, erschien es als sinnvoll, die Daten in einer separaten Tabelle zu speichern.
Somit sind die Daten von dem „Alltagsgeschäft“ in FHEM getrennt. Auf eine flache Struktur lässt sich in diesen Anwendungsfall auch leichter zugreifen.
Hier der Aufbau der separaten Tabelle, in die stündlich die Daten hineingeschrieben werden (Maria-DB).
CREATE TABLE IF NOT EXISTS `SolarEnergyFc` (
`TimeStamp` datetime NOT NULL,
`Year` int NOT NULL,
`Month` int NOT NULL,
`Day` int NOT NULL,
`Hour` int NOT NULL,
`TTT` float NULL DEFAULT 0 COMMENT "Temperature 2m above surface - source: DWD",
`Temp` float NULL DEFAULT null COMMENT "Temperature - source: local sensor",
`RRad1` int NULL DEFAULT 0 COMMENT "Global irradiance within the last hour % (0..80) - source: DWD",
`Rad1h` int NULL DEFAULT 0 COMMENT "Global irradiance kJ/m2 - source: DWD",
`SunAlt` float NULL DEFAULT 0 COMMENT "Sun altitude - source: FHEM module Astro",
`SunAz` float NULL DEFAULT 0 COMMENT "Sun azimuth - source: FHEM module Astro",
`SunD1` int NULL DEFAULT 0 COMMENT "Sunshine duration during the last Hour s - source: DWD",
`Neff` int NULL DEFAULT NULL COMMENT "Effective cloud cover % (0..100) - source: DWD",
`CloudCover` int NULL DEFAULT NULL COMMENT "Effective cloud cover % (0..100) - source: OpenWeather",
`Brightness` int NULL DEFAULT 0 COMMENT "Brightness lux - source: local sensor",
`BrightnessFc` int NULL DEFAULT 0 COMMENT "Brightness forecast lux - source: prediction",
`EnergyHour` float NULL DEFAULT 0 COMMENT "Total energy/hour kWh - source: local converter",
`EnergyHourFc` float NULL DEFAULT 0 COMMENT "Total energy/hour forecast kWh - source: prediction",
PRIMARY KEY (Year,Month,Day,Hour),
INDEX (Year,Month,Day,Hour)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
Um das FHEM Eco-System zu verlassen werden die Parameter in FHEM gesammelt und über eine stored Procedure in die separate Datenbanktabelle geschrieben. Die Schrittfolge kann wie folgt zusammengefasst werden:
In Summe erfolgt dieser Aufruf gemeinsam für zwei Perl-Subroutinen:
Hier die Perl-Subroutinen, um die Istwerte der aktuellen Stunde an das Shell-Script zu übergeben:
##############################################################################################################################
# save current values provided by DWD and locally in order to fill the AI model (runs from 04:02 to 23:02 hourly)
##############################################################################################################################
sub mySolarForecastUtils_saveCurValuesSolarFc(){
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
my $para = ReadingsVal("WetterFC","fc0_".$hour."_TTT",0).",";
$para .= ReadingsVal("EG_au_MS_1","temperatureAV60m",0).",";
$para .= ReadingsVal("WetterFc","fc0_".$hour."_RRad1",0).",";
$para .= ReadingsVal("WetterFc","fc0_".$hour."_Rad1h",0).",";
$para .= ReadingsVal("du_SunPos","fc0_".$hour."_SunAlt",0).",";
$para .= ReadingsVal("du_SunPos","fc0_".$hour."_SunAz",0).",";
$para .= ReadingsVal("WetterFc","fc0_".$hour."_SunD1",0).",";
$para .= ReadingsVal("WetterFc","fc0_".$hour."_Neff",0).",";
$para .= ReadingsVal("WetterFc2","hfc".mySolarForecastUtils_getHourIndex($hour)."_CloudCover",0).",";
$para .= ReadingsVal("EG_au_MS_1","brightnessAV60m",0).",";
$para .= ReadingsVal("du_KG_ws_WR_Ges","EHour",0);
system("/opt/fhem/saveCurValuesSolarFc.sh $para >> /opt/fhem/log/saveCurValuesSolarFc.log");
fhem("setreading du_KG_ws_WR_Ges EHourLast ".ReadingsVal("du_KG_ws_WR_Ges","EToday",0));
}
Das Script saveCurValuesSolarFc.sh enthält den Aufruf der stored Procedure:
#!/bin/bash
echo "-----------------------------------------------------------"
echo "Begin: "$(date +"%Y-%m-%d %H:%M:%S")
mysql -u <<user>> -<<passwort>> -D fhem --execute="call saveCurValuesSolarFc($@)"
echo "End: "$(date +"%Y-%m-%d %H:%M:%S")
echo "-----------------------------------------------------------"
Die Vorhersagedaten werden wie folgt zusammengestellt und übergeben:
##############################################################################################################################
# save forecast values provided by DWD in order to fill the AI model for today and tomorrow (runs from 04:02 to 23:02 hourly)
##############################################################################################################################
sub mySolarForecastUtils_saveNextValuesSolarFc(){
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
$year = $year+1900;
++$mon;
my $para = "";
my $curHour = mySolarForecastUtils_getHourIndex($hour)+1;
for(my $i=4; $i<24; $i++) { # today
if ($i > $hour) {
$para = $year.",";
$para .= $mon.",";
$para .= $mday.",";
$para .= $i.",";
$para .= ReadingsVal("WetterFc","fc0_".$i."_TTT",0).",";
$para .= ReadingsVal("WetterFc","fc0_".$i."_RRad1",0).",";
$para .= ReadingsVal("WetterFc","fc0_".$i."_Rad1h",0).",";
$para .= ReadingsVal("du_SunPos","fc0_".$i."_SunAlt",0).",";
$para .= ReadingsVal("du_SunPos","fc0_".$i."_SunAz",0).",";
$para .= ReadingsVal("WetterFc","fc0_".$i."_SunD1",0).",";
$para .= ReadingsVal("WetterFc","fc0_".$i."_Neff",0).",";
$para .= ReadingsVal("WetterFc2","hfc".$curHour."_CloudCover",0);
system("/opt/fhem/saveNextValuesSolarFc.sh $para >> /opt/fhem/log/saveNextValuesSolarFc.log");
$curHour++;
}
}
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time + 86400);
$year = $year+1900;
++$mon;
$curHour = $curHour + 4;
for(my $i=4; $i<24; $i++) { # tomorrow
$para = $year.",";
$para .= $mon.",";
$para .= $mday.",";
$para .= $i.",";
$para .= ReadingsVal("WetterFc","fc1_".$i."_TTT",0).",";
$para .= ReadingsVal("WetterFc","fc1_".$i."_RRad1",0).",";
$para .= ReadingsVal("WetterFc","fc1_".$i."_Rad1h",0).",";
$para .= ReadingsVal("du_SunPos","fc1_".$i."_SunAlt",0).",";
$para .= ReadingsVal("du_SunPos","fc1_".$i."_SunAz",0).",";
$para .= ReadingsVal("WetterFc","fc1_".$i."_SunD1",0).",";
$para .= ReadingsVal("WetterFc","fc1_".$i."_Neff",0).",";
$para .= ReadingsVal("WetterFc2","hfc".$curHour."_CloudCover",0);
system("/opt/fhem/saveNextValuesSolarFc.sh $para >> /opt/fhem/log/saveNextValuesSolarFc.log");
$curHour++;
}
}
Das Script saveNextValuesSolarFc.sh hat folgenden Inhalt:
#!/bin/bash
echo "-----------------------------------------------------------"
echo "Begin: "$(date +"%Y-%m-%d %H:%M:%S")
mysql -u <<user>> -<<passwort>> -D fhem --execute="call saveNextValuesSolarFc($@)"
echo "End: "$(date +"%Y-%m-%d %H:%M:%S")
echo "-----------------------------------------------------------"
Die Subroutine mySolarForecastUtils_getHourIndex($) ist notwendig, da die Readings bei DWD und OpenWeather unterschiedliche Sichtweisen haben. Bei DWD beschreibt z.B. ein Reading "fc1_11_SunD1" den Wert für 11 Uhr, bei OpenWeather würde die 11 bedeuten: in 11 Stunden, d.h. bei DWD sind die Angaben absolut und bei OpenWeather relativ. Diesen Unterschied gleicht die folgende Subroutine aus.
##############################################################################################################################
# get the index for the given hour
# para: hour
##############################################################################################################################
sub mySolarForecastUtils_getHourIndex($){
my ($hour) = @_;
for(my $i=1; $i<=44; $i++) {
my @parts = split /[ :]/, ReadingsVal("WetterFc2","hfc".$i."_day_of_week","");
if ($parts[1] == sprintf("%02d",$hour)) {
return $i;
}
}
return 0;
}
Die wesentlichen Sequenzen des AT-Befehl, Dummy und Notify womit der Schreibvorgang ausgelöst wird, sind hier zusammengefasst:
######################## AT-Befehl ##################################
NAME at_WetterFc
DEF +*01:00:00 set du_WetterFc Ausführen
COMMAND set du_WetterFc Ausführen
NTM 13:02:00
PERIODIC yes
RELATIVE yes
TYPE at
READINGS:
2024-12-28 12:02:05 state Next: 13:02:00
Attributes:
DbLogExclude .*
alignTime 00:02
disabledForIntervals 23:30-24:00 00:00-03:30
######################## Dummy ######################################
NAME du_WetterFc
STATE Ausführen
TYPE dummy
READINGS:
2024-12-28 12:02:00 state Ausführen
######################## Notify #####################################
NAME ny_WetterFc
DEF du_WetterFc:Ausführen.* {
if ($hour == 4) {
mySolarForecastUtils_calcSunPosition();
}
mySolarForecastUtils_saveCurValuesSolarFc();
mySolarForecastUtils_saveNextValuesSolarFc();
}
Um sich aus den einzelnen Ausschnitten ein besseres Gesamtbild ableiten zu können, sind hier die beiden wesentlichen Scripte nochmal am Stück:
Fragen oder Anregungen nehme ich gern über die Kontaktbox entgegen oder direkt per Email.
kontakt@kaempf-nk.de