Einfache und günstige Stromüberwachung zuhause

Ich habe zwar keinen fürchterlich smarten Stromzähler der jedem erzählt was ich gerade so für Strom verbrauche, aber selbst dumme Stromzähler bieten schon lange eine einfache Möglichkeit um den aktuellen Stromverbrauch automatisch auszuwerten…

Nachdem es in Teil 1 darum ging wie man Temperatur, Luftdruck und Luftfeuchte mit dem Raspberry Pi misst, und in Teil 2 das ganze mit dem ESP8266 gemacht wurde, bauen wir auf dem vorhandenem Wissen auf und gehen nun zum Stromzähler über. Die Infrastruktur von Teil 1 und 2 nutzen wir wieder und erweitern diese entsprechend.

Jeder Stromzähler hat schon seit längerem eine blinkende LED – entweder eine normale oder eine IR-LED. Dieser sog. Impulszähler heißt auch S0-Schnittstelle und ist ein Standard zum auslesen von Verbrauchswerten. Bei meinem Stromzähler sieht das Teil so aus:

Und zwar ist das obere die S0 Schnittstelle, dort wo „10.000 Imp./kWh“ steht. Das rechts ist die optische Schnittstelle mit der man per Blinksignal den Zähler steuern und auslesen kann. Uns bringt aber die optische Schnittstelle keinen Mehrwert denn den aktuellen Verbrauch in Watt und den Gesamtverbrauch in kWh kriegen wir auch über die S0 Schnittstelle.

Und wie funktioniert das? Wie man anhand der Aufschrift sehen kann blinkt dort eine IR-LED. Diese blinkt 10000 Mal pro kWh die verbraucht wurde. Sehen kann man das wenn man die LED mit einem Foto ohne IR-Filter ansieht. Normale Handys können das blinken sehen, ein Digitalfoto auch, das I-Phone nicht. Aus der Zeit zwischen zwei mal blinken kann man den aktuellen Momentanverbrauch in Watt ausrechnen (Aktueller Verbrauch in W = 360000 / zeit_zwischen_blinken_in_ms) und wenn man zählt wie oft es insgesamt geblinkt hat kann man den Verbrauch in kWh ausrechnen (Verwendete Energie in kWh = anzahl_blinken / 10000).

Die nächste Frage die sich nun naturgemäß stellt: Wie messen wir das blinken?
Ich hatte es zuallererst mit einem normalen Fototransistor versucht, denn dieser sollte eigentlich auf Infrarotlicht auch empfindlich sein (testen kann man das mit einer Fernbedienung, die haben ja IR-Leds), nur war dieser Versuch nicht so sonderlich erfolgreich, vorallem weil ich ein digitales Signal möchte damit ich mit einem Level-Change Interrupt arbeiten kann, also einen Programmcode automatisch ausführen lasse sobald ein blinken kommt, ohne da ständig das aktuelle Level abfragen zu müssen.

Zur Rettung kam die Bastelkiste mit schönen Teilen aus Fernost. In diesem Fall einem „Flame Sensor Module„. Diese Modul ist eigentlich gedacht um eine Flamme zu erkennen und ist deshalb besonders im IR-Bereich des Lichtes sensitiv (760 nm to 1100 nm). Irgendwo in diesem Spektrum ist auch die IR-Led unseres Stromzählers unterwegs. Wie praktisch!

Das Modul hat eine einstellbare Empfindlichkeit und zur Kontrolle leuchtet eine (grüne) LED wenn er ein Licht erkannt hat. Es gibt einen digital Ausgang A0 der auf High (oder Low, ich habs nicht geprüft, denn zum Zeiten messen ist es egal ob wir von High zu High oder Low zu Low messen!) geht sobald Licht erkannt wurde, ähnlich wie das Mikrofon Modul welches wir beim Morsecode entschlüsseln verwendet haben. Dort haben wir aber den analogen Ausgang A0 genommen der uns den aktuellen Messwert (Lautstärke, bzw. in unserem Fall Helligkeit) ausgibt. Da wir aber nur Impulse zählen und der ADC zum messen Zeit braucht und außerdem keinen schöner Pin Change Interrupt verwendet werden kann nützt uns der Ausgang A0 nicht viel und wir bleiben beim Ausgang D0. Diesen verbinden wir mit dem ESP8266 NodeMCU Modul Pin D1, sowie 3.3V und GND. Fertig.

Hier sieht man das ganze professionell an meinen Stromzähler gebaut (dank Tesafilm leicht wieder abbaubar und es gibt keinerlei Verbindungen zum Stromzähler):

Der ESP8266 startet beim einschalten einen 60 Sekunden Timer (der ist fürs Daten senden da) und einen Pin Change Interrupt (der erkennt wenn es geblinkt hat). Im Interrupt wird die totale Anzahl an Blinken seit Start des ESP8266 gezählt und außerdem immer der messA und messB Wert gespeichert. Speichern tun wir da einfach die Zeit seit einschalten in Millisekunden (time.ticks_ms()) beim senden rechnen wir aus den Timerwerten den Abstand zwischen dem letzten erfassten zwei Blinken aus, sowie die kWh die wir insgesamt seit Start genutzt haben. Das ganze wird dann an ein PHP-Script auf unserem Server geschickt und fertig ist der Micropython Teil auf dem ESP8266:

import time
import machine
import network
import usocket

# Globals
debug = 0
sensorid = 5
sendseconds = 60
secondstrywificonnect = 20
totblinks = 0
# GPIO 5 is Pin D1 on NodeMCU
pinwithIRsensor = 5
wifiname = '1'
wifipass = '2'
serveraddress = 'myserver.de'
passforsending = '3'
messA = 0
messB = 0

if debug:
    print(str(time.ticks_ms()) + ': ***STARTUP***')
    print(str(time.ticks_ms()) + ': sensorid: ' + str(sensorid))
    print(str(time.ticks_ms()) + ': sendseconds: ' + str(sendseconds))
    print(str(time.ticks_ms()) + ': secondstrywificonnect: ' + str(secondstrywificonnect))
    print(str(time.ticks_ms()) + ': pinwithIRsensor: ' + str(pinwithIRsensor))
    print(str(time.ticks_ms()) + ': wifiname: ' + str(wifiname))
    print(str(time.ticks_ms()) + ': wifipass: ' + str(wifipass))
    print(str(time.ticks_ms()) + ': serveraddress: ' + str(serveraddress))
    print(str(time.ticks_ms()) + ': passforsending: ' + str(passforsending))

# This function will send our Data to the Internet
def senddata(timer):
    # Any exception will return
    try:
        global messA
        global messB
        global totblinks
        if debug:
            print(str(time.ticks_ms()) + ': ***senddata***')
            print(str(time.ticks_ms()) + ': messA ' + str(messA))
            print(str(time.ticks_ms()) + ': messB ' + str(messB))
            print(str(time.ticks_ms()) + ': totblinks ' + str(totblinks))
        if messA == 0 or messB == 0:
            if debug:
                print(str(time.ticks_ms()) + ': messA or messB == 0 -> returning')
            return
        if abs(messA - messB) > 3600000:
            if debug:
                print(str(time.ticks_ms()) + ': Possible Overflow')
            if messA > messB:
                if debug:
                    print(str(time.ticks_ms()) + ': Setting messA to 0')
                messA = 0
            if messB > messA:
                if debug:
                    print(str(time.ticks_ms()) + ': Setting messA to 0')
                messB = 0
            return
        if messA > messB:
            timebetweenpulses = messA - messB
        else:
            timebetweenpulses = messB - messA
        watt = 360000 / timebetweenpulses
        if totblinks > 0:
            kwh_since_start = totblinks / 10000
        else:
            kwh_since_start = 0
        if debug:
            print(str(time.ticks_ms()) + ': timebetweenpulses (ms) ' + str(timebetweenpulses))
            print(str(time.ticks_ms()) + ': watt ' + str(watt))
            print(str(time.ticks_ms()) + ': kwh_since_start ' + str(kwh_since_start))
        # Connect to WiFi -> Get interfaces
        sta_if = network.WLAN(network.STA_IF)
        ap_if = network.WLAN(network.AP_IF)
        # Deactivate access point, we're station only
        ap_if.active(False)
        # Now connect
        connectcount = 0
        if not sta_if.isconnected():
            sta_if.active(True)
            sta_if.connect(wifiname, wifipass)
            while not sta_if.isconnected():
                connectcount = connectcount + 1
                time.sleep(1)
                if debug:
                    print(str(time.ticks_ms()) + ': Connect try: ' + str(connectcount))
                if connectcount > secondstrywificonnect:
                    # We didn't connect after secondstrywificonnect seconds. Let's sleep
                    if debug:
                        print(str(time.ticks_ms()) + ': Connect failed after ' + str(connectcount) + ' seconds sleep. Giving up.')
                    return
        # Send data to the Internet, a Post Request with http - we don't use SSL here!
        content = b'sensorid=' + str(sensorid) + '&power=' + str(watt) + '&kwh_since_start=' + str(kwh_since_start) + '&password=' + passforsending
        if debug:
            print(str(time.ticks_ms()) + ': Connecting to website')
        addr_info = usocket.getaddrinfo(serveraddress, 80)
        addr = addr_info[0][-1]
        sock = usocket.socket()
        sock.connect(addr)
        sock.send(b'POST /tempsensor.php HTTP/1.1\r\n')
        sock.send(b'Host: ' + serveraddress + b'\r\n')
        sock.send(b'Content-Type: application/x-www-form-urlencoded\r\n')
        sock.send(b'Content-Length: ' + str(len(content)) + '\r\n')
        sock.send(b'\r\n')
        if debug:
            print(str(time.ticks_ms()) + ': Sending: ' + str(content))
        sock.send(content)
        sock.send(b'\r\n\r\n')
        if debug:
            print(str(time.ticks_ms()) + ': Answer: ' + str(sock.recv(1000)))
        sock.close()
        messA = 0
        messB = 0
    except:
        if debug:
            print(str(time.ticks_ms()) + ': Exception in senddata() happend. Returning')
        return

# This function is called everytime we get a pulse
def blinkarrived(pin):
    # Any exception will return
    try:
        global messA
        global messB
        global totblinks
        if debug:
            print(str(time.ticks_ms()) + ': ***blinkarrived***')
            print(str(time.ticks_ms()) + ': messA ' + str(messA))
            print(str(time.ticks_ms()) + ': messB ' + str(messB))
            print(str(time.ticks_ms()) + ': totblinks ' + str(totblinks))
        akttime = time.ticks_ms()
        totblinks = totblinks + 1
        if messA > messB:
            messB = akttime
            if debug:
                print(str(time.ticks_ms()) + ': Setting messB to: ' + str(akttime))
            return
        else:
            messA = akttime
            if debug:
                print(str(time.ticks_ms()) + ': Setting messA to: ' + str(akttime))
            return
    except:
        if debug:
            print(str(time.ticks_ms()) + ': Exception in blinkarrived() happend. Returning')
        return

# Any exception will reset us
try:
    # Activate a timer which will send our last sample every sendseconds Seconds
    if debug:
        print(str(time.ticks_ms()) + ': Activating Timer')
    tim = machine.Timer(-1)
    tim.init(period = sendseconds * 1000, mode = machine.Timer.PERIODIC, callback = senddata)
    # Activate a callback everytime we get a blink
    if debug:
        print(str(time.ticks_ms()) + ': Activating Interrupt')
    irsensor = machine.Pin(pinwithIRsensor, machine.Pin.IN, machine.Pin.PULL_UP)
    irsensor.irq(trigger = machine.Pin.IRQ_RISING, handler = blinkarrived)
except:
    if debug:
        print(str(time.ticks_ms()) + ': Exception in main() happend. RESTART')
    machine.reset()

(Ja, da ist ein Fehler in der Debug-Ausgabe, es muss natürlich ‚wifipass:‘ und nicht ‚pinwithIRsensor:‘ in der letzten Zeile der Starup-Debugausgabe heißen!)

Im Internet kommt ein freundliches PHP-Script (welches auf die Messwerte der Temperatursensoren annimmt) was nicht nur die Daten annimmt und in eine Mysql Datenbank speichert, es rechnet auch noch den Verbrauch seit dem letzten Datensenden mit aus und schreibt ihn in die Datenbank. Das macht das Auswerten der Daten deutlich einfacher:

<?php

$sensorpass = "zzz";
$dbserver = "localhost";
$dbuser = "envsensors";
$dbpass = "uuu";
$dbname = "envsensors";

$sensorid = $_POST["sensorid"];
if (!isset($sensorid) || !is_numeric($sensorid)) {
    die('Error 1');
}

$password = $_POST["password"];
if (!isset($password) || !($password == $sensorpass)) {
    die('Error 2');
}

$temp = $_POST["temp"];
if (!isset($temp) || !is_numeric($temp)) {
    $temp = 0;
}

$press = $_POST["press"];
if (!isset($press) || !is_numeric($press)) {
    $press = 0;
}

$hum = $_POST["hum"];
if (!isset($hum) || !is_numeric($hum)) {
    $hum = 0;
}

$power = $_POST["power"];
if (!isset($power) || !is_numeric($power)) {
    $power = 0;
}

$kwh_since_start = $_POST["kwh_since_start"];
if (!isset($kwh_since_start) || !is_numeric($kwh_since_start)) {
    $kwh_since_start = 0;
}

$conn = new mysqli($dbserver, $dbuser, $dbpass, $dbname);
if ($conn->connect_error) {
    die("Error 3");
}

$lastkwhselect = "select kwh_since_start from sensorvalues where sensorid = " . $sensorid . "  order by id desc limit 1";
$lastkwhresult = $conn->query($lastkwhselect);

if ($lastkwhresult->num_rows == 1) {
    $lastkwhrow = $lastkwhresult->fetch_assoc();
    $kwh_since_last_send = $kwh_since_start - $lastkwhrow['kwh_since_start'];
} else {
    $kwh_since_last_send = 0;
}

$stmt = $conn->prepare("INSERT INTO sensorvalues (sensorid, temp, press, hum, power, kwh_since_start, kwh_since_last_send) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("idddddd", $sensorid, $temp, $press, $hum, $power, $kwh_since_start, $kwh_since_last_send);
$stmt->execute();

echo "Done.";

$stmt->close();
$conn->close();

?>

Hier noch die Struktur der Datenbank:

CREATE TABLE `sensors` (
  `id` int(11) NOT NULL,
  `name` varchar(32) NOT NULL,
  `description` text NOT NULL,
  `monitoring` tinyint(4) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `sensorvalues` (
  `id` bigint(20) NOT NULL,
  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `sensorid` int(11) NOT NULL,
  `temp` decimal(10,3) NOT NULL,
  `press` decimal(10,3) NOT NULL,
  `hum` decimal(10,3) NOT NULL,
  `power` decimal(10,3) NOT NULL,
  `kwh_since_start` decimal(15,5) NOT NULL,
  `kwh_since_last_send` decimal(10,5) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE `sensors`
  ADD PRIMARY KEY (`id`);

ALTER TABLE `sensorvalues`
  ADD PRIMARY KEY (`id`),
  ADD KEY `timestampindex` (`timestamp`),
  ADD KEY `sensorindex` (`sensorid`);

ALTER TABLE `sensors`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;

ALTER TABLE `sensorvalues`
  MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=252529;

Hier gibts noch ein Script um zu checken ob alle Sensoren sauber laufen die im Echtbetrieb sind:

<?php

$dbserver = "localhost";
$dbuser = "envsensors";
$dbpass = "uuu";
$dbname = "envsensors";

$conn = new mysqli($dbserver, $dbuser, $dbpass, $dbname);

$sensorselect = "select id, name from sensors where monitoring = 1 order by id asc";
$sensorresult = $conn->query($sensorselect);
if ($sensorresult->num_rows > 0) {
    while($sensorrow = $sensorresult->fetch_assoc()) {
        echo "Prüfe Sensor " . $sensorrow['id'] . "<br>";

        $checkselect = "select id from sensorvalues where sensorid = " . $sensorrow['id'] . " and timestamp >= date_sub(now(), interval 15 minute)";
        $checkresult = $conn->query($checkselect);
        
        if ($checkresult->num_rows == 0) {
            echo "Keine Daten. Sende Mail!<br>";
            mail("me@myserver.de", "Temperatursensor " . $sensorrow['id'] . " (" . $sensorrow['name'] . ") hat in den letzten 15 Minuten keine Daten gemeldet!", "no text");
        } 
    }
}

echo "Done.";

$conn->close();

?>

Und nachdem das ganze doch eine Menge Programmcode ist, gibt es diesen auf Github: Hier!

Natürlich zeigen wir das ganze auch in Grafana an, die beiden Querys sind recht einfach:

SELECT
  UNIX_TIMESTAMP(timestamp) as time_sec,
  power as value,
  'Power' as metric
FROM sensorvalues
WHERE $__timeFilter(timestamp)
AND sensorid = 5
ORDER BY timestamp ASC
SELECT
  UNIX_TIMESTAMP(timestamp) as time_sec,
  (power / 1000) * 27.88 as value,
  'cost in cent/hour' as metric
FROM sensorvalues
WHERE $__timeFilter(timestamp)
AND sensorid = 5
ORDER BY timestamp ASC

Dabei achten wir drauf dass der zweite Query unter „Display->Series Override“ auf der rechten Y-Achse angezeigt wird. So sieht das ganze dann in einem Zeitraum aus wo niemand zuhause ist:

Wir sehen hier unsere beiden Kühlschränke die zuhause Strom fressen. Ca. alle 75 Minuten geht einer für ca. 20 Minuten an und benötigt dabei ~75 Watt. Manchmal sind beide gleichzeitig an, dann ist es mehr Watt 🙂
Rechts sehen wir dabei den Strompreis den wir zahlen würden wenn wir eine Stunde lang die aktuelle Watt Anzahl verwenden, und zwar in Cent. Solange also kein Kühlschrank an ist verbrauchen wir 37 Watt und zahlen 1,02 Cent pro Stunde, mit Kühlschrank sind es 95 Watt und 2,66 Cent pro Stunde.

Wenn kein Kühlschrank läuft werden ca. 37 Watt verbraucht. Das ist dann der eine oder andere ESP8266, Raspberry Pi und die Fritzbox. Als Maximalwerte habe ich bislang 36 Watt bis 13846 Watt ermittelt. Um 13 kW zu erzeugen musste ich schon ordentlich Geräte anschalten, das wird in der Realität wahrscheinlich nie auftreten.

Die Stromspitzen sind übrigens der Anlaufstrom des Kühlschranks, der kann durchaus mal leicht 500 Watt betragen, er ist aber nur recht kurz. Messen tun wir ja immer nur kurz vorm Senden der Daten in einem Blinkintervall. Das heißt manchmal erwischen wir den Anlaufstrom, manchmal nicht. Um trotzdem zu wissen wieviel Strom wir so pro Monat verbrauchen gibt es die Spalten kwh_since_start und kwh_since_last_send, die machen Abfragen wie diese recht einfach:

select
timestamp as time,
sum(kwh_since_last_send) as power_total,
(sum(kwh_since_last_send) * 0.2788) as price_in_eur
from sensorvalues
where sensorid = 5
and year(timestamp) >= year(curdate()) - 1
group by year(timestamp), month(timestamp)
order by id asc

Der Query ist schon an Grafana angepasst für eine schöne Balkengrafik, deshalb etwas komisch geschrieben. Und ja, ich zahle bei Stromanbieter 27.88 Cent pro kWh…

Inspiriert wurde ich übrigens zu dem Projekt von volkszaehler.org – die lesen auch allerlei Stromzählerdaten aus. Meiner Meinung nach nur deutlich komplexer als bei mir.

Beteilige dich an der Unterhaltung

1 Kommentar

Schreib einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.