Vom Arduino in die MySql Datenbank


Allgemeine Informationen

Auf dieser Seite zeigen wir euch, wie wir die Daten, die wir über die D0-Schnittstelle (SML) vom Stromzähler erhalten, mit dem Mikrocontroller einlesen, umwandeln, verarbeiten und schließlich in einer Datenbank speichern.

Inhaltsverzeichnis

Die Hardware

Hier dreht sich alles um die ersten Schritte zum Auslesen und Verstehen des SML-Protokolls deines Stromzählers – mit einfacher Hardware und einem Mikrocontroller wie dem Arduino.

Was brauchst du?
Für den Einstieg benötigst du im Grunde drei Dinge:

  • Einen SML-fähigen Stromzähler
    Moderne Stromzähler unterstützen das SML-Protokoll (Smart Message Language), über das sie ihre Messwerte seriell übertragen.
  • Einen Mikrocontroller (z.B. Arduino)
    Dieser dient als Schnittstelle, um die vom Stromzähler kommenden Daten zu empfangen und auszuwerten.
  • Einen Lesekopf (Infrarot-Sensor oder optische Schnittstelle)
    Der Lesekopf wird direkt auf den IR-Ausgang deines Stromzählers gesetzt und verbindet sich mit dem Mikrocontroller.
Hardwareaufbau
Der Lesekopf wird fest auf den IR-Port des Stromzählers angebracht und mit dem Mikrocontroller verbunden. Wie genau das aussieht, zeigt unser Schaltplan weiter unten. Dort findest du auch Tipps zum richtigen Anschluss und zur Spannungsversorgung.

Platzierung und Installation
Ob du den Mikrocontroller direkt im Zählerschrank, im Sicherungskasten oder extern anbringst, ist flexibel und hängt von deinem individuellen Setup ab. Wichtig ist nur, dass die Verbindung zwischen Lesekopf und Zähler stabil bleibt und die Verkabelung sicher erfolgt.

Stromzähler

TYP: ISKRA MT691

Mikrocontroller

Typ: Wemos D1 Mini Pro

Platine

Kleine Platine mit Anschlüsse für den Lesekopf und 2 Status Led`s

IR Lesekopf

Typ: Hichi IR v1.1

Die MYSQL Datenbank

Die Daten, die der Mikrocontroller ermittelt, werden in einer MySQL-Datenbank gespeichert. Dies bietet mehrere Vorteile:
  1. Zuverlässige Speicherung: Eine Datenbank gewährleistet, dass die erfassten Daten sicher und dauerhaft gespeichert werden, ohne dass sie durch Systemausfälle oder Neustarts verloren gehen.
  2. Einfache Abfrage und Analyse: Mit einer relationalen Datenbank wie MySQL können die Daten effizient abgefragt und analysiert werden. Dadurch lassen sich Muster, Trends und Anomalien leicht erkennen.
  3. Skalierbarkeit: MySQL bietet eine hohe Skalierbarkeit, sodass auch bei steigender Datenmenge die Performance nicht beeinträchtigt wird.
  4. Zugriffskontrolle und Sicherheit: Die Datenbank ermöglicht eine gezielte Verwaltung von Benutzerrechten, sodass nur berechtigte Personen auf sensible Informationen zugreifen können.
  5. Datenintegrität: MySQL sorgt dafür, dass die Daten konsistent und korrekt bleiben, was für die langfristige Nutzung und Auswertung der Daten entscheidend ist."

Datenbankstruktur und Speicherung von Verbrauchs- und Einspeisewerten

Die Daten werden in zwei zentralen Tabellen gespeichert, um eine präzise und effiziente Verwaltung der Energieverbrauchs- und Einspeisewerte zu gewährleisten:

Tabelle `obis_180_280`

obis_180_280: Diese Tabelle speichert die täglichen Verbrauchs- und Einspeisewerte in Kilowattstunden (kWh). Sie dient der langfristigen Erfassung und Analyse des Energieverbrauchs und der Einspeisung über den Tagesverlauf.

Tabelle `obis_10070`

obis_10070: Hier werden die aktuellen Verbrauchs- und Einspeisewerte in Watt gespeichert. Diese Tabelle ermöglicht eine Echtzeitüberwachung des Energieflusses und bietet eine detaillierte Ansicht der momentanen Verbräuche und Einspeisungen.

Zweck und Nutzung der Daten

Tageswerte (in kWh) aus der Tabelle obis_180_280 sind besonders nützlich für langfristige Auswertungen, etwa für Monats- oder Jahresberichte. Echtzeitdaten (in Watt) aus der Tabelle obis_10070 ermöglichen eine sofortige Rückmeldung über den aktuellen Energieverbrauch und die Einspeisung, was besonders für die Optimierung der Energieerzeugung und -nutzung von Bedeutung ist. Beide Tabellen arbeiten Hand in Hand, um eine umfassende und flexible Analyse der Energieflüsse zu ermöglichen.

Arduino Quellcode

Den Arduino-Code können Sie direkt kopieren und in Ihre Arduino IDE einfügen. Bitte beachten Sie, dass Sie an insgesamt 5 Stellen Ihre persönlichen Daten wie WLAN-SSID, WLAN-Passwort, Serveradresse und Zugangspasswort eintragen müssen. Diese Stellen sind im Code durch ** YOUR VALUE ** gekennzeichnet und müssen individuell angepasst werden, damit die Verbindung und Datenübertragung korrekt funktioniert.

			// WIFI-Konfiguration und Verbindungsaufbau
	#include <ESP8266WiFi.h>
	#include <WiFiClientSecure.h>
	const char* ssid = "**YOUR SSID**"; // SSID des WLANs
	const char* password = "**YOUR PASSWORD**"; // Passwort des WLANs
	const char* host = "**YOUR DOMAIN**"; // Serveradresse zum Hochladen der Daten
	const int httpsPort = 443; // HTTPS-Port für verschlüsselte Verbindung 
// Serielle Schnittstelle (SoftwareSerial) für den IR-Kopf am Zähler
	#include <SoftwareSerial.h>
	SoftwareSerial SmlSerial(4, 5); // RX = GPIO4, TX = GPIO5 (nur RX wird verwendet)
// SML-Protokoll-Konstanten und OBIS-Kennungen (Kennzahlen für Zählerdaten)
	byte bytesmlread; // Zwischenspeicher für eingelesene Bytes
	const byte byt_Start_Seq[] = { 0x1B, 0x1B, 0x1B, 0x1B, 0x01, 0x01, 0x01, 0x01 }; // Startsequenz eines SML-Telegramms
	const byte byt_Stop_Seq[]  = { 0x1B, 0x1B, 0x1B, 0x1B, 0x1A }; // Endsequenz eines SML-Telegramms
	// OBIS-Kennungen zur Erkennung bestimmter Messwerte:
	const byte byt_180_Seq[] = { 0x77, 0x07, 0x01, 0x00, 0x01, 0x08, 0x00, 0xFF, 0x65, 0x00, 0x1C, 0x49, 0x04, 0x01, 0x62, 0x1E, 0x52, 0xFF}; // OBIS 1.8.0 (Bezug in kWh)
	const byte byt_280_Seq[] = { 0x77, 0x07, 0x01, 0x00, 0x02, 0x08, 0x00, 0xFF, 0x01, 0x01, 0x62, 0x1E, 0x52, 0xFF }; // OBIS 2.8.0 (Einspeisung in kWh) 
	const byte byt_10070_Seq[] = { 0x77, 0x07, 0x01, 0x00, 0x10, 0x07, 0x00, 0xFF, 0x01, 0x01, 0x62, 0x1B, 0x52, 0x00 }; // OBIS 10.7.0 (aktuelle Wirkleistung in Watt -Einspeisung/+Bezug)
// Variablen zur Ablaufsteuerung ("Zustandsmaschine")
	uint8_t SequenceStep = 1; // Aktueller Sequenzschritt
	uint8_t SequenceIndex = 0; // Index zum Abgleich der Bytes in Sequenzen
	uint8_t intCountByteVal[3] = {0, 0, 0}; // Anzahl der relevanten Bytes für OBIS 1.8.0, 2.8.0, 10.7.0
	uint8_t intAktLoopCount = 0; // Aktuelle Anzahl abgeschlossener Lesezyklen
	uint8_t intSollLoopCount = 60; // Anzahl Lesezyklen, bevor ein Upload erfolgt (~1 Zyklus/Sekunde)
// Puffervariablen zum Speichern der Zählerwerte (HEX und dezimal)
	byte byt_180_HEX[4]; // Zählerwert 1.8.0 (Bezug) in Hex
	byte byt_280_HEX[4]; // Zählerwert 2.8.0 (Einspeisung) in Hex
	byte byt_10070_HEX[4]; // Aktuelle Leistung (OBIS 10.7.0) in Hex
	double dbl_180toDB = 0; // Zählerwert 1.8.0 (Bezug) in Dezimal (kWh)
	double dbl_280toDB = 0; // Zählerwert 2.8.0 (Einspeisung) in Dezimal (kWh)
	double dbl_10070toDB = 0; // Leistung OBIS 10.7.0 in Dezimal (Watt)
//Led`s
	const int LedErrorPin = 12; //LED Error Pin
	const int LedOkPin = 13;  //LED OK Pin
	
void setup() {
	//Serial.begin(115200); // Serielle Ausgabe zur Debug-Ausgabe aktivieren
	WiFi.begin(ssid, password); // WLAN-Verbindung aufbauen
    WiFi.setAutoReconnect(true); // Automatische WLAN-Wiederverbindung
    WiFi.persistent(false); // Vermeidet Flash-Verschleiß	
	SmlSerial.begin(9600); // SML-Datenübertragung starten (IR-Kopf am Zähler)
    pinMode(LedErrorPin, OUTPUT); // Setzt den LedErrorPin als Ausgang
    pinMode(LedOkPin, OUTPUT); // Setzt den LedOkPin als Ausgang
}

void loop() {
  //Fehlerausgabe wenn Step zu lange aktiv
  ErrorCodeLed(SequenceStep);

	// Schritt 1: Prüfen, ob WLAN verbunden ist
	if ((SequenceStep == 1) && (WiFi.status() == WL_CONNECTED)) {
		SequenceStep ++; // Weiter zur nächsten Sequenz 1-->2
		digitalWrite(LedOkPin, HIGH); //LED Prozess SML Running OK
	}
	
	// Schritt 2-12: SML-Daten auslesen und OBIS-Werte extrahieren
	if (SmlSerial.available()) { //wenn Daten vorhanden
		bytesmlread = SmlSerial.read(); //Temp-Speicher der SML Nachricht
		switch (SequenceStep) {
			// Schritt 2: Prüfen auf Startsequenz
			case 2: 
				if (bytesmlread == byt_Start_Seq[SequenceIndex]) {
					SequenceIndex ++;
					if (SequenceIndex == sizeof(byt_Start_Seq)) {
						SequenceIndex = 0;
						if (intAktLoopCount < intSollLoopCount-1) {
							SequenceStep = 9;
						}else{
							SequenceStep ++; // Weiter zur nächsten Sequenz 2-->3
						}
					}
				} else {
					SequenceIndex = 0;
				}
			break;
			// Schritt 3–5: OBIS 1.8.0 (Bezug)
			case 3: // Suche Sequenz für Zählerwert OBIS 1.8.0
				if ((bytesmlread == byt_180_Seq[SequenceIndex])||SequenceIndex == 11) { //Sequenz Index 12 wird Übersprungen byte nicht Konstant
					SequenceIndex ++;
					if (SequenceIndex == sizeof(byt_180_Seq)) {
						SequenceIndex = 0;
						bytesmlread = 0; //Leeren um sicherzustellen, dass das nächste Byte empfangen wurde
						SequenceStep ++; // Weiter zur nächsten Sequenz 3-->4
					}
				} else {
					SequenceIndex = 0;
				}
			break;
			case 4: // Ermittle die Anzahl der Byte für Zählerwert OBIS 1.8.0
				if (bytesmlread != -1) {
					intCountByteVal[0] = rechteHexZiffer(bytesmlread)-1;
					SequenceStep ++; // Weiter zur nächsten Sequenz 4-->5
				}
			break;
			case 5: // Zählerwert 1.8.0 speichern
				byt_180_HEX[SequenceIndex] = bytesmlread;
				SequenceIndex ++;
				if (SequenceIndex == intCountByteVal[0]) {
					SequenceIndex = 0;
					SequenceStep ++; // Weiter zur nächsten Sequenz 5-->6
				}
			break;
			// Schritt 6–8: OBIS 2.8.0 (Einspeisung)
			case 6: // Suche Sequenz für Zählerwert OBIS 2.8.0
				if (bytesmlread == byt_280_Seq[SequenceIndex]) {
					SequenceIndex ++;
					if (SequenceIndex == sizeof(byt_280_Seq)) {
						SequenceIndex = 0;
						bytesmlread = 0; //Leeren um sicherzustellen, dass das nächste Byte empfangen wurde
						SequenceStep ++; // Weiter zur nächsten Sequenz 6-->7
					}
				} else {
					SequenceIndex = 0;
				}
			break;
			case 7: // Ermittle die Anzahl der Byte für Zählerwert OBIS 2.8.0
				if (bytesmlread != -1) {
					intCountByteVal[1] = rechteHexZiffer(bytesmlread)-1;
					SequenceStep ++; // Weiter zur nächsten Sequenz 7-->8
				}
			break;
			case 8: // Zählerwert 2.8.0 Speichern
				byt_280_HEX[SequenceIndex] = bytesmlread;
				SequenceIndex ++;
				if (SequenceIndex == intCountByteVal[1]) {
					SequenceIndex = 0;
					SequenceStep ++; // Weiter zur nächsten Sequenz 8-->9
				}
			break;
			// Schritt 9–11: OBIS 10.7.0 (aktuelle Wirkleistung)
			case 9: // Suche Sequenz für Zählerwert OBIS 10.7.0
				if (bytesmlread == byt_10070_Seq[SequenceIndex]) {
					SequenceIndex ++;
					if (SequenceIndex == sizeof(byt_10070_Seq)) {
						SequenceIndex = 0;
						bytesmlread = 0; //Leeren um sicherzustellen, dass das nächste Byte empfangen wurde
						SequenceStep ++; // Weiter zur nächsten Sequenz 9-->10
					}
				} else {
						SequenceIndex = 0;
				}
			break;
			case 10: // Ermittle die Anzahl der Byte für Zählerwert OBIS 10.7.0
				if (bytesmlread != -1) {
					intCountByteVal[2] = rechteHexZiffer(bytesmlread)-1;
					SequenceStep ++; // Weiter zur nächsten Sequenz 10-->11
				}
			break;
			case 11: // Zählerwert 10.7.0 speichern
				byt_10070_HEX[SequenceIndex] = bytesmlread;
				SequenceIndex ++;
				if (SequenceIndex == intCountByteVal[2]) {
					SequenceIndex = 0;
					SequenceStep ++; // Weiter zur nächsten Sequenz 11-->12
				}
			break;
			case 12: // Suche Stop-Sequence
				if (bytesmlread == byt_Stop_Seq[SequenceIndex]) {
					SequenceIndex ++;
					if (SequenceIndex == sizeof(byt_Stop_Seq)) {
						SequenceIndex = 0;
						SequenceStep ++; // Weiter zur nächsten Sequenz 12-->13
					}
				} else {
					SequenceIndex = 0;
				}
			break;
		}
	}

	//Werte Übermitteln
	if (SequenceStep == 13){
		dbl_10070toDB += parseWattValue(byt_10070_HEX, intCountByteVal[2]);
		intAktLoopCount ++;
		if (intAktLoopCount == intSollLoopCount){
			digitalWrite(LedOkPin, LOW); //LED Prozess Datensammlung abgeschlossen
			//Werte zusammenstellen
			dbl_10070toDB /= intAktLoopCount;
			dbl_10070toDB = roundToDecimalPlaces(dbl_10070toDB,2);
			dbl_180toDB = parseUnsignedValue(byt_180_HEX,intCountByteVal[0]);
			dbl_180toDB /= 10000;
			dbl_180toDB = roundToDecimalPlaces(dbl_180toDB,3);
			dbl_280toDB = parseUnsignedValue(byt_280_HEX,intCountByteVal[1]);
			dbl_280toDB /= 10000;
			dbl_280toDB = roundToDecimalPlaces(dbl_280toDB,3);
			// Use WiFiClientSecure class to create TLS connection
			WiFiClientSecure client;
			client.setInsecure();
			if (client.connect(host, httpsPort)) {
				client.print(String("GET ") + "/**URL TO YOUR PHP**.php?PW=**YOUR SALT PASSWORD**" +
										"&P180=" + String(dbl_180toDB) +
										"&P280=" + String(dbl_280toDB) +
										"&P10070=" + String(dbl_10070toDB) +
										" HTTP/1.1\r\n" +
										"Host: " + host + "\r\n" +
										"User-Agent: BuildFailureDetectorESP8266\r\n" +
										"Connection: close\r\n\r\n");
				// Warten bis alles übertragen wurde (Server hat geantwortet)
				while(client.connected() && !client.available()) {
						yield();  // Watchdog füttern
				}
    		client.stop();  // Verbindung sauber schließen
			}
			// Speicher Leeren für neue Werte
			intAktLoopCount = 0;
			dbl_180toDB = 0;
			dbl_280toDB = 0;
			dbl_10070toDB = 0;
		}
		memset(intCountByteVal, 0, sizeof(intCountByteVal)); // Reset Array relevanten Bytes
		SequenceStep = 1; // Sequenz vollständig (Neustart)
	}
}

//Funktionen
	/*Parse 10.07.0 aktuelle Wirkleistung Bezug/Einspeisung */
    long parseWattValue(byte* data, int length) {
      if (length < 1 || length > 4) return 0; // Ungültige Länge
      long result = 0;

      // Bytes zusammenfügen (Big Endian)
      for (int i = 0; i < length; i++) {
        result <<= 8;
        result |= data[i];
      }

      // Prüfen, ob der Wert negativ ist (höchstes Bit des ersten Bytes gesetzt?)
      if (data[0] & 0x80) {
        // Zweierkomplement-Sign-Erweiterung
        // Anzahl der ungenutzten oberen Bits (32 - length*8)
        int shift = (4 - length) * 8;
        result = (result << shift) >> shift; // Sign-Erweiterung durch Links- und Rechtsschieben
      }
      return result;
    }

	/*Parse Zählerstände 1.8.0 und 2.8.0 (immer positiv)*/
    unsigned long parseUnsignedValue(byte* data, int length) {
      if (length < 1 || length > 4) return 0;
      unsigned long result = 0;
      for (int i = 0; i < length; i++) {
        result <<= 8;
        result |= data[i];
      }
      return result;
    }

	/*Rückgabe rechte Hex Ziffer als Integer*/
    byte rechteHexZiffer(byte hexWert) {
      return hexWert & 0x0F;
    }

	/*Zahlen Runden auf Nachkommastellen*/
	double roundToDecimalPlaces(double value, int decimalPlaces) {
		double scalingFactor = pow(10.0, decimalPlaces);
		return round(value * scalingFactor) / scalingFactor;
	}

	/*Error Code durch LED ausgeben*/
	void ErrorCodeLed(int intSmlStep) {
		uint32_t static previousMillis = millis(); // speichert wie viele Sekunden seit der letzten Änderung vergangen sind
		uint32_t static interval = 5000; // Interval zwischen zwei Änderungen
		boolean static value = LOW; // Startwert der LED
		int static intStep = 0; //Aktueller Schritt
		int static intStepOld; // Alter Schritt speichern
		int static BlinkCount = 0; //Speicher Anzahl aktuell geblinkt

		if (intStep < 2) {
			if (intSmlStep == intStepOld) {
				intStep = 1;
			} else {
				intStepOld = intSmlStep;
				intStep = 0;
				previousMillis = millis();
			}
		}

		if (intStep == 1) {
			if (millis() - previousMillis > interval) {
				previousMillis = millis();   // aktuelle Zeit abspeichern
				intStep = 2;
				interval = 500;
			}
		}

		if (intStep == 2) {
			if (millis() - previousMillis > interval) {
				previousMillis = millis();   // aktuelle Zeit abspeichern
				value = !value; // LED Zustand wecheln.
				digitalWrite(LedErrorPin, value);// Wert auf den Ausgang schreiben
				BlinkCount ++;
				if (BlinkCount == intStepOld * 2) {
					intStep = 0;
					interval = 5000;
					previousMillis = millis();
					BlinkCount = 0;
				}
			}
		}
	}
			
		

Technische Beschreibung des Stromzähler-Arduino-Programms

Dieses ESP8266-Programm liest über einen IR-Kopf die SML-Daten eines digitalen Stromzählers aus und überträgt die Messwerte per HTTPS an einen Webserver. Die Steuerung erfolgt über eine Zustandsmaschine mit 13 Sequenzschritten. Dabei wird die aktuelle Wirkleistung (OBIS 10.7.0) kontinuierlich erfasst, während die Zählerstände (OBIS 1.8.0 und 2.8.0) nur einmal pro Upload-Zyklus verarbeitet werden. Dies reduziert die Datenmenge und erhöht die Effizienz.

Sequenzschritte im Detail

  1. Schritt 1 – WLAN-Verbindung prüfen:
    Der ESP8266 prüft, ob eine Verbindung zum WLAN besteht. Bei erfolgreicher Verbindung wird die OK-LED aktiviert und zur nächsten Sequenz übergegangen.
  2. Schritt 2 – Startsequenz erkennen:
    Ein gültiges SML-Telegramm beginnt mit einer festen Bytefolge. Diese wird Byte für Byte abgeglichen.
    Wichtig: Wenn der Upload-Zähler (intAktLoopCount) noch nicht erreicht ist, wird direkt zu Schritt 9 gesprungen – die Zählerstände 1.8.0 und 2.8.0 werden übersprungen und nur die aktuelle Leistung (10.7.0) erfasst.
  3. Schritt 3 – OBIS 1.8.0 Kennung erkennen:
    Die Bytefolge zur Kennung des Strombezugs (OBIS 1.8.0) wird Byte für Byte abgeglichen. Ein Byte innerhalb der Sequenz ist variabel und wird übersprungen, um die Erkennung robuster zu machen.
  4. Schritt 4 – Byteanzahl für OBIS 1.8.0 bestimmen:
    Das nächste empfangene Byte enthält die Länge der folgenden Datenbytes. Die rechte Hex-Ziffer wird extrahiert und um 1 reduziert.
  5. Schritt 5 – Zählerwert OBIS 1.8.0 speichern:
    Die ermittelten Bytes werden in einem Array gespeichert. Sobald alle Bytes empfangen wurden, wird zur nächsten Sequenz übergegangen.
  6. Schritt 6 – OBIS 2.8.0 Kennung erkennen:
    Die Bytefolge zur Kennung der Einspeisung (OBIS 2.8.0) wird Byte für Byte abgeglichen. Bei vollständiger Übereinstimmung wird zur Byteanzahlbestimmung übergegangen.
  7. Schritt 7 – Byteanzahl für OBIS 2.8.0 bestimmen:
    Wie bei Schritt 4 wird die Anzahl der folgenden Datenbytes ermittelt.
  8. Schritt 8 – Zählerwert OBIS 2.8.0 speichern:
    Die empfangenen Bytes werden gespeichert. Nach Abschluss wird zur nächsten Sequenz übergegangen.
  9. Schritt 9 – OBIS 10.7.0 Kennung erkennen:
    Die Bytefolge zur Kennung der aktuellen Wirkleistung (OBIS 10.7.0) wird Byte für Byte abgeglichen. Diese Kennung kann sowohl positive als auch negative Werte enthalten.
  10. Schritt 10 – Byteanzahl für OBIS 10.7.0 bestimmen:
    Die Anzahl der folgenden Datenbytes wird ermittelt. Da hier auch negative Werte möglich sind, erfolgt später eine Zweierkomplement-Auswertung.
  11. Schritt 11 – Leistungswert OBIS 10.7.0 speichern:
    Die empfangenen Bytes werden gespeichert und für die spätere Mittelwertbildung verwendet.
  12. Schritt 12 – Stop-Sequenz erkennen:
    Die Endsequenz des SML-Telegramms wird erkannt. Damit ist der Datensatz vollständig.
  13. Schritt 13 – Datenverarbeitung und Upload:
    Nach intSollLoopCount (standardmäßig 60) erfolgreichen Durchläufen wird die mittlere Leistung berechnet und zusammen mit den Zählerständen per HTTPS an den Server gesendet. Danach werden alle Werte zurückgesetzt und der Zyklus beginnt von vorn.

Besonderheiten der Ablaufsteuerung

  • Die OBIS-Werte 1.8.0 und 2.8.0 werden nur im letzten Zyklus vor dem Upload verarbeitet, um Flash-Verschleiß und unnötige Datenmengen zu vermeiden.
  • Die OBIS-Werte 10.7.0 (aktuelle Leistung) werden bei jedem Durchlauf erfasst und später gemittelt.
  • Die Sequenzsteuerung ist robust gegenüber fehlerhaften Telegrammen: Bei Abweichungen wird der Sequenzindex zurückgesetzt.

Verwendete Funktionen

  • parseWattValue: Wandelt die Bytefolge der OBIS 10.7.0 in einen vorzeichenbehafteten Integerwert um. Negative Werte werden über Zweierkomplement korrekt interpretiert.
  • parseUnsignedValue: Wandelt die Bytefolgen der OBIS 1.8.0 und 2.8.0 in positive Integerwerte um. Diese werden durch 10.000 geteilt, um kWh-Werte zu erhalten.
  • rechteHexZiffer: Extrahiert die rechte Hex-Ziffer eines Bytes zur Bestimmung der Byteanzahl für die folgenden Daten.
  • roundToDecimalPlaces: Rundet Dezimalwerte auf eine definierte Anzahl von Nachkommastellen für bessere Lesbarkeit und reduzierte Datenmenge.
  • ErrorCodeLed: Gibt den aktuellen Sequenzschritt als Blinkcode über die Error-LED aus, wenn ein Schritt zu lange aktiv bleibt. Die Anzahl der Blinkimpulse entspricht dem Sequenzschritt.

Status-LEDs

  • LED OK (Pin 13): Leuchtet während erfolgreicher Datenerfassung und zeigt an, dass die WLAN-Verbindung steht und die SML-Daten korrekt verarbeitet werden.
  • LED Error (Pin 12): Blinkt bei Fehlern oder wenn ein Sequenzschritt zu lange aktiv bleibt. Die Blinkanzahl entspricht dem Schritt, in dem der Fehler aufgetreten ist.

Die gesamte Ablaufsteuerung basiert auf einer effizienten Zustandsmaschine, die sicherstellt, dass nur vollständige und valide SML-Telegramme verarbeitet werden. Die zyklische Mittelwertbildung der Leistung sorgt für geglättete Werte und reduziert kurzfristige Schwankungen. Die HTTPS-Übertragung erfolgt verschlüsselt und zuverlässig über die WiFiClientSecure-Klasse.

PHP to DB Quellcode

Den PHP-Code können Sie direkt kopieren und auf Ihren Server hochladen. Bitte beachten Sie, dass Sie an insgesamt 5 Stellen Ihre persönlichen Daten wie Datenbank-Name, Datenbank-Passwort, Datenbank-Benutzer, Datenbank-Host und Salt-Passwort eintragen müssen. Diese Stellen sind im Code durch ** YOUR VALUE ** gekennzeichnet und müssen individuell angepasst werden, damit die Verbindung und Datenübertragung korrekt funktioniert.

			<?php
	/*Fehler ausschalten*/
	//error_reporting(E_ALL);
	//ini_set("display_errors", 1);
	error_reporting(0);
	/* Datenbankverbindung */
	/* Datenbankverbindung */
	$username = "** YOUR USERNAME **";
	$password = "** YOUR PASSWORD **";
	$database = "**YOUR DATABASE NAME **";
	$hostname = "** YOUR HOST IP + PORT **"; 
	//connection to the database
	$dbcon = mysqli_connect($hostname, $username, $password, $database);
	mysqli_set_charset($dbcon, 'utf8');	
	/* Variablen */
	//GET
	$Get_PW = $_GET["PW"]; //Passwort zur Berechtigungsbestätigung
	$Get_obis180 = $_GET["P180"]; //Zählerwert für OBIS 1.8.0
	$Get_obis280 = $_GET["P280"]; //Zählerwert für OBIS 2.8.0
	$Get_obis10070 = $_GET["P10070"]; //Zählerwert für OBIS 10.07.0
	//Zeitstempel
	$timestamp = time();
	$aktTime = date("H:i:s", $timestamp);
	$aktDate = date("Y-m-d", $timestamp);
	$yesterday = date("Y-m-d", strtotime("-1 day"));
	//Allgemeine Variablen
	$PWsoll = "** YOUR SALT PASSWORD HASH **";
	$switch_db_case = 0;
	//Variablen Datenbankabfrage
	$DBdataArray = array();
	$dif_used_180 = 0;
	$dif_used_280 = 0;
	$dif_180 = 0;
	$dif_280 = 0;

	// Überprüfen, ob Passwort und Zählerwerte gesetzt sind
	if ($Get_PW == $PWsoll && $Get_obis180 != "" && $Get_obis280 != "" && $Get_obis10070 != "") {
		
		/* OBIS 10.07.0 in DB schreiben */
		$statement = "INSERT INTO obis_10070 (obis_10070, TimeStamp) VALUES ('" . $Get_obis10070 . "', '" . $aktDate . " " . $aktTime . "')";
		mysqli_query($dbcon, $statement);
		
		/* OBIS 1.8.0 und 2.8.0 */
		// Letzte Werte ermitteln
		$statement = "SELECT * FROM obis_180_280 ORDER BY Date DESC LIMIT 2";
		$sqlresult = mysqli_query($dbcon, $statement);
		
		if ($sqlresult && mysqli_num_rows($sqlresult) > 0) {
			// Schleife für alle Datensätze (in diesem Fall 2)
			while ($dsatzSQL = mysqli_fetch_assoc($sqlresult)) {
				// Speichern der Daten im Array
				$DBdataArray[] = $dsatzSQL;
			}

			// Sicherstellen, dass genau 2 Datensätze im Array sind
			if (count($DBdataArray) == 2) {
			//Datenbank hat min zwei Einträg
				if($DBdataArray[0]["date"] == $aktDate){ 
					//5. Letzter Eintrag ist von Heute
					$switch_db_case = 5;
				}elseif ($DBdataArray[0]["date"] == $yesterday){
					//6. Letzter Eintrag ist von Gestern
					$switch_db_case = 6;
				}else{
					//7. Letzter Eintrag ist schon länger her (Bei Ausfall länger als 1 Tag).
					$switch_db_case = 7;
				}
			} elseif(count($DBdataArray) == 1) {
			//Datenbank hat nur einen Eintrag
				if($DBdataArray[0]["date"] == $aktDate){ 
					//2. Datenbank neu. Enthält nur Einträge von heute.
					$switch_db_case = 2;
				}elseif($DBdataArray[0]["date"] == $yesterday){
					//3. Datenbank neu. Enthält nur Einträge von Gestern.
					$switch_db_case = 3;
				}else{
					//4. Letzter Eintrag ist schon länger her (Bei Ausfall länger als 1 Tag).
					$switch_db_case = 4;
				}
			}
		} else {
			//1. Neustart, noch kein Datensatz vorhanden 
			$switch_db_case = 1;
		}
		echo $switch_db_case;
		// Datenbank anpassen
		switch ($switch_db_case) {
			case 1:
				//OK
				//Neustart kein Datensatz vorhanden
				$statement = "INSERT INTO obis_180_280 (used_180, used_280, obis_180, obis_280, date) VALUES ( '0', '0', '" . $Get_obis180 . "', '" . $Get_obis280 . "', '" . $aktDate . "')";
				mysqli_query($dbcon, $statement);
				break;
			case 2:
				//OK
				//Nur ein Eintrag mit heutigem Datum, Wert Updaten
				$dif_used_180 = $DBdataArray[0]["used_180"] + ($Get_obis180 - $DBdataArray[0]["obis_180"]);
				$dif_used_280 = $DBdataArray[0]["used_280"] + ($Get_obis280 - $DBdataArray[0]["obis_280"]); 
				$statement = "Update obis_180_280 SET obis_180=$Get_obis180, obis_280=$Get_obis280, used_180=$dif_used_180, used_280=$dif_used_280 WHERE Date='" . $aktDate . "'";
				mysqli_query($dbcon, $statement);
				break;
			case 3:
				//OK
				//Nur ein Eintrag mit gestrigem Datum, Werte von heute eintragen.
				$dif_used_180 = $Get_obis180 - $DBdataArray[0]["obis_180"];
				$dif_used_280 = $Get_obis280 - $DBdataArray[0]["obis_280"];
				$statement = "INSERT INTO obis_180_280 (used_180, used_280, obis_180, obis_280, date) VALUES ( '".$dif_used_180."', '".$dif_used_280."', '" . $Get_obis180 . "', '" . $Get_obis280 . "', '" . $aktDate . "')";
				mysqli_query($dbcon, $statement);				
				break;
			case 4:
				//OK
				//Nur ein Eintrag mit älterem Datum, Werte bis heute nachtragen.
				$LatestDBDate = new DateTime($DBdataArray[0]["date"]);
				$DayDiff = $LatestDBDate->diff(new DateTime($aktDate))->days;//-1
				$dif_used_180 = round(($Get_obis180 - $DBdataArray[0]["obis_180"])/$DayDiff,3);
				$dif_used_280 = round(($Get_obis280 - $DBdataArray[0]["obis_280"])/$DayDiff,3);
				$dif_180 = $DBdataArray[0]["obis_180"];
				$dif_280 = $DBdataArray[0]["obis_280"];
				$statement = "INSERT INTO obis_180_280 (used_180, used_280, obis_180, obis_280, date) VALUES ";
				for ($i = 1; $i <= $DayDiff-1; $i++ ){
					$nextday =  date("Y-m-d", strtotime("+$i day", strtotime($DBdataArray[0]["date"])));
					$dif_180 += $dif_used_180;
					$dif_280 += $dif_used_280;
					$statement .= "('$dif_used_180', '$dif_used_280', '$dif_180', '$dif_280', '$nextday'), ";
				}
				$statement .= "('".$dif_used_180."', '".$dif_used_280."', '" . $Get_obis180 . "', '" . $Get_obis280 . "', '" . $aktDate . "')";
				mysqli_query($dbcon, $statement);				
				break;
			case 5:
				//OK
				//Zwei oder mehr Einträge. Der letzte Eintrag ist von heute.
				$dif_used_180 = $Get_obis180 - $DBdataArray[1]["obis_180"];
				$dif_used_280 = $Get_obis280 - $DBdataArray[1]["obis_280"]; 
				$statement = "Update obis_180_280 SET obis_180=$Get_obis180, obis_280=$Get_obis280, used_180=$dif_used_180, used_280=$dif_used_280 WHERE Date='" . $aktDate . "'";
				mysqli_query($dbcon, $statement);			
				break;
			case 6:
				//OK
				//Zwei oder mehr Einträge. Der letzte Eintrag ist von gestern.
				$dif_used_180 = $Get_obis180 - $DBdataArray[0]["obis_180"];
				$dif_used_280 = $Get_obis280 - $DBdataArray[0]["obis_280"];
				$statement = "INSERT INTO obis_180_280 (used_180, used_280, obis_180, obis_280, date) VALUES ( '".$dif_used_180."', '".$dif_used_280."', '" . $Get_obis180 . "', '" . $Get_obis280 . "', '" . $aktDate . "')";
				mysqli_query($dbcon, $statement);	
				break;
			case 7:
				//OK
				//Nur Werte mit älterem Datum, Werte bis heute nachtragen.
				$LatestDBDate = new DateTime($DBdataArray[0]["date"]);
				$DayDiff = $LatestDBDate->diff(new DateTime($aktDate))->days;
				$dif_used_180 = round(($Get_obis180 - $DBdataArray[0]["obis_180"])/$DayDiff,3);
				$dif_used_280 = round(($Get_obis280 - $DBdataArray[0]["obis_280"])/$DayDiff,3);
				$dif_180 = $DBdataArray[0]["obis_180"];
				$dif_280 = $DBdataArray[0]["obis_280"];
				$statement = "INSERT INTO obis_180_280 (used_180, used_280, obis_180, obis_280, date) VALUES ";
				for ($i = 1; $i <= $DayDiff-1; $i++ ){
					$nextday =  date("Y-m-d", strtotime("+$i day", strtotime($DBdataArray[0]["date"])));
					$dif_180 += $dif_used_180;
					$dif_280 += $dif_used_280;
					$statement .= "('$dif_used_180', '$dif_used_280', '$dif_180', '$dif_280', '$nextday'), ";
				}
				$statement .= "('".$dif_used_180."', '".$dif_used_280."', '" . $Get_obis180 . "', '" . $Get_obis280 . "', '" . $aktDate . "')";
				mysqli_query($dbcon, $statement);				
				break;
		}
	}
	
	/* Verbindung zur SQL-DB trennen */
	mysqli_close($dbcon);
?>
			
		

Technische Beschreibung der serverseitigen PHP-Datei

Diese PHP-Datei verarbeitet die vom ESP8266 übermittelten Zählerdaten und speichert sie in einer MySQL-Datenbank. Sie prüft die Gültigkeit der Anfrage, berechnet Differenzwerte und aktualisiert die Datenbank je nach Zustand. Die Datei ist so aufgebaut, dass sie auch bei Ausfällen oder Neustarts konsistente Daten liefert.

1. Konfiguration und Verbindung

  • Fehlerausgabe deaktivieren: Die PHP-Fehlermeldungen sind standardmäßig ausgeschaltet, um die Ausgabe zu minimieren.
  • Datenbankverbindung: Die Zugangsdaten (Benutzername, Passwort, Datenbankname, Host) müssen an vier Stellen durch persönliche Werte ersetzt werden (** YOUR VALUE **).
  • Zeichensatz: Die Verbindung wird auf UTF-8 gesetzt, um Sonderzeichen korrekt zu verarbeiten.

2. Empfang der Messwerte

  • GET-Parameter: Die Datei erwartet drei Werte vom ESP8266:
    • P180: Zählerstand für OBIS 1.8.0 (Bezug)
    • P280: Zählerstand für OBIS 2.8.0 (Einspeisung)
    • P10070: aktuelle Wirkleistung (OBIS 10.7.0)
    • PW: Passwort zur Authentifizierung (muss mit $PWsoll übereinstimmen)
  • Zeitstempel: Die aktuelle Uhrzeit und das Datum werden ermittelt und für die Datenbankeinträge verwendet.

3. Passwortprüfung und Datenvalidierung

  • Die Daten werden nur verarbeitet, wenn:
    • Das übermittelte Passwort korrekt ist
    • Alle drei Messwerte gesetzt sind

4. Speicherung der aktuellen Leistung (OBIS 10.7.0)

  • Die aktuelle Leistung wird direkt in die Tabelle obis_10070 geschrieben – unabhängig vom Zustand der anderen Daten.

5. Verarbeitung der Zählerstände (OBIS 1.8.0 und 2.8.0)

Die Datei prüft, welche Einträge bereits in der Tabelle obis_180_280 vorhanden sind und entscheidet anhand des Datums, wie die neuen Werte verarbeitet werden. Es gibt sieben mögliche Fälle:

  1. Fall 1 – Kein Eintrag vorhanden:
    Die Datenbank ist leer. Ein neuer Eintrag mit den aktuellen Werten wird erstellt.
  2. Fall 2 – Ein Eintrag von heute:
    Die heutigen Werte werden aktualisiert. Die Differenz zum vorherigen Wert wird berechnet und aufaddiert.
  3. Fall 3 – Ein Eintrag von gestern:
    Ein neuer Eintrag für heute wird erstellt. Die Differenz zum gestrigen Wert wird berechnet und gespeichert.
  4. Fall 4 – Ein Eintrag mit älterem Datum:
    Die Differenz wird auf die fehlenden Tage verteilt. Für jeden Tag wird ein Eintrag mit interpolierten Werten erstellt.
  5. Fall 5 – Zwei oder mehr Einträge, letzter von heute:
    Die heutigen Werte werden aktualisiert. Die Differenz zum vorletzten Eintrag wird berechnet und gespeichert.
  6. Fall 6 – Zwei oder mehr Einträge, letzter von gestern:
    Ein neuer Eintrag für heute wird erstellt. Die Differenz zum gestrigen Wert wird berechnet.
  7. Fall 7 – Zwei oder mehr Einträge, letzter älter als gestern:
    Wie in Fall 4: Die Differenz wird auf die fehlenden Tage verteilt und für jeden Tag ein Eintrag erstellt.

6. Datenbankstruktur

  • Tabelle obis_10070: Speichert die aktuelle Leistung mit Zeitstempel.
  • Tabelle obis_180_280: Speichert Bezug und Einspeisung pro Tag sowie die berechnete Differenz (used_180, used_280).

7. Abschluss

  • Nach der Verarbeitung wird die Verbindung zur Datenbank geschlossen.

Hinweis zur Konfiguration

Die Datei enthält 5 Stellen, an denen persönliche Zugangsdaten eingetragen werden müssen. Diese sind im Code durch ** YOUR VALUE ** gekennzeichnet und müssen angepasst werden, damit die Verbindung zur Datenbank funktioniert.

Die PHP-Datei ist robust gegenüber Ausfällen und sorgt dafür, dass auch bei längerer Unterbrechung die Zählerdaten korrekt interpoliert und gespeichert werden. So bleibt die Datenhistorie vollständig und nachvollziehbar.

Donation

Euch hat es bei uns gefallen oder die Beiträge haben euch weitergeholfen, dann könnt uns gerne mit einer kleinen Spende für weitere Projekte unterstützen. Vielen Dank

Letzte Änderung: 28.10.2025