MQTT Einführung Teil 3

Aus FHEMWiki

MQTT Einführung Teil 3

Zeit sich die Hände schmutzig zu machen


NOCH NICHT GANZ FERTIG


Übersicht

Im folgenden basteln wir einen einfachen Temperatursensor.

Das ist nicht aufregend. Jedoch ist der Code im Originalzustand sehr gut überschaubar. Man kann daher gut die Änderungen sehen.

Originales Projekt

hier bitte nachlesen

Mit freundlicher Genehmigung des Autors hier der originale Quellcode:

#include "DHT.h"
 
#define DHTPIN 9     
#define DHTTYPE DHT22 //DHT11, DHT21, DHT22
 
DHT dht(DHTPIN, DHTTYPE);
 
void setup() 
{
  Serial.begin(9600); 
  Serial.println("DHT22 - Test!");
 
  dht.begin();
}
 
void loop() 
{
  float h = dht.readHumidity();     //Luftfeuchte auslesen
  float t = dht.readTemperature();  //Temperatur auslesen
 
  // Prüfen ob eine gültige Zahl zurückgegeben wird. Wenn NaN (not a number) zurückgegeben wird, dann Fehler ausgeben.
  if (isnan(t) || isnan(h)) 
  {
    Serial.println("DHT22 konnte nicht ausgelesen werden");
  } 
  else
  {
    Serial.print("Luftfeuchte: "); 
    Serial.print(h);
    Serial.print(" %\t");
    Serial.print("Temperatur: "); 
    Serial.print(t);
    Serial.println(" C");
  }
}

Benötigte Hardware

  • Arduino
  • DHT-22 Sensor (nicht der Beste, aber seine Verwendung hält den Code einfach)
  • 10 kOhm Widerstand
  • Steckbrett
  • Drähte

Zusätzlich

  • Ethernetshield mit W5100 Chip
  • MQTT Broker (Testweise: 37.187.106.16 => IP von test.mosquitto.org)

Kurze Beschreibung was der originale Code tut

  • Zeilen 1-6: Library einbinden, PIN festlegen, initialisieren.
  • Zeilen 8-14: Serielle Schnittstelle konfigurieren, netten Text schreiben...
  • Zeilen 18-19: Sensorwerte auslesen
  • Zeilen 21-25: Fehlerbehandlung
  • Zeilen 26-34: Sensorwerte mit Beschreibung an die serielle Schnittstelle senden

Bibliotheken die zu installieren sind

Anmerkung:

Die Arduino IDE bietet viele Möglichkeiten Bibliotheken einzubinden. Bevorzugt verwendet man den eingebauten Library-Manager. Wer das nicht will, kann auch die Bibliotheken als ZIP Datei herunterladen, und dann mit "Bibliothek aus ZIP Datei hinzufügen" einbinden.

Im folgenden die Links zu den ZIP Dateien Für den originalen Sketch

In der Adruino IDE gibt es eine Warnung, WARNUNG: Unberechtigter Ordner .github in der Bibliothek 'DHT sensor library'; diese kann ignoriert werden. Da die Meldung aber immer kommt, löschen wir einfach besagten Ordner ".github" im Ordner der DHT Sensor Library.

Für MQTT brauchen wir drei weitere Libraries:

Notwendige Änderungen

Im folgenden bauen wir den Sketch um. Zunächst prüfen wir, was am Sketch an sich geändert werden muss, anschließend fügen wir den MQTT Code hinzu. Zuletzt nutzen wir noch einige MQTT Features für weitere Optimierungen.

Änderungen am Skript selbst

Die einfachste Möglichkeit ein Skript auf MQTT umzustellen, ist das Ändern der Ausgabe von Serial.print in ein mqtt.publish. Hier ergibt sich im Sketch ein Problem: Pubsubclient kann keine Werte vom Typ "float" als Payload versenden. Daher müssen wir zwei Konvertierungen hinzufügen:

  • dtostrf(h,6, 1, humidity);
  • dtostrf(t,6, 1, temperature);

machen den Trick: Nehme float h, benutze insgesamt 6 Stellen, 1 Nachkommastelle und schreibe das Ergebnis in humidity.

Weiterhin ist es angenehmer, Variablen etc. am Anfang eines Sketches zu definieren, das machen wir auch noch.

Änderungen für MQTT

Am einfachsten ist, wir nehmen uns einen der mitgelieferten MQTT Beispielsketche: mqtt_reconnect_nonblocking. Das müssen wir eigentlich nur an den jeweiligen Stellen in den originalen Sketch kopieren. Dabei sind die IP Adressen anzupassen und eine MAC Adresse zu vergeben.

Statt oder zusätzlich zu unserer Ausgabe über die serielle Schnittstelle fügen wir noch einen client.publish ein, so dass die Werte auf dem Broker landen.

Der Vollständigkeit halber, hier der Sketch für das nicht - blockierende MQTT reconnect:

/*
 Reconnecting MQTT example - non-blocking

 This sketch demonstrates how to keep the client connected
 using a non-blocking reconnect function. If the client loses
 its connection, it attempts to reconnect every 5 seconds
 without blocking the main loop.

*/

#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>

// Update these with values suitable for your hardware/network.
byte mac[]    = {  0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED };
IPAddress ip(172, 16, 0, 100);
IPAddress server(172, 16, 0, 2);

void callback(char* topic, byte* payload, unsigned int length) {
  // handle message arrived
}

EthernetClient ethClient;
PubSubClient client(ethClient);

long lastReconnectAttempt = 0;

boolean reconnect() {
  if (client.connect("arduinoClient")) {
    // Once connected, publish an announcement...
    client.publish("outTopic","hello world");
    // ... and resubscribe
    client.subscribe("inTopic");
  }
  return client.connected();
}

void setup()
{
  client.setServer(server, 1883);
  client.setCallback(callback);

  Ethernet.begin(mac, ip);
  delay(1500);
  lastReconnectAttempt = 0;
}


void loop()
{
  if (!client.connected()) {
    long now = millis();
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      // Attempt to reconnect
      if (reconnect()) {
        lastReconnectAttempt = 0;
      }
    }
  } else {
    // Client connected

    client.loop();
  }

}

Optimierungsmöglichkeiten

Unfreiwilliger Netzwerktest

Jedesmal wenn die Schleife durchgelaufen wird, bekämen wir eine Nachricht mit der Temperatur und eine mit der Luftfeuchtigkeit. Das ist nicht so wirklich schlau. Hier bieten sich 3 Alternativen an:

  • Delay => dumme Idee, blockiert den Arduino, nicht benutzen
  • eine nicht-blockierende Methode für den Arduino nehmen => kann man machen
  • Senden nur bei Werteänderung => vermutlich das Ressourcenschonendste

Ich verwende Variante 3.

Wir benötigen dazu noch zwei weitere Variablen (h_alt und t_alt); jedesmal wenn die Sendeschleife durchlaufen wird, speichern wir h und t in diese Variablen. Zusätzlich benötigt unsere Schleife nach der Fehlerbehandlung noch einen if Zweig, der dafür sorgt, dass nichts getan wird wenn beide Werte sich nicht geändert haben.

Jetzt können wir MQTT anweisen, die letzten Messwerte retained zu senden. Sonst würden wir erst dann Werte bekommen, wenn sich entweder die Temperatur oder die Luftfeuchte verändert haben.

  • client.publish("zuHause/Arduino_1/Kueche/Kuehlschrank/Luftfeuchte",humidity);

wird zu

  • client.publish("zuHause/Arduino_1/Kueche/Kuehlschrank/Luftfeuchte",humidity, true);
  • das true dahinter ist das Flag für retained Messages

Nachtrag zum Thema Pausen

Mit den Codeänderungen oben haben wir schon mal ein dauerhaftes publishen der gleichen Daten verhindert. Jedoch gibt es natürlich noch einige technische Besonderheiten, die wir ebenfalls berücksichtigen sollten:

Bis jetzt führt unser Sketch bei jedem Durchlauf durch void.loop() eine Messung durch. Das hat zwei Folgen:

  • der Sensor ist im Dauerbetrieb
  • kleine Schwankungen führen natürlich zu einem erneuten publishen.

Wenn wir den Code, sagen wir, nur alle 2 Minuten ausführen würden, würderd der Sensor weniger belastet. Beim dem Verwendeten spielt es nicht wirklich eine große Rolle, aber es gibt Sensoren die auf einen derart häufigen Zugriff durchaus empfindlich reagieren können.

Gleichzeitig bekommen wir viele kleine Schwankungen (ich nenne sie mal Messungenauigkeiten) übermittelt, die uns eigentlich nicht interessieren.

Die einfache Variante wäre wieder ein Delay. Dieses würde recht schnell zu einem Fehler führen: Ein Delay blockiert. Für MQTT bedeutet es, dass keine Keep-alive Pakete mehr gesendet werden. Hätten wir eingehende Nachrichten, dann würden wir diese auch verpassen. Wir merken uns: nutze niemals Delays.

Die oben schon angesprochene Lösung, eine nicht blockierende Methode verwenden, ist sehr gut geeignet.

Wie geht das?

Zunächst zählt der Arduino seine Laufzeit in Millisekunden. Wir schreiben unseren Temperaturausleseteil in eine kleine Schleife.

  • zwei weitere Variablen
    • unsigned long previousMillis = 0; //Zählervariable, zählt Millisekunden seit dem letzten Funktionsaufruf nach oben
    • const long interval = 120000; //120000 Millisekunden aka 120 Sekunden, das Interval wie oft der Sensor überhaupt benutzt wird
  • Drei Zeilen Programmcode mehr:
unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    
    h = dht.readHumidity();     //Luftfeuchte auslesen
    t = dht.readTemperature();  //Temperatur auslesen
  }

Last Will & Testament

Retten wir auch die Statusmeldungen des Skripts nach MQTT. Dazu ergänzen wir den client.connect Aufruf:

  • if (client.connect("Arduino_1")) {

um folgendes:

  • if (client.connect("Arduino_1", "zuHause/Arduino_1", 0, true, "offline")) {
    • Arduino_1 nennt sich unser Sensor
    • "zuHause/Arduino_1" ist das Topic für dessen Statusmeldungen
    • 0 ist die QoS
    • true bedeutet, Nachricht retained senden
    • "offline" ist das, was auf dem Topic geschrieben werden soll, wenn die Verbindung abbricht
    • Anmerkung:
    • Unsauberes disconnect wird verursacht durch:
    • Abbruch der Netzwerkverbindung
    • Protokollfehler (z.B. ungültige Topics, fehlerhafte Payload)

Warum machen wir das?

  • beim Start des Sensors steht nun "online" im Status
  • bei einem Sensorfehler werden wir auf den Sensorfehler aufmerksam gemacht "Sensorfehler"
  • bricht die Netzwerkverbindung ab, steht automatisch "offline" in dem Topic
  • bei einer erfolgreichen Messung mit Datenübertragung steht wieder "online" im Topic

Auf diese Art und Weise haben wir eine aussagekräftige Zustandsmeldung in unserem Topic. Diese können wir bequem mit FHEM überwachen, um bei einem Fehler weitere Maßnahmen einzuleiten. Ein Vergleich des Alters der Sensorwerte wird überflüssig.


MQTT an sich

Wer keinen eigenen Broker hat und (für diesen einen Test) keinen aufsetzen will, kann testweise auch einen öffentlichen Broker benutzen: test.mosquitto.org stellt so einen bereit. Bitte beachten, dass dieser Broker wirklich öffentlich ist. Mit ping test.mosquitto.org bekommt man die IP Adresse, die man, ich möchte es nochmal betonen, zu Testzwecken, in seinen Sketch schreiben kann.

Wir benötigen für MQTT entsprechende Topics.

  • "zuHause/Arduino_1/Kueche/Kuehlschrank/Luftfeuchte"
  • "zuHause/Arduino_1/Kueche/Kuehlschrank/Temperatur"

== Daus ergibt sich letztlich folgender Sketch

#include <DHT.h>
#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>
 
#define DHTPIN 9     
#define DHTTYPE DHT22 //DHT11, DHT21, DHT22
 
DHT dht(DHTPIN, DHTTYPE);

byte mac[]    = {  0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED }; //eine MAC Adresse wählen, darf im eigenen Netz nur 1x vorkommen
IPAddress ip(192, 168, 5, 220); //eine gültige IP Adresse für das eigene Netz
IPAddress server(192, 168, 5, 2); //die Adresse wo der eigene MQTT Broker drauf läuft

void callback(char* topic, byte* payload, unsigned int length) {
  // handle message arrived
}

EthernetClient ethClient;
PubSubClient client(ethClient);

long lastReconnectAttempt = 0;

boolean reconnect() {
  if (client.connect("Arduino_1", "zuHause/Arduino_1", 0, true, "offline")) {
    // Once connected, publish an announcement...
    client.publish("zuHause/Arduino_1","online", true);
    // ... and resubscribe
    client.subscribe("inTopic");
  }
  return client.connected();
}
static char humidity[15]; //Speicherbereich reservieren um die Fechtigkeit zu speichern
static char temperature[15];
float h = 0.0;
float h_alt = 0.0;
float t = 0.0;
float t_alt = 0.0;
unsigned long previousMillis = 0; //Zählervariable, zählt Millisekunden seit dem letzten Funktionsaufruf nach oben
const long interval = 120000; //120000 Millisekunden aka 120 Sekunden, das Interval wie oft der Sensor überhaupt benutzt wird

void setup() 
{
  client.setServer(server, 1883);
  client.setCallback(callback);

  Ethernet.begin(mac, ip);
  delay(1500);
  lastReconnectAttempt = 0;
  
  Serial.begin(9600); 
  Serial.println("DHT22 - Test!");
 
  dht.begin();
}
 
void loop() 
{
  if (!client.connected()) {
    long now = millis();
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      // Attempt to reconnect
      if (reconnect()) {
        lastReconnectAttempt = 0;
      }
    }
  } else {
    // Client connected

    client.loop();
  }

unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    
    h = dht.readHumidity();     //Luftfeuchte auslesen
    t = dht.readTemperature();  //Temperatur auslesen
  }
    
  // Prüfen ob eine gültige Zahl zurückgegeben wird. Wenn NaN (not a number) zurückgegeben wird, dann Fehler ausgeben.
  if (isnan(t) || isnan(h)) 
  {
    Serial.println("DHT22 konnte nicht ausgelesen werden");
    client.publish("zuHause/Arduino_1","Sensorfehler",true); //true sendet die Nachricht retained, d.h. die Nachricht bleibt solange auf dem Broker, bis etwas neues kommt
  }
  else if (h == h_alt && t == t_alt)
  {
    //nix machen
  }
  else
  {
    client.publish("zuHause/Arduino_1","online", true);
    dtostrf(h,6, 1, humidity);
    dtostrf(t,6, 1, temperature);
    client.publish("zuHause/Arduino_1/Kueche/Kuehlschrank/Luftfeuchte",humidity, true);
    client.publish("zuHause/Arduino_1/Kueche/Kuehlschrank/Temperatur",temperature, true);
    h_alt = h; //den alten Messwert aufheben
    t_alt = t; //um nur bei Veränderung zu reagieren
    Serial.print("Luftfeuchte: "); 
    Serial.print(h);
    Serial.print(" %\t");
    Serial.print("Temperatur: "); 
    Serial.print(t);
    Serial.println(" C");
    
  }
}

Zusammenfassung

Um aus einem "normalen" Sketch einen MQTT Sketch zu machen muss man also:

  • Netzwerk konfigurieren
  • MQTT einrichten

Im wesentlichen kann man einfach den MQTT Beispielsketch "mqtt reconnect nonblocking" in seinen Sketch einbauen.

Geringe Änderungen im vorhandenen Code sind nötig, um diversen Typen von Variablen als MQTT Payload verwenden zu können

Als Bonus bekommen wir "vernünftige" Statuswerte für unseren Sensor, die sich einfach in FHEM abfangen und auswerten lassen. Als weiteren Bonus betrachte ich, dass die Netzwerklast sinkt. Zum Vergleich möchte ich hier die Jeelink Sensoren anführen, aber auch viele 433 MHz Sensoren, die einfach im "wenige Sekunden Takt" ihre Werte in der Gegend rumfunken. Von da her kann durch den Einsatz von MQTT-Techniken die Funkbelastung der Umgebung reduziert werden. Einerseits trägt man also weniger zur "Sensorischen Luftverschmutzung" bei, andererseits entlastet man auch sein FHEM vor der Aufgabe, unnötige Werte erst mal zu verwerfen.

Danke

Hier möchte ich mal Danke sagen. An Beta-User, der seit Teil 1 das Geschreibsel vorab durchliest und immer wertvolle Tipps und Änderungsvorschläge parat hat.

Links

Originales Projekt

Teil 1 der MQTT Einführung

Teil 2 der MQTT Einführung

Diskussionsthread im Forum