SML auf Ethernet (UDP)

Bei meinen Basteleien zum Auslesen eines Smartmeters (Smartmeter D0, SML) habe ich anfangs noch nicht mit Tasmota und Co arbeiten können. In der Zeit hatte ich auch noch keinen MQTT-Server o.ä. und dachte, dass ich einfach ein Serial2UDP-Gateway mit einem ESP8266 schreibe. Der ESP sollte seriell die Daten vom Smartmeter ablesen und per UDP-Paket ins Netzwerk "Broadcasten". Damit spare ich mir die Konfiguration von Servern und quasi jedes Endgerät könnte den Zählerstand selbst auswerten.

Mittlerweile ist dieser Code nicht mehr aktiv und nur noch als Muster hier veröffentlicht.

Bei Net at Work haben wir nicht nur Aufgaben für Office 365 Consultants und Windows Supporter. Auch pfiffige Entwickler dürfen sich bewerben. Unsere Mitarbeiter sind auch Bienenzüchter (IoT Sensoren), Modellflugbauer, Hausautomatisierer u.a.
https://www.netatwork.de/unternehmen/karriere/

Hardware

Meine Überlegung war folgende.

  • ESP8266 als Basis
    Versorgung mit Batterie oder 5V Netzadapter, IR-Fototransistor an RxD.
  • System bucht sich ins WiFi ein, liest die Messwerte und sendet diese per UDP ins LAN oder per HTTPS an einen hinterlegten Server. Ich erspare mir so einen permanent aktiven Client auf dem ein Server läuft.
  • Erfassung
    Mein eh vorhandenes Monitoring-System (Siehe PRTG) startet regelmäßig ein Skript, welches genau diese UDP-Pakete empfängt und an PRTG übergibt

Zuerst musste ich natürlich die Hardware entsprechend vorbereiten. Ich habe mich für ein "fertiges" Board entschiede, welches schon einen USB-Anschluss inkl. Programmierung hat, von der Arduino-IDE unterstützt wird und sehr klein ist. Meine Wahl fiel auf das Wemos D1 und eine BPW40, die ich einfach direkt angeschlossen habe:

Der Emitter ist auf "G" = (GND) verbunden und der Kollektor an "RxD" angeschlossen. Zum "Programmieren" muss ich den Fototransistor allerdings "dunkel" abdecken oder abziehen. Ich habe vorher mit einem Amperemeter den Kurzschlussstrom gemessen (5 mA). Selbst bei 5 Volt wären das also maximal 25mW. Laut Datenblatt soll die BPW40 aber bis zu 150mW bzw. einen Kollektorstrom von bis zu 100mA aushalten. Das ist im grünen Bereich. Später wird die Schaltung natürlich in ein Gehäuse eingebaut und die LED mit einem Ringmagnet und schwarzen Abdeckungen montiert. Aber auch so ist die Funktion schon sehr zuverlässig.

Der nächste Schritt ist die Software, die folgende Schritte durchläuft. Knifflig war etwas, dass die serielle Kommunikation nur einen Puffer von 64bytes hat, während ein Datagramm knapp 400Bytes sind. Also darf ich beim Lesen nicht trödeln. Insbesondere Ausgaben zur Fehlersuche bremsen bei 9600 Baud den Ablauf. Da der Stromzähler aber immer wieder sendet, macht es hier mal nichts aus, ein Paket zu verlieren. Ich habe meinen Code dann einfach den Buffer leert, bis ca. keine Daten mehr kommen. Dann kann ich davon ausgehen, dass es bald "losgeht" und dann stumpf lesen, bis wieder eine Pause kommt. Um den Code einfach zu halten, findet im Code keine umfangreiche Validierung, Normalisierung oder Konvertierung statt. Das überlasse ich dem empfangenden Programm. So ist der Code mit der Hardware quasi universell mit vielen Systemen einsetzbar, die spontan Daten senden.

LED als Status

Da der ESP ja keine Bildschirmausgabe hat und ich nicht immer am USB-Anschluss einen Notebook mit Terminal anschliessen will, habe ich die eingebaute LED als Statusanzeige programmiert.

Blink (Anzahl, Einschaltzeit, Ausschaltzeit, Pause am Ende) Phase Beschreibung

blink(1,5000,500,1000)

INIT Startet

 

blink(4,100,500,2000)

Serial ist initialisiert

 

blink(5,50,100,2000)

WiFi Verbindung wird hergestellt

 

blink(6,100,500,2000)

WiFi Connected

 

blink(7,100,500,2000)

INIT abgeschlossen

 

blink(2,400,100,2000)

LOOP Startet

 

Sek Aus, 0,5 Sek an 

Warte auf Serial Bytes

 

blink(5,100,100,0)

Sende UDP Paket 

 

blink(10,100,100,2000)

Fehler beim Einlesen

 

blink(100,30,70,0)

10 Sek "warten"

 

Zum Testen konnte ich das System erst mal direkt am PC seriell ansteuern.

UDP auf dem Kabel

Ehe sie weiter unten den Code finden, zeige ich ihnen hier erst einmal, dass es wirklich funktioniert hat. Jede Information, die der ESP8266 seriell bekommen hat, wurde per UDP ins Netz gesendet.

 

Die Ausgabe ist im Wireshark gut zu sehen.

Code

Hier der aktuelle Code. Die Zugangsdaten des WiFi habe ich aber absichtlich "falsch" eingetragen. Es lohnt sich also nicht nach Hövelhof zu fahren um auf "Free Internet" zu hoffen.

/*
 * ESP8266 and D=-Smart Metering
 * 
 * Simple Code to collect the SML-Data from a IR-LED and send it using UDP to the local subnet
 * Configuration : Enter the ssid and password of your Wifi AP. Enter the port number your server is listening on.
 *
 *Serial Port RxD is used to read data from Smartmeter. Phototransistor is pulling down the RxD-Line. Should work even with PC connected
 *Serial Port TxD is used to send debug output
 *
 * 20160501 frank@carius.de  initial Version  
 * Portions from  http://www.esp8266.com/viewtopic.php?f=29&t=2222
 */

// Every serial.write or serial.writeln will take long at 9600 baud. So remove them in production

#include <ESP8266WiFi.h>
#include <WiFiUDP.h>
extern "C" {  //required for system_get_chip_id()
#include "User_interface.h"
  // uint16 readvdd33(void);
}

// Global constants for WiFi connections
int status = WL_IDLE_STATUS;
const char* ssid = "FCHAUS";             //  your network SSID (name)  Case sensible !
const char* pass = "wifi4haus";       // your network password
IPAddress udpip(192,168,178,255);       // specify target IP
unsigned int udpport = 12345;           // Specify Source and Target Port

// Buffer for serial reading 
int  serIn;             // var that will hold the bytes-in read from the serialBuffer
byte datagram[1000];    // array that will hold the different bytes 
int  serindex = 0;    // index of serInString[] in which to insert the next incoming byte

int  serialbyte;        // Byte to store serial input data
// Create a UDP instance to send and receive packets over UDP
WiFiUDP Udp;            // Instantiate UDP-Class

//
//  Blink Function for Feedback of Status, takes about 1000ms
//
void blink(int count, int durationon, int durationoff, int delayafter) {
  for (int i=0; i < count; i++) {
    digitalWrite(LED_BUILTIN, LOW);   // Turn the LED on 
    delay(durationon);
    digitalWrite(LED_BUILTIN, HIGH);  // Turn the LED off by making the voltage HIGH
    delay(durationoff);
  }
  delay(delayafter);
}

// ------------------------------------------------
//   SETUP running once at the beginning
// ------------------------------------------------
//   Initialize  Serial, WIFi and UDP
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);     // Initialize the LED_BUILTIN pin as an output
  blink(1,5000,500,1000);   // Signal startup
  Serial.begin(9600);   // Open serial communications and wait for port to open:
  while (!Serial) {
    ; // wait for serial port to connect. 
  }
  Serial.println("ESP8266-DO-Logger init");
  blink(4,100,500,2000);      // Signal Serial OK
  
  //
  // Wait for connect to AP
  //
  Serial.print("Start WiFi to SSID: ");
  Serial.println(ssid);
  while (WiFi.status() != WL_CONNECTED) {
    blink(5,50,100,2000);
    WiFi.begin(ssid, pass);
  }

  // Report Data to Serial Port. 
  Serial.print("Connect with IP:");
  Serial.println(WiFi.localIP());
  blink(6,100,500,2000);   
  
  // Start UDP Object
  Serial.print("Start UDP-Service on port:");
  Serial.println(udpport);
  Udp.begin(udpport);
  // Serial.setTimeout(1000);  // Set Timeout for Serial.readBytesUntil()
  blink(7,100,500,2000);   
  Serial.print("INIT Done");
}


// ------------------------------------------------
//   MAIN LOOP RUNNING all the time
// ------------------------------------------------
void loop() {
  Serial.print("LOOP: ");          // Send some startup data to the console
  Serial.print("SSID: ");               // SSID of Network
  Serial.print(WiFi.SSID());            // SSID ausgeben
  Serial.print("     IP Address: ");    // assigned IP-Address
  IPAddress ip = WiFi.localIP();        // IP Adresse auslesen
  Serial.println(ip);                   // IP Adresse ausgeben
  blink(2,400,100,2000);                // Show status

  //
  // Clear Serial Data
  //
  Serial.println("C"); 
  while (Serial.available()) {
    while (Serial.available()) {
      serialbyte = Serial.read(); //Read Buffer to clear
    }
    //Serial.print("F");
    delay(10);  // wait approx 10 bytes at 9600 Baud to clear bytes in transmission
  }

  //
  // assume, that there is now a pause. Wait for start of transmission
  // 
  Serial.println("W"); 
  int flashcount = 0;
  while (!Serial.available()){
    flashcount++;
    // Serial.println(flashcount);
    if (flashcount == 400) { 
      digitalWrite(LED_BUILTIN, LOW);       // Turn the LED on 
    }
    else if (flashcount > 500) {
      digitalWrite(LED_BUILTIN, HIGH);      // Turn the LED off 
      flashcount=0;
    }
    else {
      delay(5);  // wait 5 ms for new packets
    }
  }

  // We got some bytes. read until next pause
  Serial.println("R"); 
  // Serial.println("Reading serial data"); 
  Serial.setTimeout(500);   // Set Timeout to 500ms.
  serindex = Serial.readBytes(datagram,1000);
  // serindex = Serial.readBytesUntil('',datagram,1000);  // read all serial data and end with timeout.. How to read without looking for stop character 
  //serindex = 0;
  //while (Serial.available() && (serindex < 1000)){
  //  while (Serial.available() && (serindex < 1000)){
  //    serialbyte = Serial.read();   // Read Data with a 1000ms timeout 
  //    datagram[serindex] = serialbyte;
  //    serindex++;
  //  }
  //  delay(10);  // wait 10ms for more bytes
  //}

  if (serindex < 1000) {
    Serial.println("D"); 
    blink(5,100,100,0);
    Serial.print("Datagram received. Total Bytes:");Serial.println(serindex);
  
    //Serial.println("Sending UDP-Paket  XML");
    //Udp.beginPacket(udpip, udpport);  // Start new paket
    //Udp.write("<SMLR  eader>");
    //Udp.write("  <chipid>");
    //Udp.print(system_get_chip_id());
    // Udp.write("#IP of ESP8266#");
    //Udp.write("  </chipid>");
    //Udp.write("  <ipaddress>");
    //Udp.println(WiFi.localIP());
    //Udp.write("  </ipaddress>");
    //Udp.write("  <datagram>");
    //for (int i=0; i < serindex ; i++) {
      //char zeichen = String(datagram[i], HEX)[0];
      //Udp.write(zeichen);
    //  Udp.write(datagram[i]);
    // }
    //Udp.write("  </datagram>");
    //Udp.write("</SMLReader>");
    //Udp.endPacket();   // Send paket
  
    Serial.println("Sending UDP-Paket  RAW");
    Udp.beginPacket(udpip, udpport);  // Start new paket
    Udp.write(datagram,serindex);
    Udp.endPacket();   // Send paket
  }
  else {       // Error out of bounds during reading bytes from serial.
    blink(10,100,100,2000);
  } 
  
  // Serial.println("Sleep 10 Seconds");
  blink(100,30,70,0);
}

Der Code ist sicher nicht "schön" aber funktionierte und vielleicht brauche ich ihn ja noch mal für andere Dinge, z.B. könnte das Gerät tief schlafen und durch einen Schaltkontakt (Briefkasten, Wassermelder, Leckage etc.)  aufwachen und eine Meldung absetzen.

PRTG Probe

Nun fehlt nur noch die PRTG-Probe, die ab uns an mal gestartet wird und die eingehenden Daten einfängt, verarbeitet und zur Auswertung weiter gibt. Auch hier ist natürlich wieder PowerShell meine erste Wahl. Es ist damit viel einfacher einen UDP-Port zum "Lesen" zu starten als in VBScript o.ä. und eine EXE wollte ich nun doch mal nicht schreiben.

Hinweis
Achten Sie darauf, dass Sie den UDP-Port auch in der Firewall freischalten, sonst wartet ihre Probe vergeblich auf eingehende Pakete

Auf der Seite PS UDP habe ich schon entsprechende Vorarbeiten geleistet. Allerdings muss ich hier natürlich noch die Daten auflösen und konvertieren um sie dann z.B. an PRTG zu übergeben.

# Smartmeter2prtg

param (
   [string]$localip = "0.0.0.0",
   [string]$udplistenport="12345"
)

$udpClient = New-Object system.Net.Sockets.Udpclient($udplistenport)
$RemoteIpEndPoint = New-Object System.Net.IPEndPoint([system.net.IPAddress]::Parse($localip)  , $udplistenport);

Write-host "Receive-UDP:Wait für Data on Port: $udplistenport"
$data=$udpclient.receive([ref]$RemoteIpEndPoint)
write-host "Received packet from IP " $RemoteIpEndPoint.address ":" $RemoteIpEndPoint.Port

# Parse Content
write-host "Content" ([string]::join("",([System.Text.Encoding]::ASCII.GetChars($data))))

# Generate PRTG Output

Das Skript ist noch nicht fertig und wird es wohl auch nicht mehr, denn mittlerweile kann ich die Daten per Tasmota lesen und auswerten.

Mittelfristig wäre es natürlich auch eine Idee, einen Dienst zu schreiben, der von mehreren Sensoren die Daten annimmt und dann an PRTG z.B. als HTTPPush-Sensor an PRTG zu senden. Noch direkter wäre, wenn die Aufbereitung der SML-Daten in dem Wemos selbst passiert und dieser dann gleich als HTTPPushSensor die Daten an PRTG übermittelt. Das macht aber erst Sinn, wenn ich mal mehrere SML-Zähler als Beispiele ausgelesen habe. Schon mein Zähler konnte ich nur eingeschränkt decodieren..Ich vermute ja nicht, dass PRTG selbst irgendwann selbst SML-Datensttöme direkt lesen kann.

Zwischenstand

Mit viel Elan gestartet, immer wieder verzögert und mittlerweile durch freie Software wie Tasmota überholt. Anders kann ich dieses nicht abgeschlossene Projekt nicht beschreiben. Aber ich habe viel dabei gelernt und Spaß hat es auch gemacht. Vielleicht können Sie von dem ein oder anderen Code noch etwas mitnehmen aber produktiv werden ich ihn wohl nicht mehr nehmen.

Weitere Links