Stromzähler
TYP: ISKRA MT691
Im ersten Teil unseres Stromzähler-Projekts haben wir den Drehstromzähler Itron 3.HZ-AC-D1-A1 mithilfe eines Mikrocontrollers und eines IR-Lesekopfs erfolgreich ausgelesen.
Die erfassten Daten wurden über einen längeren Zeitraum in eine Datenbank übertragen und dort gespeichert.
Doch alles hat irgendwann ein Ende – so auch unser treuer Itron-Zähler.
Der neue Stromzähler, ein ISKA MT691, war mit dem bisherigen Arduino-Quellcode leider nicht kompatibel.
Daher habe ich das Projekt neu aufgesetzt.
Zwar musste ich nicht komplett von vorne beginnen, doch ich habe mich erneut durch zahlreiche Internetseiten gelesen, um mich in das Thema einzuarbeiten.
Mittlerweile verstehe ich das SML-Protokoll deutlich besser und möchte hier meine Erfahrungen sowie die einzelnen Schritte meiner erfolgreichen Umsetzung mit euch teilen.
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:
Wenn alles korrekt angeschlossen und verbunden ist, können wir mit dem Arduino-Code beginnen.
Wichtige Hinweise:
INFO ON aktiviert. Diese sorgt offenbar nur dafür, dass mehr Informationen auf dem Display des Stromzählers angezeigt werden – sie beeinflusst jedoch nicht die optische Schnittstelle.
#include <SoftwareSerial.h>;
// SoftwareSerial nutzen, um Daten von der optischen Schnittstelle des Stromzählers zu lesen
// GPIO4 = RX (Empfang vom Zähler), GPIO5 = TX (nicht benutzt in diesem Fall)
SoftwareSerial irSerial(4, 5);
// Puffer zum Zwischenspeichern der empfangenen Bytes
#define BUFFER_SIZE 256
byte buffer[BUFFER_SIZE];
int idx = 0; // Aktuelle Position im Puffer (Index)
void setup() {
Serial.begin(115200); // Serielle Verbindung zum PC – hohe Baudrate empfohlen
irSerial.begin(9600); // Serielle Verbindung zum Stromzähler (z. B. über die D0-Schnittstelle)
}
void loop() {
// Solange Daten vom Stromzähler verfügbar sind und der Puffer nicht voll ist:
while (irSerial.available() && idx < BUFFER_SIZE) {
buffer[idx++] = irSerial.read(); // Nächstes Byte lesen und im Puffer speichern
}
// Wenn der Puffer voll ist, geben wir alle gesammelten Bytes als HEX-Werte aus
if (idx >= BUFFER_SIZE) {
for (int i = 0; i < idx; i++) {
// Optional: eine führende Null anzeigen, damit alles zweistellig ist (z. B. "0A" statt "A")
if (buffer[i] < 0x10) Serial.print("0");
// Byte als HEX-Wert ausgeben (z. B. "1B", "7E", etc.)
Serial.print(buffer[i], HEX);
Serial.print(" ");
}
Serial.println(); // Neue Zeile nach der Ausgabe
idx = 0; // Puffer zurücksetzen für den nächsten Durchlauf
}
}
Mit diesem Code erhalten wir vollständige und zusammenhängende SML-Protokolle im seriellen Monitor.
Die einzelnen Protokolle werden durch spezielle Start- und Endsequenzen voneinander abgegrenzt:
1B 1B 1B 1B 01 01 01 011B 1B 1B 1B 1ADiese Sequenzen sind im SML-Standard definiert und ermöglichen es, einzelne Telegramme bei kontinuierlichem Datenstrom zuverlässig zu erkennen und zu verarbeiten.
Hier ein einzelnes Protokoll von meinem ISKRA MT691:
1B 1B 1B 1B 01 01 01 01 76 05 03 F8 1F BD 62 00 62 00 72 63 01 01 76 01 01 05 01 52 B5 3F 0B 0A 01 48 53 4C 00 05 69 4E BA 72 62 01 65 01 52 B3 9D 62 01 63 BA CF 00 76 05 03 F8 1F BE 62 00 62 00 72 63 07 01 77 01 0B 0A 01 48 53 4C 00 05 69 4E BA 07 01 00 62 0A FF FF 72 62 01 65 01 52 B3 9D 75 77 07 01 00 60 32 01 01 01 01 01 01 04 49 53 4B 01 77 07 01 00 60 01 00 FF 01 01 01 01 0B 0A 01 48 53 4C 00 05 69 4E BA 01 77 07 01 00 01 08 00 FF 65 00 1C 01 04 01 62 1E 52 FF 65 02 EF 85 5E 01 77 07 01 00 02 08 00 FF 01 01 62 1E 52 FF 65 00 1E 6B AA 01 77 07 01 00 10 07 00 FF 01 01 62 1B 52 00 53 0B 10 01 01 01 63 1F 7C 00 76 05 03 F8 1F BF 62 00 62 00 72 63 02 01 71 01 63 FA 7C 00 00 1B 1B 1B 1B 1A 01 54 68
Nachfolgend sehen Sie eine Tabelle, in der die zuvor empfangenen Daten in einzelne Segmente unterteilt werden. Für uns sind die fett markierten Werte interessant. Die mit * markierten Werte werden unten genauer beschrieben.
| 📡 SML-Telegramm | |
|---|---|
| 1B 1B 1B 1B 01 01 01 01 | Startsequenz |
| 76 05 03 F8 1F BD | Zeitstempel / Message-ID |
| 62 00 | Public-Key Info (leer) |
| 62 00 | Public-Key Info (leer) |
| 72 63 01 01 | Transaktionsdaten |
| 76 01 01 05 01 52 B5 3F 0B | Message-Header |
| 0A 01 48 53 4C 00 05 69 4E BA | Server-ID: ISKRA MT691 (verändert) |
| 🔁 Wiederholung der Server-ID & Steuerdaten | |
| 72 62 01 65 01 52 B3 9D | Message-Body |
| 62 01 63 BA CF 00 | Steuerdaten |
| 76 05 03 F8 1F BE | Zeitstempel |
| 62 00 62 00 | Public-Key Info (leer) |
| 72 63 07 01 77 01 0B | Datenblock |
| 0A 01 48 53 4C 00 05 69 4E BA | Server-ID erneut (verändert) |
| ⚙️ OBIS-Setup & Konfiguration | |
| 07 01 00 62 0A FF FF | OBIS Setup Info |
| 72 62 01 65 01 52 B3 9D 75 | Steuerdaten |
| 77 07 01 00 60 32 01 01 01 01 | OBIS-Definitionen |
| 01 01 04 49 53 4B 01 | OBIS-Definitionen (Fortsetzung) |
| 77 07 01 00 60 01 00 FF 01 01 | OBIS-Definitionen |
| 01 01 0B | Ende OBIS-Definitionen |
| 0A 01 48 53 4C 00 05 69 4E BA | Server-ID erneut (verändert) |
| 01 | Ende Block |
| ⚡ Verbrauchswerte | |
| 77 07 01 00 01 08 00 FF | OBIS 1.8.0 (Bezug) |
| 65 00 1C 01 04 01 | Struktur-Header |
| 62 1E 52 FF | Datentyp: Integer /10000 |
| 65 | * Marker für SEQUENCE (5 Byte) |
| 02 EF 85 5E | ** Zählerstand: 49251678 → 4925.1678 kWh |
| 01 | Endblock |
| 🔋 Einspeisung | |
| 77 07 01 00 02 08 00 FF | OBIS 2.8.0 (Einspeisung) |
| 01 01 62 1E 52 FF | Datentyp: Integer /10000 |
| 65 | * Marker für SEQUENCE (5 Byte) |
| 00 1E 6B AA | ** Zählerstand: 1995690 → 199.5690 kWh |
| 01 | Endblock |
| ⚙️ Momentanwert (Bezug) | |
| 77 07 01 00 10 07 00 FF | OBIS 10.7.0 |
| 01 01 62 1B 52 00 | Datentyp: Integer |
| 53 | * Unsigned Integer, 3 Byte |
| 0B 10 | *** Bezug oder Einspeisung: 2832 Watt Bezug |
| 01 | Endblock |
| ✅ Abschluss | |
| 01 01 63 1F 7C 00 | Steuerdaten / CRC / Padding |
| 76 05 03 F8 1F BF | Timestamp |
| 62 00 62 00 | Public-Key Info (leer) |
| 72 63 02 01 71 01 63 FA 7C | Abschlussdaten |
| 00 00 | Padding |
| 1B 1B 1B 1B 1A 01 54 68 | Endsequenz |
*: Ein Hexwert besteht aus einem Byte. Die rechten vier Bit des Bytes zeigen die Anzahl der nachfolgenden Bytes, die den Zählerwert beinhalten.
Dabei ist zu beachten, dass das letzte Byte (HEX 0x01) zu dieser Anzahl dazugehört, aber nicht im Zählerwert enthalten ist.
**: Dieser Datensatz beinhalten den Zählerwert (1.8.0 oder 2.8.0), der später im Dezimal durch 10.000 geteilt werden muss, um den Zählerstand in kWh zu erhalten.
Die Anzahl der Bytes ergibt sich aus der zuvor ermittelten Länge minus einem Byte für den Endblock.
***: Dieser Datensatz beinhaltet den Zählerwert für OBIS 10.07.0. Nach der Umrechnung in Dezimalzahl gibt dieser den aktuellen Bezug oder die Einspeisung in Watt an.
Die Anzahl der Bytes ergibt sich aus der zuvor ermittelten Länge minus einem Byte für den Endblock. Ein negativer Wert bedeutet Einspeisung, ein positiver Wert Bezug.
Als Hilfe könnt Ihr diesen kleinen HEX ↔ DEC Umrechner verwenden. Die Umrechnung erfolgt in beide Richtung. Einfach im jeweiligen Feld einen Wert eintragen. Hierbei ggf. die führende Null nicht vergessen.
Umwandlung der Zählerstände aus dem Stromzähler 1.8.0 und 2.8.0
Die folgenden Informationen beschreiben die Funktion, die dazu dient, die Hex-Werte aus den Stromzählern 1.8.0 und 2.8.0 in eine lesbare, ganzzahlige Zahl umzuwandeln.
Diese Zählerstände werden häufig als Hex-Werte übertragen, und die Funktion parseUnsignedValue verarbeitet diese, um den entsprechenden unsigned long Wert zu berechnen.
Der aufruf der Funktion kann wie folgt geschehen.
// Beispiel 1: 2 Byte (0x12 0x34 = 4660)
byte data1[] = {0x12, 0x34};
unsigned long val1 = parseUnsignedValue(data1, 2);
// Beispiel 2: 4 Byte (0x00 0x00 0x27 0x10 = 10000)
byte data2[] = {0x00, 0x00, 0x27, 0x10};
unsigned long val2 = parseUnsignedValue(data2, 4);
/* Parse Zählerstände 1.8.0 und 2.8.0 (immer positiv)
Diese Funktion liest die Zählerstände aus einem Array von Bytes und
gibt den entsprechenden unsigned long Wert zurück. Der Wert wird
durch eine Kombination von 1 bis 4 Bytes gebildet. */
unsigned long parseUnsignedValue(byte* data, int length) {
// Überprüfen, ob die Länge des Arrays im gültigen Bereich (1 bis 4) liegt.
// Wenn die Länge ungültig ist, wird 0 zurückgegeben.
if (length < 1 || length > 4) return 0;
unsigned long result = 0; // Initialisiere den Ergebniswert auf 0
// Iteriere über jedes Byte im Array, um den finalen Wert zu berechnen
for (int i = 0; i < length; i++) {
result <<= 8; // Verschiebe das Ergebnis um 8 Bits nach links (entspricht *256)
result |= data[i]; // Füge das aktuelle Byte `data[i]` mit einer OR-Operation hinzu
}
// Gib den berechneten unsigned long Wert zurück
return result;
}
Die Funktion parseUnsignedValue hat die Aufgabe, Byte-Werte aus den Zählern zu lesen und diese zu einer Zahl zusammenzuführen. Diese Funktion unterstützt Längen von 1 bis 4 Bytes, sodass sie flexibel für verschiedene Formate von Zähler-Daten eingesetzt werden kann.
Für jedes Byte im Array wird das aktuelle Ergebnis um 8 Bits nach links verschoben, um Platz für das nächste Byte zu schaffen. Anschließend wird das Byte mit einer OR-Operation an das Ergebnis angehängt. Dieser Vorgang wird für alle Bytes im Array wiederholt, bis die endgültige Zahl gebildet ist.
unsigned long zurück, der die kombinierte Zahl der Hex-Werte repräsentiert.Diese Umwandlung ist besonders nützlich, wenn du die Zählerstände aus den Modulen 1.8.0 und 2.8.0 korrekt auslesen und verarbeiten möchtest. Die Funktion ermöglicht es, die Hex-Werte in eine standardisierte Zahl zu konvertieren, die dann weiterverarbeitet oder angezeigt werden kann.
Die Messwerte des Stromzählers werden in einem speziellen Format, oft als Hexadezimalwert (HEX), übertragen. Diese Werte können sowohl für den aktuellen Bezug (Stromverbrauch) als auch für die Einspeisung von Strom (z. B. aus einer Solaranlage) verwendet werden. Die Herausforderung besteht darin, dass der HEX-Wert sowohl positive als auch negative Zahlen darstellen kann. Genau hier setzt die Funktion parseWattValue an.
Der aufruf der Funktion kann wie folgt geschehen.
// Beispiel: negativer Wert (Zweierkomplement, z. B. 0xFF 0x9C = -100)
byte data2[] = {0xFF, 0x9C};
long watt2 = parseWattValue(data2, 2);
// Beispiel: 4 Byte Wert
byte data3[] = {0x00, 0x01, 0x86, 0xA0}; // = 100000 dezimal
long watt3 = parseWattValue(data3, 4);
// Funktion zur Umrechnung des Stromwertes (Wirkleistung) aus einem HEX-Wert
/* Diese Funktion nimmt ein Array von Bytes (data) und die Länge dieser Bytes und berechnet den entsprechenden
Wert der Wirkleistung (in Watt), der als positiver oder negativer Wert vorliegen kann. */
long parseWattValue(byte* data, int length) {
if (length < 1 || length > 4) return 0; // Ungültige Länge, nur 1 bis 4 Bytes sind erlaubt
long result = 0;
// Die einzelnen Bytes werden hier zu einer Zahl zusammengesetzt.
// Das erfolgt im Big-Endian-Format, wobei das erste Byte den höchsten Wert darstellt.
for (int i = 0; i < length; i++) {
result <<= 8; // Verschiebe das bisherige Ergebnis um 8 Bits nach links
result |= data[i]; // Füge das aktuelle Byte zum Ergebnis hinzu
}
// Prüfen, ob das erste Byte ein "Vorzeichenbit" gesetzt hat (höchstes Bit, 0x80)
if (data[0] & 0x80) {
// Der Wert ist negativ (Zweierkomplement)
// Berechnung der Anzahl der ungenutzten Bits
// (die Anzahl der Bits, die im Wert nicht genutzt werden, abhängig von der Länge der Eingabedaten)
int shift = (4 - length) * 8; // Berechne, wie viele Bits wir für das Vorzeichen anpassen müssen
result = (result << shift) >> shift; // Vorzeichen wird durch Links- und Rechtsschieben erweitert
}
// Rückgabe des berechneten Wertes
return result;
}
Die Funktion nimmt ein Array von Bytes (die Hex-Werte) und berechnet daraus eine Ganzzahl, die den Stromverbrauch oder die Einspeisung in Watt angibt. Dabei wird berücksichtigt, ob der Wert positiv oder negativ ist, was durch das Zweierkomplement des HEX-Werts ermittelt wird.
0x80) auf „1“ gesetzt sein kann, um anzuzeigen, dass der Wert negativ ist.
Diese Funktion stellt sicher, dass die Hexadezimalwerte, die vom Stromzähler geliefert werden, korrekt als tatsächliche Wirkleistung in Watt interpretiert werden. So kann der tatsächliche Stromverbrauch oder die Einspeisung zuverlässig ermittelt werden, unabhängig davon, ob der Wert positiv oder negativ ist.