Eigenverbrauchsoptimierung mit Open Source

  Philipp Seitzinger   Lesezeit: 20 Minuten  🗪 1 Kommentar

Mit Hilfe eines Raspbarry Pi und einigen Scripten lässt sich der Eigenverbrauch auf den persönlichen Bedarf hin optimieren.

eigenverbrauchsoptimierung mit open source

Hinweis: Dieser Artikel wurde im Rahmen unseres Weihnachtsgewinnspiels eingereicht.

Seit August bin ich stolzer Besitzer einer grösseren Solaranlage. Es ist bereits die 2. Anlage. Letztes Jahr montierte ich selber eine Plug&Play-Anlage, welche maximal 600 Watt liefert. Die neue Anlage liefert etwas über 7 kW.
Leider gibt es bei uns für den ins Netz eingespiesenen Strom nur sehr wenig Geld. Deshalb war es mein Ziel, den Eigenverbrauch zu optimieren. Der Wärmepumpenboiler soll eingeschaltet werden, sobald genug Strom vom Dach kommt.


Dazu habe ich verschiedene Scripte in Bash und PHP erstellt. Alles läuft auf einem Raspberry Pi 3. Für die Schaltung des Boilers verwende ich ein RPi Relay Board von Waveshare. Vom Anbieter gibt es verschiedene Programme zur Schaltung des Relay Board. Ich verwende die Python-Scripte. Der Boiler hätte direkt mit dem Wechselrichter verbunden werden können. Aus Kostengründen habe ich darauf verzichtet. Der Boiler hat eine Timer-Funktion. Auf der Steuerplatine gibt es 2 Kontakte für die Timer-Funktion. Sind die Kontakte kurz geschlossen, ist die Heizung deaktiviert. Bei offenem Kontakt kann die Heizung starten, wobei die Steuerung des Boilers entscheidet, wann geheizt wird.

Es gibt eine Webseite mit der Anzeige der Leistung und Status der Boilerschaltung.

Die Werte sind in einer SQLite-Datenbank gespeichert. Pro Tag gibt es eine SQLite-DB-Datei. Im Webinterface kann ich das Datum wählen, um die Werte eines bestimmten Tages zu sehen. Je nach Leistung wird der Hintergrund der Tabellenzelle farblich verändert.

Die Datei index.php hat folgenden Inhalt:

<html>
<head>
<meta charset="utf-8">
<title>Auswertung</title>

<script language="javascript">
<!--
function Change() {
document.form1.method="post";
document.form1.action="auswertung.php";
document.form1.submit();
}
//--></script> 
</head>
<body>
<center>
<form name="form1">
<h1>Auswertung </h1>
<br><br>

<?PHP

$DBDATEI = $_POST['dbtag'];

echo "Datum:&nbsp;&nbsp;&nbsp;<select name=\"dbtag\" onchange=\"javascript:Change();\">\n";

if ( $_POST['dbtag'] == "Heute" )
    {
    echo "<option Value=\"werte\" selected >Heute</option>\n";
    } else {
    echo "<option Value=\"werte\">Heute</option>\n";
    }

$fp = popen("ls -1 werte_*.db | cut -f2 -d\_ | cut -f1 -d\.","r");
while(!feof($fp))
    {
    $inhalt = trim(fgets($fp,300));
    if ( $_POST['dbtag'] == $inhalt )
        {
        echo "<option Value=\"$inhalt\" selected >$inhalt</option>\n";
        } else {
        echo "<option Value=\"$inhalt\">$inhalt</option>\n";
        }
    }
echo "</select><br><br>\n";
pclose($fp); 

if ( ! isset($DBDATEI) || $DBDATEI == "werte" )
    {
    $SQLDATEI = "werte.db";
    } else {
    $SQLDATEI = "werte_".$DBDATEI.".db";
    }

echo $SQLDATEI."<br>\n";  
$db = new PDO("sqlite:$SQLDATEI");

$HWERTE = $db->query("select * from max WHERE ID = 1;")->fetch();

echo "<h2>Tagesh&ouml;chstwert: ".$HWERTE[1]." / Jahresh&ouml;chstwert: ".$HWERTE[2]."</h2>\n";

echo "<table border=\"1\" cellpadding=\"2\" cellspacing=\"0\">\n";
echo "<tr><th>ZEIT</th><th>PHOTOV</th><th>VERBRAUCH</th><th>BOILER</th></tr>\n";
$result = $db->query("SELECT * FROM daten ORDER BY ZEIT;");
foreach($result as $row)
    {
    echo "<tr><td>".$row[0]."</td>\n";
    $WERT2 = sprintf ( '%02d', $row[1] / 30 );
    $FARBE = sprintf("%02X%02X%02X", $WERT2, 0, 0);
    if ( $row[1] > 6000 )
        {
        echo "<td align=\"right\" style=\"background-color:#FF0000; color:#EEC900\";><b>".$row[1]."</b></td>\n";
        } else {
        echo "<td align=\"right\" style=\"background-color:#".$FARBE."; color:#EEC900\";>".$row[1]."</td>\n";
        }
    echo "<td align=\"right\">".$row[2]."</td>\n";
    if ( $row[3] == 1 )
        {
        echo "<td><img src=\"switch2_on.png\"></td></tr>\n";
        } else {
        echo "<td><img src=\"switch2_off.png\"></td></tr>\n";
        }
}
echo "</table><hr>\n";
$section = file_get_contents('./ch2status.txt', FALSE, NULL, 0, 1);

if ( $section == 1 )
    {
    echo "<h2>Boiler ist zurzeit (manuell) ausgeschaltet</h2>\n";
    } else {
    echo "<h2>Boiler ist zurzeit (manuell) eingeschaltet</h2>\n";
    }

?>               

</form>
</center>
</body>
</html>

Die grosse Solaranlage hat einen Fronius-Wechselrichter. Über eine API lassen sich die Werte auslesen. Sehr interessant ist die Anzeige des Eigenverbrauchs. Es war mir vorher nicht bewusst, wie viel Strom ein normaler Boiler frisst. Deshalb habe ich einen Wärmepumpenboiler einbauen lassen.

Über einen Cronjob läuft das Script abfrage.sh von morgens 6 Uhr bis 19:55 h alle 5 Minuten. Beim ersten Start pro Tag wird der Boiler ausgeschaltet (ende Niedertarif), die DB-Datei vom Vortag umbenannt, Log umbenannt und eine neue, leere DB angelegt. Die Werte vom Fronius-Wechselrichter hole ich über wget ab mit folgenden 2 Befehlen:

wget "http://192.168.1.150/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&DeviceID=1&DataCollection=CommonInverterData" -O werte.txt
wget "http://192.168.1.150/solar_api/v1/GetMeterRealtimeData.cgi?Scope=System" -O werte2.txt

Die Ausgabe der Werte erfolgt im JSON-Format. Mit dem Programm jq lassen sich die Werte in ein
verarbeitbares Format umwandeln, z.B.:

PHOTOV=`cat werte.txt | jq '.Body[].PAC.Value'`
VERBRAUCH=`cat werte2.txt | jq '.Body.Data[].PowerReal_P_Sum'`

Wenn genug Strom vom Dach kommt und ein Überschuss ins Netz eingespiesen wird, zeigt der Wechselrichter dies als Minuswert an. Als Schwellwert für das Einschalten des Boilers habe ich -1000 gesetzt, also wenn mehr als 1000 W eingespiesen wird. Der Boiler bleibt so lange eingeschaltet, bis der Verbrauchswert positiv wird. Ein- und Ausschalten wird mit Unix-Zeit-Stempel in die Datenbank geschrieben.

Zur Minute 55 erfolgt eine Abfrage des Höchstwertes am Tag und den Jahreshöchstwert. Das Script aktualisiert die Werte in die Tabelle max.

Um 18:55 h startet das Script checkboiler.php mit folgendem Inhalt:

#!/usr/bin/php
<?PHP

$SQLDATEI = "werte.db";
$EZEIT = 0;
$GZEIT = 0;

$db = new PDO("sqlite:$SQLDATEI");

$result = $db->query("SELECT * FROM boiler;");
foreach($result as $row)
    {
    $EZEIT = $row[2] - $row[1];
    echo $EZEIT."\n";
    $GZEIT = $GZEIT + $EZEIT;
    }
$MINUTEN = $GZEIT / 60;
IF ( $MINUTEN < 360 )
    {
    system("touch boilernacht.ctl", $result1);
    }
echo "Minuten: $MINUTEN\n";

?>               

Dieses Script prüft, wie viele Minuten der Boiler eingeschaltet war. Liegt der Wert unter 360, schreibt das Script eine Kontrolldatei. Um 21:10 h startet ein Cronjob. Ist die Kontrolldatei vorhanden, schaltet der Boiler ein (Nachtstrom - Niedertarif).

Das Script abfrage.sh hat folgenden Inhalt:

#!/bin/bash
NOW=`date '+%H%M'`
MIN=`date '+%M'`
ZEIT=`date '+%H:%M:%S'`
TAG=`date '+%Y-%m-%d' -d "1 day ago"`
cd /var/www/html/fronius
wget "http://192.168.1.150/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&DeviceID=1&DataCollection=CommonInverterData" -O werte.txt
wget "http://192.168.1.150/solar_api/v1/GetMeterRealtimeData.cgi?Scope=System" -O werte2.txt

PHOTOV=`cat werte.txt | jq '.Body[].PAC.Value'`
VERBRAUCH=`cat werte2.txt | jq '.Body.Data[].PowerReal_P_Sum'`
ZEIT=`cat werte2.txt | jq '.Head.Timestamp' | tr -d \" | tr 'T' ' ' | cut -c1-19`
if [ $NOW = "1855" ]
then
    rm -f boiler.ctl
    ./checkboiler.php
    if [ -r boilernacht.ctl ]
    then
        echo "Boiler Nachtstrom einschalten" 
    fi
fi
if [ $NOW = "0600" ]
then
    mv werte.db werte_${TAG}.db
    mv abfrage.log abfrage_${TAG}.log
    rm -f boilernacht.ctl
    rm -f keinaus.ctl
    echo "stoppen"
    # Ist der Kontakt geschlossen, stoppt der WPB
    /usr/local/bin/CH2_an.py
    echo 1 >ch2status.txt
    chmod 666 werte_${TAG}.db
    echo "BEGIN TRANSACTION;" >solar.sql
    echo "CREATE TABLE daten (ZEIT TEXT,  PHOTOV INTEGER,  VERBR INTEGER, BOILER INTEGER);" >>solar.sql
    echo "CREATE TABLE max (ID INTEGER, TAG INTEGER, JAHR INTEGER);" >>solar.sql
    echo "CREATE TABLE boiler (ID INTEGER, START INTEGER, ENDE INTEGER);" >>solar.sql
    BZEIT=`date '+%s'`
    BZEIT2=`date '+%s' -d "1 minute"`
    echo "INSERT INTO boiler VALUES($NOW, $BZEIT, $BZEIT2);" >>solar.sql
    echo "INSERT INTO max VALUES(1, 0, 0);" >>solar.sql
    echo "COMMIT;" >>solar.sql
    sqlite3 werte.db <solar.sql
    chmod 666 werte.db
fi
echo "BEGIN TRANSACTION;" >solar.sql
if [ $MIN -eq 55 ]
then
    wget "http://192.168.1.150/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&DeviceID=1&DataCollection=MinMaxInverterData" -O werte10.txt
    TAGHWERT=`cat werte10.txt | jq '.Body[].DAY_PMAX.Value'`
    JAHRHWERT=`cat werte10.txt | jq '.Body[].YEAR_PMAX.Value'`
    echo "UPDATE max set TAG=$TAGHWERT, JAHR=$JAHRHWERT WHERE ID=1;" >>solar.sql
fi
# Ist Einspeisung groesser 1000 pruefen, ob Boiler eingeschaltet ist - allenfalls einschalten
SCHALTW=-1000
VERBRINT=`echo $VERBRAUCH | awk '{printf("%d"),$1;}'`
result=$(( SCHALTW > VERBRINT ))

if [ $result -eq 1 ]
    then 
    BLSTAT=`echo "SELECT BOILER from daten ORDER BY ZEIT DESC LIMIT 0,1;" | sqlite3 werte.db`
    if [ $BLSTAT -eq 0 ]
    then
        echo "starten"
        # Ist der Kontakt getrennt, startet der WPB
        /usr/local/bin/CH2_aus.py
        echo 0 >ch2status.txt
        BZEIT=`date '+%s'`
        echo "INSERT INTO boiler VALUES($NOW, $BZEIT, 0);" | sqlite3 werte.db
        touch boiler.ctl
    fi
fi
# Wenn ueber boiler.php das Ausschalten verhindert werden soll, Daten schreiben und Script verlassen
if [ -r keinaus.ctl ]
then
    echo "Boiler nicht ausschalten" 
    echo "INSERT INTO daten VALUES('${ZEIT}', ${PHOTOV}, ${VERBRAUCH}, 1);" >>solar.sql
    echo "COMMIT;" >>solar.sql
    sqlite3 werte.db <solar.sql
    exit 0
fi
# Ist Einspeisung kleiner 1000 pruefen, ob Boiler eingeschaltet ist - allenfalls ausschalten
# Wenn der Boiler eingeschaltet ist und immer noch Strom eingespiesen wird, soll der
# Boiler nicht ausgeschaltet werden.
if [ $result -eq 0 ]
    then 
    BLSTAT=`echo "SELECT BOILER from daten ORDER BY ZEIT DESC LIMIT 0,1;" | sqlite3 werte.db`
    if [ $BLSTAT -eq 1 -a $VERBRINT -gt 0 ]
    then
        echo "stoppen"
        # Ist der Kontakt geschlossen, stoppt der WPB
        /usr/local/bin/CH2_an.py
        echo 1 >ch2status.txt
        GETID=`echo "SELECT MAX(ID) from boiler;" | sqlite3 werte.db`
        BZEIT=`date '+%s'`
        echo "UPDATE boiler set ENDE = $BZEIT WHERE ID = $GETID;" | sqlite3 werte.db
        rm -f boiler.ctl
    fi
fi
if [ -r boiler.ctl ]
then
    echo "INSERT INTO daten VALUES('${ZEIT}', ${PHOTOV}, ${VERBRAUCH}, 1);" >>solar.sql
else
    echo "INSERT INTO daten VALUES('${ZEIT}', ${PHOTOV}, ${VERBRAUCH}, 0);" >>solar.sql
fi
echo "COMMIT;" >>solar.sql
sqlite3 werte.db <solar.sql

Über die Webseite boiler.php ist ein manuelles Schalten des Boilers möglich, sowie das Erzwingen für die Nachtschaltung. Ausserdem lässt sich verhindern, dass der Boiler vom Script abfrage.sh ausgeschaltet wird. Für eine schöne Präsentation der Webseiten verwende ich Bootstrap.

Die Datei boiler.php hat folgenden Inhalt:

<!DOCTYPE html>
<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Boiler ein- ausschalten</title>

<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
<link rel="shortcut icon" href="favicon.ico">

<!-- <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,700" rel="stylesheet"> -->

<!-- Animate.css -->
<link rel="stylesheet" href="css/animate.css">
<!-- Icomoon Icon Fonts-->
<link rel="stylesheet" href="icomoon.css">
<!-- Simple Line Icons -->
<link rel="stylesheet" href="css/simple-line-icons.css">
<!-- Bootstrap  -->
<link rel="stylesheet" href="css/bootstrap.css">
<!-- Superfish -->
<link rel="stylesheet" href="css/superfish.css">
<!-- Flexslider  -->
<link rel="stylesheet" href="css/flexslider.css">

<link rel="stylesheet" href="css/style.css">

<!-- Modernizr JS -->
<script src="../js/modernizr-2.6.2.min.js"></script>
<!-- FOR IE9 below -->
<!--[if lt IE 9]>
<script src="js/respond.min.js"></script>
<![endif]-->
<script language="javascript">
<!--
function Change(z) {
document.form1.method="post";
document.form1.action="boiler.php?"+(z);
document.form1.submit();
}
//--></script> 
</head>
<body>
<form name="form1">
<?php
$FUNKTION = getenv("QUERY_STRING");
//----   Test Switch Actor ----
if ( $FUNKTION == "ein" )
    {
    system("sudo /usr/local/bin/CH2_aus.py", $result1);
    system("echo 0 >/var/www/html/fronius/ch2status.txt", $result1);
    }
if ( $FUNKTION == "aus" )
    {
    system("sudo /usr/local/bin/CH2_ein.py", $result1);
    system("echo 1 >/var/www/html/fronius/ch2status.txt", $result1);
    }
if ( $FUNKTION == "nacht" )
    {
    system("touch /var/www/html/fronius/boilernacht.ctl", $result1);
    }
if ( $FUNKTION == "keinaus" )
    {
    system("touch /var/www/html/fronius/keinaus.ctl", $result1);
    }

?>
    <div id="fh5co-wrapper">
    <div id="fh5co-page">
    <div id="fh5co-header">
        <header id="fh5co-header-section">
            <div class="container">
                <div class="nav-header">
                    <a href="#" class="js-fh5co-nav-toggle fh5co-nav-toggle"><i></i></a>
                    <h1 id="fh5co-logo">Boiler ein- ausschalten</h1>
                    <!-- START #fh5co-menu-wrap -->
                </div>
            </div>
        </header>     
    </div>
        <div id="fh5co-counter-section" class="fh5co-counters">
            <div class="container">
                <div class="row">
                    <div class="col-md-6 col-md-offset-3 text-center fh5co-heading">
<?PHP
?>
            </div>
                <div class="col-md-6 col-md-offset-3 text-center fh5co-heading">
                        <h2>Boiler</h2>
            <!--<input class="btn btn-primary" type="button" name="boilerein" value="Einschalten">-->
            <div class="btn-group btn-group-toggle" data-toggle="buttons">
                <!--<label class="btn btn-primary" disabled>-->
                <label class="btn btn-primary">
                <input type="radio" name="boiler" id="option1" autocomplete="off" onclick="javascript:Change('ein');">Ein
                </label>
                <label class="btn btn-danger active">
                <input type="radio" name="boiler" id="option2" autocomplete="off" onclick="javascript:Change('aus');">Aus
                </label>
                <label class="btn btn-primary">
                <input type="radio" name="boiler" id="option3" autocomplete="off" onclick="javascript:Change('nacht');">Nachtstrom ein
                </label>
                <label class="btn btn-primary">
                <input type="radio" name="boiler" id="option4" autocomplete="off" onclick="javascript:Change('keinaus');">Ausschalten verhindern
                </label>
            </div>
                    </div>
                </div>
<?PHP
?>               

            </div>
        </div>

</form>
</body>
</html>



Als weiteren Ausbauschritt habe ich mir überlegt, die Wetterprognose zu berücksichtigen. Wenn an einem schlechten Tag der Boiler zu wenig eingeschaltet und die Einschaltung nachts zum Niedertarif geplant ist, soll die Nachtschaltung nicht erfolgen, wenn für den nächsten Tag schönes Wetter vorausgesagt wird. Die Wetterprognose hole ich von der Seite openweathermap.org. Es braucht eine einmalige Registrierung. Von openweathermap gibt es eine ID, welche in der Abfrage mitgeliefert werden muss. Die Werte erhalte ich im JSON-Format.

Für die Weiterverarbeitung verwende ich wieder jq. Das Script nachtstrom.sh hat folgenden Inhalt:

#!/bin/bash
cd /var/www/html/fronius
wget "https://api.openweathermap.org/data/2.5/forecast?q=Langenthal,ch&units=metric&lang=de&appid=XXXXXXXXXXXXXXXXXXXXXX" -O forecast.txt

for i in 0 1 2 3 4 5 6 7 8 9 10 11
do
    echo "cat forecast.txt | jq '.list[${i}].dt_txt' | cut -c2-14 | tr ' ' '_' >datum.txt" >fct.sh
    echo "cat forecast.txt | jq '.list[${i}].weather[].id' > ${i}.wth" >>fct.sh
    chmod +x fct.sh
    ./fct.sh
    DATUM=`cat datum.txt`
    echo $DATUM
    mv ${i}.wth ${DATUM}.wth
done    

MORGEN=`date +%Y-%m-%d -d "1 day"`
rm -f ${MORGEN}_00.wth
rm -f ${MORGEN}_03.wth
rm -f ${MORGEN}_06.wth
rm -f ${MORGEN}_21.wth
ANZAHL=`grep 80[0-3] ${MORGEN}*.wth | wc -l`
echo $ANZAHL
if [ $ANZAHL -gt 1 ]
then
    echo "Keine Nachtschaltung"
    #rm -f boilernacht.ctl
fi

if [ -r boilernacht.ctl ]
then
    echo "Boiler Nachtstrom einschalten" 
    # Ist der Kontakt getrennt, startet der WPB
    /usr/local/bin/CH2_aus.py
fi

Bis bei uns im Mittelland der Nebel eingesetzt hat, funktionierte die Sache gut. Dann hat an einem Tag die Wetterprognose  Sonnenschein angekündigt und bei uns war Nebel. Dies hatte zur Folge, dass wir kalt duschen mussten und ich müsste mir einiges anhören. Deshalb ist diese Funktion zurzeit deaktiviert.

In der Crontab gibt es folgende Einträge:

*/5 6-19 * * * /var/www/html/fronius/abfrage.sh >>/var/www/html/fronius/abfrage.log 2>&1
10 21 * * * /var/www/html/fronius/nachtstrom.sh >/var/www/html/fronius/nachtstrom.log 2>&1
0 13 * 1,2,11,12 0,6 /var/www/html/fronius/wochenende.sh >/var/www/html/fronius/wochenende.log 2>&1

In den Wintermonaten ist der Warmwasserverbrauch etwas höher und es gibt nur wenig Sonnenschein.
Deshalb soll am Samstag und am Sonntag ab 13:00 h der Boiler eingeschaltet werden.

Das Ganze ist auf meine Situation eingerichtet. Ich hoffe, dass trotzdem jemand profitieren kann, um seinen Eigenverbrauch zu optimieren.

Tags

Boiler, Werte, Script, JSON-Format, Tag, Wert, Wechselrichter, Strom

René Metaal
Geschrieben von René Metaal am 6. August 2023 um 11:28

Hallo zusammen, Ich habe jetzt auch eine "große" Solaranlage (22 Panels) mit einem fronius Wechselrichter und bin neu in PV Land. Habe aber 35 Jahren Erfahrung mit shell scripting und Linux/Unix. Deswegen versuch ich hier in der Community viel zu lernen, und zu dokumentieren. Meine Beitrage ist zwar nicht so groß aber ich gebe mein Bestes. (Sorry für Schreibfehler ich bin ein Niederländer.)