Inhalt

Ziele und Vorbereitungen

  1. Erste Schritte
  2. Anzapfen des Shellys
  3. Mehr PV, Akkus und E-Zigaretten
  4. Anzeige des Ladestands und Einspeisestatus-LED
  5. Tiefschlaf ist angesagt
  6. Schaltbild und vollständiger Code

Ziele und Vorbereitungen

Im letzten Blogeintrag habe ich beschrieben, wie ich mir eine miniPV-Anlage aufs Dach gebaut habe. Das ganze Projekt ist jetzt schon wieder fast 5 Monate her, jetzt, in den Osterferien, habe ich endlich wieder Zeit gefunden, um mich dem nächsten Projekt zu widmen: Ich möchte die aktuelle PV-Produktion, die aktuelle Gesamtleistung und die aktuelle dem Netz entnommene oder eingespeiste Leistung irgendwie sichtbar machen.

Dafür habe ich mir schon Mitte November einen ESP8266 mit und ohne OLED-Display gekauft.

In diesem Blogeintrag möchte ich nun beschreiben, wie ich es geschafft habe den ESP entsprechend zu programmieren und dazu so zu beschalten, dass er über einen kleinen LiPo-Akku und einer PV-Zelle autark läuft. Ich möchte meine Einzelschritte und Fehler auch mit dokumentieren, bemühe mich aber das Ganze nicht allzu lang und unterhaltsam zu halten. Wer nur die fertige Beschaltung und den fertigen Code möchte, muss bis ganz zum Ende scrollen.

Neben dem Nutzen und der Neugierde mal mit einem neuen Microcontroller zu arbeiten, stand der positive Nebeneffekt, dass wir in der Schule auch überlegen uns ein paar ESP anzuschaffen und im Lernfeld „Cyber-physische Systeme“ ein bisschen damit programmieren. Da kann man sich daheim auch schon mal damit beschäftigen 😉

1. Erste Schritte

Als erstes wollte ich einfach nur das Display zum Leben erwecken und gucken, wie es funktioniert dort unterschiedliche Texte darzustellen und zu positionieren. Den Einstieg dazu tat die Anleitung direkt im Ebay-Artikel. Jetzt wieder mein etwas eingerostetes Arduino / C++ Code-Wissen reaktivieren und schon nach kurzer Zeit hatte ich Text auf dem Display.

Sowas in diese Richtung habe ich auch schon mit den Arduino (ein anderer Microcontroller) gemacht. Der ESP hat allerdings WLAN, also war das der nächste Schritt: Eine WLAN-Verbindung mit meinem Gäste-WLAN „Internetschnorrer“ herstellen, wo auch alle weiteren SmartHome-Geräte drin hängen. Das ist schnell geglückt und ich habe nach erfolgreicher Verbindung einfach einen Zähler hinter dem „Loading“ hochlaufen lassen. Auch das ist gar nicht soo einfach, da die Funktion, um etwas auf dem Display darzustellen den Datentyp „char“ benötigt, Zahlen, die man auch hochzählen kann, aber zum Datentyp „int“ gehören. Eine Umwandlung über ein Array musste also her.

Ich machte mir auch gleich Gedanken, was ich denn gerne wo auf dem Display dargestellt haben möchte und entwarf einen „Muster-Screen“ mit fiktiven Werten.

Fiktive Angaben, reicht aber um herauszufinden, wo was stehen soll und wie es aussieht

Checklist:

  • ✅ Display ansteuern
  • ✅ WLAN-Verbindung aufbauen
  • ➡️ Next up: Wert des Shelly auf dem Display darstellen

2. Anzapfen des Shellys

2.1 Der Plan

Ich möchte die Daten (aktueller Energieverbrauch) von meinen Shellys über WLAN abfragen.

Das will ich genauer wissen. Weiterlesen…. (<– clickme)

Die Shellys sind auch dafür bekannt, dass man sie mit vielen Protokollen steuern bzw. abfragen kann. Ich hatte mir erst überlegt, ob ich das vielleicht auch über MQTT (ein weit verbreitetes IoT Protokoll für genau solche Zwecke) mache. Allerdings braucht das MQTT-Protokoll immer einen Broker, also sozusagen eine Zentrale. Die habe ich nicht und will sie auch nicht irgendwo aufsetzen, nur wegen zwei Shellys von denen ich Daten benötige. Vielleicht kommt das irgendwann mal, wenn ich mein SmartHome nicht mehr über die Cloud steuern lasse, sondern mir selbst den Heimserver aufsetze, aber dafür brauche ich erstmal wieder einen Zeitraum mit viiel Zeit…

2.2 ChatGPT und der UDP Fail

Ich startete mit einer Anfrage an den Microsoft Copilot (also GPT-4). Die Antwort funktionierte, allerdings war das nicht so das gelbe vom Ei…

Das will ich genauer wissen. Weiterlesen…

Ich dachte mir ich frage einfach mal den Copilot, also GPT-4, wie das die Abfrage umsetzen würde. Ich erhielt einen Code, der mit ein paar kleinen Anpassungen tatsächlich funktioniert hat. Grundsätzlich bemühe ich mich die Anzahl der Anfragen an KI möglichst gering zu halten, gegenüber dem normalen Googlen sieht die Energiebilanz von KI doch sehr schlecht aus.

//Funktion zur Abfrage von Werten aus einem Shelly
double getShellyData(String rpccmd, const char* shellyIP, String JSONValue){
  //RPC Befehl an das Shelly senden
  double power;
  udp.beginPacket(shellyIP, shellyPort);
  udp.print(rpccmd);
  udp.endPacket();
  //auf Antwort warten
  delay(500);
  if (udp.parsePacket()){
    char reply[1024];
    int len = udp.read(reply,1023);
    if (len>0){
      reply[len]=0;
      //JSON-Daten parsen
      StaticJsonDocument<1024> doc; //Größe je nach erwarteter Antwortgröße
      DeserializationError error = deserializeJson(doc,reply);
      if(!error){
        power = doc["result"][JSONValue];
      } else {
        power = 999; //Fehler beim parsen der JSON Datei
      }
    }
  }
  return power;
}

Der Code zeigt die Funktion einer Abfrage an ein Shelly. Ein Aufruf könnte folgendermaßen aussehen:
PVpwr = getShellyData("{\"id\": 1, \"method\": \"PM1.GetStatus\", \"params\": {\"id\":0}",PVshellyIP, "apower");

Der Code zeigt die Funktion einer Abfrage an ein Shelly. Ein Aufruf könnte folgendermaßen aussehen:

PVpwr = getShellyData("{\"id\": 1, \"method\": \"PM1.GetStatus\", \"params\": {\"id\":0}",PVshellyIP, "apower");

Bild auf dem nur Wert bei „Erzeugung“

So super funktioniert hat das aber nicht, es gab folgende Probleme:

  • Manchmal kommt als Wert „0“ oder etwas ungültiges (siehe Code „Fehler beim parsen der JSON Datei“)

Ich schaffte es nicht den Befehl auch für das zweite Shelly gangbar zu machen

das erste Mal ein wirklich richtiger Wert – hier verschwommen, weil die Belichtungszeit der Kamera zu lang war…

2.3 Ein neuer Ansatz muss her

Bei der Suche nach Lösungen bin ich auf die Idee gekommen, dass ich auch bei einer Standard-HTTP-Anfrage, wie sie ein Browser macht, eine Antwort bekomme. Diese Antwort muss ich eigentlich nur noch auswerten.

oben sieht man die Anfrage – und unten eine typsiche JSON-Antwort
Das will ich genauer wissen. Weiterlesen…

Bei meiner Suche nach den Befehlen und Namen der Datenfelder hatte ich mittlerweile auch die Shelly-API-Dokumentation immer weiter studiert.

Dabei bin ich draufgekommen, dass die RPC Commands einfache HTTP GET Befehle sind und dann eine JSON als Antwort kommt.

Also gabs eine neue Anfrage an den Copilot, diesmal sehr präzise formuliert

ich möchte mit einem ESP8266 einen http get request an ein Shelly gerät senden. Die Antwort auf den Befehl […] sieht folgendermaßen aus: [JSON Antwort wie im Screenshot].

Anfrage an den Copilot

Und siehe da, ich bekomme eine Antwort, die – natürlich nach ein paar Anpassungen – funktioniert.

2.4 Finale Veränderungen

Ich kopiere das Ganze für mein zweites Shelly, passe den Code entsprechend an und schon habe ich beide Werte.

  • Wert 1: Aktuell erzeugte Leistung durch die PV – gemessen vom Shelly PM mini in der Einspeisesteckdose („apower“ im JSON, immer negativ, weil Einspeisung)
  • Wert 2: Aktuell dem Netz entnommene Leistung (Wert positiv) oder aktuelle Einspeisung ins Stromnetz (Wert negativ) – gemessen durch das Shelly Pro 3EM im Sicherungskasten („total_act_power“ im JSON)
zweite Reihe rechts das Shelly Pro 3EM und die drei Spulen um die Außenleiter oberhalb des FI.
Shelly PM mini unterhalb der Steckdose am Balkon

Wenn ich beide Werte verrechne, kann ich die aktuell benötigte Gesamtleistung in der Wohnung ausrechnen. Dargestellt sieht das dann so aus:

Alle Werte werden angezeigt und stimmen 🥳

Auch das Umschalten zwischen „Netzbezug“ und „Einspeisung“ in der letzten Zeile funktioniert:

Checkliste:

  • ✅ Display ansteuern
  • ✅ WLAN-Verbindung aufbauen
  • ✅ Werte per selbstgebautem UDP-Paket abfragen
  • ✅ Fehler bekommen – Lust verlieren – nach Alternativen suchen
  • ✅ Werte per http Request abfragen
  • ✅ Codeoptimierungen
  • ➡️Next up: Externe Spannungsversorgung des ESP

3. Mehr PV, Akkus aus E-Zigaretten

3.1 Der Plan

Da so ein ESP recht sparsam ist, möchte ich ihn mit einer kleinen PV-Zelle und einem Akku als Speicher betreiben.

Das will ich genauer wissen. Weiterlesen…

Der ESP ist auch dafür bekannt, dass er einen sehr geringen Energiebedarf hat, daher dachte ich mir sollte es ja möglich sein ihn auch über eine kleine PV-Zelle mit einem Akku als Pufferspeicher dauerhaft zu betreiben. Nach ersten Tests, bei denen ich den ESP einfach mal über Nacht an einer Powerbank hängen lies, zeigten mir, dass die Stromaufnahme wohl doch höher als gedacht ist. Eine kleine 4000mAh Powerbank war nach ca. 10 Std leer. Gut, dass der ESP auch eine deep-Sleep Funktion hat… aber da kümmere ich mich später drum.

90mA sind dann doch schon sehr viel….

Ich fand eine Webseite, die genau das beschrieben hat, was ich vorhatte, und ich bestellte mir die fehlenden Teile.

3.2 Ausschlachten der E-Zigaretten

Kommen wir nun zu einer der bescheuertsten Umweltsünden, die die Konsumgesellschaft in den letzten Jahren so hervorgebracht hat: Wegwerf-E-Zigaretten.

Einmal-E-Zigaretten – liegen auf Festivals zu hunderten am Boden

Die Dinger gibt es auf diversen Festivals und anderen Events zu kaufen. Drin steht ein geladener Akku, stinkendes Aroma zeug und ein Verdampfer. Das Problem: wenn das Aroma verdampft ist, ist das Teil für den Abfalleimer, bzw. eigentlich den Elektroschrott, bestimmt. Wie es mit Wegwerf-Produkten aber so ist, landen sie oft einfach auf dem Boden. Traurig.

eine auseinander genommene E-Zigarette – stinkt wiederlich nach Aroma

Aus den Akkus aus E-Zigaretten betreibe ich mittlerweile auch drei Temperatursensoren und meine Türklingel, statt Knopfzellen bzw. zwei AAA-Batterien.

Zur Verteidigung der dampfenden Mitmenschen muss man sagen, dass es die Dinger auch in Wiederauffüll- und -aufladbar gibt. Die landen dann im Normalfall nicht auf dem Boden.

Mehr Infos…
manche sehen wieder anders aus…
interessant auch, dass alle ein Mikrofon verwenden um das Ziehen zu erkennen und den Verdampfer einzuschalten

3.3 Energieversorgung ESP

Ich nehme also einen Akku aus der E-Zigarette und bastle mir aus Laderegler und PV-Zelle die Energieversorgung.

Das will ich genauer wissen. Weiterlesen…

Ich bereitete also einen Akku aus einer E-Zigarette vor indem ich eine kleine Platine mit Über-/Unterspannungsschutz anlötete (hätte ich mal genauer gucken sollen, was ich bestellt habe, denn der Laderegler hat die Schutzfunktion schon eingebaut…).

Schutzbeschaltung des LiPos

Nungut, jetzt noch die PV-Panels verbinden, da kam mir die Idee das mit Schaschlik Spießen und Heißkleber zu realisieren. Dann den Spannungsregler mit seinen zwei (bei mir drei) Kondensatoren an die Platine löten.

zwei PV-Zellen mit Schaschlikspießen verbunden 😉
Von links: Kondensator – Spannungsregler – 2x Kondensator – so bekommt der ESP konstante 3,3V

PV-Modul an den Laderegler anschließen, Akku und ESP an den Laderegler anschließen: Funktioniert. Bei Sonnenlicht leuchtet auch die Lade-LED, der Akku scheint also zu laden, wenn Sonne scheint.

Akku, Laderegler (leuchtet blau) und PV-Zelle am Fenster

Checkliste:

  • ✅ Display ansteuern
  • ✅ WLAN-Verbindung aufbauen
  • ✅ Werte per selbstgebautem UDP-Paket abfragen
  • ✅ Fehler bekommen – Lust verlieren – nach Alternativen suchen
  • ✅ Werte per http Request abfragen
  • ✅ Codeoptimierungen
  • ✅ Akku aus E-Zigarette ausbauen
  • ✅ Spannungsversorgung löten
  • ➡️ Next up: Batterieanzeige auf Display und Einspeisestatus per LED

4. Anzeige des Ladestands und Einspeisestatus-LED

4.1 Lötarbeiten

Im Artikel der Random Nerd Tutorials war auch gleich die Möglichkeit beschrieben, wie man die Akkuspannung messen kann. Warum also nicht den Akkustand auch auf dem Display anzeigen? Dafür musste erstmal was gelötet werden. Dazu sollte noch eine LED und ein Button…

Das will ich genauer wissen. Weiterlesen…

Daher habe ich den Spannungsteiler direkt mit an meine Platine gelötet. Da ich nicht ganz die passenden Widerstände hatte, habe ich mir schnell einen neuen Spannungsteiler berechnet, der sich mit den mir zur Verfügung stehenden Widerständen realisieren lässt.

Infobox Analogeingang und Spannungsteiler

Der Analogeingang des ESP wandelt eine anliegende anliegende Spannung in einen digitalen Wert (10bit, also zwischen 0 und 1023) um. Es gilt zu wissen, welche Spannung dem HIGH-Wert (1023) entspricht. Für meinen Fall war das 3,3V (leider nur zu 50% richtig, wie sich später herausstellt). Bedeutet also, dass am Analogeingang maximal 3,3 V anliegen dürfen.

Ein LiPo-Akku hat allerdings, wenn er voll ist eine Spannung von 4,2V und als leer gilt er, wenn nur noch 3,2V übrig sind. Betrachten wir die maximale Spannung von 4,2V.  Bei dieser Spannung wollen wir, dass 3,3V am Analogpin anliegen. Also müssen wir dafür sorgen, dass davor bereits 0,9V an einem „Vorwiderstand“ abfallen. Es wird also die Gesamtspannung von 4,2V in zwei Spannungen aufgeteilt: 0,9V und 3,3V. Dies erreicht man mit zwei Widerständen in Reihe, welche man mit einer Formel berechnen kann:

Spannungsteiler aus drei Widerständen

Da ich schon dabei war, wollte ich auch gleich eine LED mit anlöten. Jetzt hatte ich allerdings schon den Energiespargedanken im Kopf und ich wollte erstmal testen welchen Vorwiderstand ich benötige, damit die Helligkeit der LED gerade ausreichend ist (oft kann man LEDs mit nur halben Strom betreiben und sie leuchten fast genauso hell, wie mit doppeltem Strom).

mit dem regelbaren Widerstand konnte ich die Helligkeit der LED einstellen. Danach nur noch den Widerstandswert messen.

Einen Knopf habe ich auch gleich mit angelötet. Mit dem will ich später den ESP aus dem Schlaf jederzeit aufwecken können.

Fertiges Lötprodukt mit LED und Button
von vorne sieht es nicht so wild aus 😉

4.2 Programmieren der Ladestandsanzeige

Das war schwieriger als gedacht, da meine zwei ESP ein bisschen anders beschalten sind. Naja, nachdem ich dann die Analogwerte den richtigen Akkuspannungen zuordnen konnte, hat es auch funktioniert.

Das will ich genauer wissen. Weiterlesen…

Der Ausgang des Spannungsteilers ist am Analogeingang des ESP angeschlossen. Den Wert des Analogeingangs kann man im Code auslesen. Er besitzt eine Auflösung von 10 bit, das bedeutet liegt die maximale Spannung (laut Random Nerd Tutorials 3,3V) an, wird der Wert 1023 ausgegeben, liegen 0V an, gibt die Funktion analogRead den Wert 0 zurück. Jetzt musste ich also nur noch die Werte für „Akku voll“ und „Akku leer“ bestimmen. Dazu nahm ich mein zweites ESP8266-Board und baute auf dem Entwicklerboard schnell den Spannungsteiler auf. Als Akkuspannung nahm ich mein regelbares Netzteil und lies mir die aktuellen Werte der analogRead-Funktion direkt auf dem Laptop ausgeben.

Bestimmen der Analogwerte zu bestimmten Spannungen

Werte ermittelt und in den Code eingebracht. 117% Akkustand bei einem Akku der nur etwa 70% voll ist. Da stimmt was nicht. Spannungen nachgemessen, passt alles. Am Analogpin liegen ca, 2,8V, was auch mitten im festgelegen Bereich (0-3,3V) liegt. Nochmal am anderen ESP probiert, da funktioniert es. Verzweiflung. Bin ich doof? Googlen hilft: Der eigentliche ESP8266-Chip arbeitet mit max. 1V am Analogeingang. Bei den meisten Boards auf denen er installiert ist, ist ein weiterer Spannungswandler aufgelötet um 3,3V als Maximalwert zu akzeptieren. Mein Board ohne das Display hat diesen Spannungswandler, das mit dem Display nicht. Also meinen eigenen Spannungswandler so berechnen, dass bei maximaler Akkuspannung von 4,2V nur 1V am Analogpin ankommt.

andere Widerstände am Spannungswandler…

Äquivalente Analogwerte neu bestimmt und siehe da, schon geht’s. Danke für den Stolperstein…

juchuu, ein gültiger Akkustand 🙂

4.3 Einspeisestatus-LED

Das ist schnell erklärt: Die kleine grüne LED neben dem ESP soll 1x blinken, wenn die Einspeisung unter 100W, 2x blinken, wenn unter 200W, 3x unter 300W, usw. Da es mehrere Bereiche gab, dachte ich mir, ich probieren mal die Switchcase-Struktur im Code aus.

Hat auch direkt funktioniert.

Checkliste:

  • ✅ Display ansteuern
  • ✅ WLAN-Verbindung aufbauen
  • ✅ Werte per selbstgebautem UDP-Paket abfragen
  • ✅ Fehler bekommen – Lust verlieren – nach Alternativen suchen
  • ✅ Werte per http Request abfragen
  • ✅ Codeoptimierungen
  • ✅ Akku aus E-Zigarette ausbauen
  • ✅ Spannungsversorgung löten
  • ✅ Ladestand anzeigen
  • ✅ Einspeisestatus-LED umsetzen
  • ➡️ Finale: Energieverbrauch optimieren

5. Tiefschlaf ist angesagt

Der ESP besitzt einen Tiefschlaf-Modus, bei dem er nur etwa 20µA benötigt. Den wollte ich mir nun zu Nutze machen, da der kleine 600mAh-Akku aus der E-Zigarette sonst niemals die Nacht durchhalten würde. Zudem weiß ich ja, dass, wenn es dunkel ist, auch die Einspeisung Null ist.

Wieder gab es ein Top-Tutorial auf Random Nerd Tutorials.

5.1 Reset-Schwierigkeiten

Ich wollte mit dem angelöteten Button den ESP aufwecken. Ich hatte anfangs gedacht, dass es im Code gelöst wird, aber nein, zum Neustart wird der ESP zurückgesetzt. Wie der Resetbutton am PC, daher nochmal umlöten.

Das will ich genauer wissen. Weiterlesen…

Als erstes stellte ich fest, dass ich den Button falsch gelötet hatte. Man kann nicht einfach irgendeinen Digitaleingang zum Reset (also zum Neustart) des ESP nehmen, sondern es muss der Reset-Pin sein. Ja, da hätte man sich denken können, aber ich dachte man kann im Code sicher auch einen Pin festlegen – im Nachhinein ein dämlicher Gedanke. Also habe ich den Button umgelötet. Weitere Unaufmerksamkeit meinerseits: Damit das automatische Aufwachen nach einer bestimmten Zeit funktioniert, muss sich der ESP selbst zurücksetzen können, dafür muss der D0-Pin mit dem Reset-Pin verbunden werden. Das habe ich natürlich gekonnt ignoriert und mich gewundert, warum nix funktioniert… Naja, es war spät am Abend.

Am nächsten Tag habe ich den Fehler schnell gefunden, die Brücke gelötet und schon gings.

jetzt kamen die gelben Verbindungen dazu… Einer, damit sich der ESP beim Start selbst zurücksetzen kann, der zweite für den Button

5.2 Was soll auf dem Display stehen? – NTP Zeitsynchronisierung

Ich stellte fest, dass der Display eingeschaltet bleibt, wenn der ESP in den Schlafmodus geht. Den Vorteil wollte ich nutzen und dort zumindest den letzten Verbrauch und den Akkustand angezeigt lassen.

Dann wäre es natürlich cool, wenn auch dran steht, von welcher Uhrzeit die Daten sind. Naja, wenn der ESP schon WLAN und somit auch Internet hat, wird es nicht so schwer sein die aktuelle Uhrzeit abzufragen. Wieder half – wer hätte es gedacht – Random Nerd Tutorials.

so soll das in etwa aussehen…

5.3 PV-Optimierungen

Trotz des Schlafmodus reichte der Akku nicht. Also beschloss ich mich, mal nachzumessen, wo welche Ströme fließen. Ich stellte fest, dass die zwei PV-Panels, welche ich für die Energieversorgung nutze, hinter der Fensterscheibe (inkl. Fliegengitter) hochgradig ineffizient sind. Bei starker Bewölkung kommen gerade mal 3mA rum. Na das reicht natürlich nicht, wenn der ESP alleine schon ca. 75mA benötigt. Am Fenster ohne Fliegengitter waren es schon 8 mA. Draußen dann ca. 80mA. Aha! Geht doch. Also habe ich die Panels wasserfest gemacht und jetzt außen vor das Fenster gelegt.

5.4 Finale Energieoptimierung

Zudem geht der ESP jetzt in den Tiefschlaf, sobald die PV-Produktion für 30 Sekunden auf unter 100W fällt (starke Bewölkung) oder der Akkustand unter 50% liegt. Nach einer Stunde wacht er automatisch wieder auf und evtl. beginnen die 30 Sekunden von vorne. Somit steht auf dem Display immer ein max. eine Stunde alter Wert des Energiebedarfs und Akkustands.

Im Tiefschlaf fließen mit eingeschaltetem Display noch etwa 6,4mA. Daher habe ich beschlossen doch nachts (zwischen 0 und 5 Uhr) das Display ganz auszuschalten, schließlich bin ich von den 20µA noch um den Faktor 320 weg. Das brachte mäßigen Erfolg. Statt den 6,4mA sind es bei ausgeschaltetem Display 2,4mA. Klar nochmal mehr als halbiert, aber immer noch über das 100-fache vom erhofften Wert. 

Eine kurze Recherche lieferte das Ergebnis: Der ESP (auf den Bildern der kleine silberne Chip mit der WLAN-Antenne links) benötigt wirklich nur die 20µA, der ganze Rest geht für den USB zu Seriell-Adapter drauf, der ja auch noch auf dem Board ist…. Auf den könnte ich verzichten, indem ich mir einen „nackten“ ESP-Chip kaufe und diesen mit externem USBtoSerial-Adapter programmiere. Das würde das Projekt maximal energieeffizent machen, aber jetzt gucke ich mir erstmal an, wie sich der Akku in Verbindung mit dem recht „aggressiven“ Energiesparmodus so schlägt.

Fazit nach ca. 2 Wochen: Auch am Ende eines bewölkten Tages hat der Akku noch gut 70%, reicht also locker aus 🙂

Checkliste:

  • ✅ Display ansteuern
  • ✅ WLAN-Verbindung aufbauen
  • ✅ Werte per selbstgebautem UDP-Paket abfragen
  • ✅ Fehler bekommen – Lust verlieren – nach Alternativen suchen
  • ✅ Werte per http Request abfragen
  • ✅ Codeoptimierungen
  • ✅ Akku aus E-Zigarette ausbauen
  • ✅ Spannungsversorgung löten
  • ✅ Ladestand anzeigen
  • ✅ Einspeisestatus-LED umsetzen
  • ✅ Tiefschlaf umsetzen
  • ✅ Energieoptimierungen
  • ➡️ Gehäuse designen und 3D-drucken
  • ➡️ Zweite Displayseite mit Tagesgesamtverbrauch und -erzeugung

6. Schaltbild und vollständiger Code

6.1 Schaltbild

Naja, ich habe jetzt keine Lust mehr ein fertiges Schaltbild zu zeichnen, im Prinzip gibt’s die alle in den verlinkten Random Nerd Tutorials. Aber hier gibt’s ein beschriftetes Foto meiner Bauteile:

6.2 Vollständiger Code

Obacht: ich bin kein Softwareentwickler und das ist definitiv das Aufwändigste, was ich bisher so gemacht habe. Es gibt also sicherlich einiges noch an Optimierungsvarianten.

/*
Code by mrneveroff - neveroff.de
u8g2 I2C Display Dokumentation: https://github.com/olikraus/u8g2/wiki/u8g2reference
Arduino C++ Code Dokumentation: https://www.arduino.cc/reference/en/
Shelly Doku (Link für EnergyMeter, z.b. das Pro 3EM): https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/EM/
PV-Selbstversorgung des ESP: https://randomnerdtutorials.com/power-esp32-esp8266-solar-panels-battery-level-monitoring/
Deep Sleep Beschaltung und Infos: https://randomnerdtutorials.com/esp8266-deep-sleep-with-arduino-ide/
GitHub der NTPClients: https://github.com/arduino-libraries/NTPClient
NTP Zeitsynchro: https://randomnerdtutorials.com/esp8266-nodemcu-date-time-ntp-client-server-arduino/
*/

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <U8g2lib.h>
#include <NTPClient.h>
#include <WiFiUdp.h>

//Taster und LED Initialisierung
int LED = D1; //LED die leutet, wenn eingespeist wird

//WiFi Konfig
const char* ssid = "Internetschnorrer";
const char* password = "EinBloedesPasswort";
IPAddress local_IP(192, 168, 189, 200);
IPAddress gateway(192, 168, 189, 1);
IPAddress subnet(255, 255, 255, 0);
IPAddress primaryDNS(192, 168, 189, 1); // optional

//NTP Konfig
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org");

//Variableninitialisierung
bool testmode = false; //Um einzelne Funktionen im Code zu testen
String totalActPower; //Gesamtwert des Shelly Pro 3EM (Leistung)
double totpwr; //Gesamtwert als Double
String apower; //Aktueller Wert des Shelly PM mini in der Einspeisedose des BKW (Leistung)
double PVpwr; //Erzeugungswert (apower) als Double
double totusage; //Summe des Gesamtverbrauchs der Wohnung
int batteryLevel; //Akkustand in Prozent
int lowpowercounter; //Zähler, der hochzählt, wenn nur geringe Einspeisung und niedriger Akkustand um rechtzeitig in den deepsleep zu schalten

//OLED-Display Initialisierung
U8G2_SSD1306_128X64_NONAME_F_SW_I2C
u8g2(U8G2_R0,/*clock=*/14,/*data=*/12,U8X8_PIN_NONE); //U8G2_R0 gibt Displayorientierung an; GPIO14 für I2C Clock und GPIO12 für I2C Data

//Funktion die die LED mit übergebener Anzahl blinken lässt
void blink(int count){
  for(int i=0;i<count;i++){
    digitalWrite(LED,HIGH);
    delay(100);
    digitalWrite(LED,LOW);
    //Serial.println(i);
    delay(300);    
  }
}

//Funktion die Double-Wert umwandelt in Char und dann an gegebener Stelle in den Display Buffer schreibt
void writeDisplayBuffer(double wert,int x, int y){
  char powerStr[10];
  dtostrf(wert,5,1,powerStr); //double in char* umwandeln, max. 5 stellen, 1 nachkommastelle
  u8g2.drawStr(x,y,powerStr);
}

//wie oben, nur ohne Nachkommastellen
void writeDisplayBufferInt(double wert,int x, int y){
  char powerStr[10];
  dtostrf(wert,5,0,powerStr); //double in char* umwandeln, max. 5 stellen, 0 nachkommastellen
  u8g2.drawStr(x,y,powerStr);
}

void setup() {
  //Serial.begin(921600);
  //delay(10);
  //Serial.print("test");
  pinMode(LED,OUTPUT);
  pinMode(A0, INPUT);
  blink(1);
  u8g2.begin();
  u8g2.setFont(u8g2_font_mercutio_basic_nbp_tf);
  u8g2.drawStr(0,10,"Initialisierung");
  u8g2.drawStr(0,26,"Verbinde mit WLAN:");
  u8g2.drawStr(0,40,ssid);
  u8g2.sendBuffer();

  //Initialisierung, damit hochgezählt und später die IP angezeigt werden kann
  int i=0;
  char b[16];
  String str;

  //Wifi Config setzen und Verbindung aufbauen
  WiFi.config(local_IP, gateway, subnet, primaryDNS);
  WiFi.begin(ssid,password);
  while(WiFi.status() != WL_CONNECTED){
    u8g2.drawStr(0,54,"warten...");
    str=String(i);
    str.toCharArray(b,50);
    u8g2.drawStr(70,54, b);
    u8g2.sendBuffer();
    i++;
    delay(1000);
  }
  timeClient.begin();
  timeClient.setTimeOffset(7200); //UTC+2 = CEST = Sommerzeit
  //timeClient.setTimeOffset(3600); //UTC+1 = CET = Winterzeit

  //IP-Adresse auf dem Display anzeigen
  u8g2.clearBuffer();
  u8g2.drawStr(0,10,"Verbunden mit SSID:");
  u8g2.drawStr(0,26,ssid);
  //u8g2.drawStr(0,38,"___________________");
  u8g2.drawStr(0,42,"IP-Adresse:");
  str=WiFi.localIP().toString();
  str.toCharArray(b, 50);
  u8g2.drawStr(0,56,b);
  u8g2.sendBuffer();
  blink(2);
  delay(1360);
}

void loop() {
  batteryLevel = map(analogRead(0), 327, 435, 0, 100); //Aktuellen Akkustand auslesen; 327 = 3,3V; 435 = 4,2V Akkuspannung über Spannungsteiler; max. 1V am A0 Pin
  //batteryLevel = analogRead(0); //Um Analogwert direkt auszugeben; hierfür Zeile davor und Zeile mit "%" auskommentieren
  u8g2.clearBuffer();
  u8g2.drawStr(0,10,"Status:");
  u8g2.drawStr(78,10,"Bat:");
  writeDisplayBufferInt(batteryLevel,98,10); //INT funktion macht keine Nachkommastellen
  u8g2.drawStr(120,10,"%");
  u8g2.drawStr(0,26,"Erzeugung:");
  u8g2.drawStr(116,26,"W");
  u8g2.drawStr(0,40,"Verbrauch:");
  u8g2.drawStr(116,40,"W");
  u8g2.drawStr(0,47,"__________________");
  u8g2.drawStr(116,61,"W");
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    WiFiClient client;

    // Anfrage an das Shelly PRO 3EM im Sicherungskasten
    http.begin(client,"http://192.168.189.89/rpc/EM.GetStatus?id=0");
    int httpCode = http.GET();

    if (httpCode > 0) {
      String payload = http.getString();
      //Serial.println(payload);
      int startIndex = payload.indexOf("total_act_power") + 17;
      int endIndex = payload.indexOf(",", startIndex);
      totalActPower = payload.substring(startIndex, endIndex);
      //Serial.print("Gesamt:");
      //Serial.println(totalActPower);
    }
    http.end();

    // Anfrage an das Shelly PM mini in der Einspeisedose
    http.begin(client,"http://192.168.189.5/rpc/PM1.GetStatus?id=0");
    httpCode = http.GET();

    if (httpCode > 0) {
      String payload = http.getString();
      int startIndex = payload.indexOf("apower") + 9;
      int endIndex = payload.indexOf(",", startIndex);
      apower = payload.substring(startIndex, endIndex);
      //Serial.print("Erzeugung:");
      //Serial.println(apower);
    }
    http.end();
  }
  //u8g2.drawStr(87,26,apower.c_str()); //Darstellen aktuelle Erzeugung
  PVpwr = apower.toDouble(); //umwandlen des Strings zu einem Double-Wert,
  writeDisplayBuffer(PVpwr,81,26); //wäre nicht nötig, siehe auskommentierter u8g2.drawStr oben, allerdings sind so alle drei Werte untereinander zentriert
  totpwr = totalActPower.toDouble(); //same here
  if(totpwr>0){
    digitalWrite(LED,LOW); //Sicherstellen, dass Einspeise-LED aus
    u8g2.drawStr(0,61,"Netzbezug:");
    writeDisplayBuffer(totpwr,81,61);
    totusage = PVpwr + totpwr; //wenn Energie aus dem Netz genommen wird, ergibt sich die Gesamtleistung aus der Summe der aktuellen Erzeugung und dem Netzbezug
    writeDisplayBuffer(totusage,81,40);
  }else{
    u8g2.drawStr(0,61,"Einspeisung:");
    totpwr = totpwr * -1; //Vorzeichen umdrehen, weil Einspeisung als negativer Verbrauch angezeigt wird
    writeDisplayBuffer(totpwr,81,61);
    totusage = PVpwr - totpwr; //wenn ins Netz eingespeist wird, dann ist die Differenz aus erzeugter Energie und der eingespeisten Energie, die aktuelle Leistung der Wohnung
    writeDisplayBuffer(totusage,81,40);
    int totpwrint = floor(totpwr); //Nachkommastellen von totpwr ignorieren und in ein int speichern 
    switch(totpwrint){
      case 0 ... 99:
        blink(1);
        break;
      case 100 ... 199:
        blink(2);
        break;
      case 200 ... 299:
        blink(3);
        break;
      case 300 ... 399:
        blink(4);
        break;
      case 400 ... 499:
        blink(5);
        break;
      case 500 ... 599:
        blink(6);
        break;
      case 600 ... 900:
        digitalWrite(LED,HIGH);
        break;
    }
  }
  u8g2.sendBuffer();
  delay(1000);
  if (testmode==true){
    PVpwr=999;
    batteryLevel=69; //verhindert den Sleepmodus
  }
  //Abfrage ob geringe PV-Produktion und niedriger Akkustand, wenn ja, dann gute Nacht (Normalbetrieb ca, 74mA)
  if (PVpwr<100 || batteryLevel<50){
    lowpowercounter++;
    if (lowpowercounter>30){
      u8g2.clearBuffer();
      timeClient.update();
      int hour = timeClient.getHours();
      if(hour>=0 && hour<=5){ //Nachts soll der Display ganz aus sein (ca. 2,4mA)
        //u8g2.drawStr(0,10,".");
        //u8g2.sendBuffer();
        //delay(2000);
        u8g2.setPowerSave(1); //Display ganz ausschalten
        delay(100);
        ESP.deepSleep(7200e6); //deepsleep für 5 Stunden
      }else{
        u8g2.drawStr(19,12,"SLEEPING");
        u8g2.drawStr(78,12,"Bat:");
        writeDisplayBufferInt(batteryLevel,98,12); //INT funktion macht keine Nachkommastellen
        u8g2.drawStr(120,12,"%");
        u8g2.drawStr(0,28,"Verbrauch:");
        writeDisplayBuffer(totpwr,81,28);
        u8g2.drawStr(116,28,"W");
        u8g2.drawStr(0,43,"Stand:");
        String formattedTime = timeClient.getFormattedTime();
        u8g2.drawStr(37,43,formattedTime.c_str());
        u8g2.drawStr(80,43,"Uhr");
        u8g2.drawStr(0,61,"push button to wake up");
        u8g2.setFont(u8g2_font_unifont_t_weather);
        u8g2.drawGlyph(0, 12, 0x0029); //Mond https://github.com/olikraus/u8g2/wiki/fnticons 
        u8g2.sendBuffer();
        delay(100);
        ESP.deepSleep(3600e6); //deepsleep für 1 Stunde, benötigt ca. 6,4mA
      }
    }
  }else{
    lowpowercounter = 0;
  }
}

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.