Bewässerungsanlage - Programmierung - Wemos D1 Mini

Sie befinden sich hier:
Smart Home Lösungen
»
Bewässerungsanlage im Eigenbau
»
Programmierung Microcontroller (Wemos D1 Mini)

Ein Microcontroller als Verbindung zu FHEM

Ein Wemos D1 Mini ist ein 32-Bit Microcontroller mit integrierter WiFi Schnittstelle basierend auf einem ESP8266 Chip. In meiner Bewässerungsanlage dient er zur Steuerung der Relais (Ventile und Pumpe) aus FHEM heraus über das MQTT-Protokoll.

Die implementierte Logik und den dazugehörigen Sketch finden Sie hier.

Kommunikation Wemos D1 Mini ⇔ FHEM

Bei der Kommunikation zwischen dem Wemos D1 Mini und FHEM habe ich mich für das MQTT-Protokoll entschieden. Es sendet und empfängt nur sehr kleine Datenpakete und ist für IoT-Anwendungen ideal. Sie können damit Befehle an den Wemos D1 Mini senden und auch Informationen bzw. Rückmeldungen vom Microcontroller empfangen und in FHEM weiter verarbeiten.

Voraussetzung hierfür ist ein MQTT-Server (ein sogenannter Broker), der Informationen empfangen und auch anderen Geräten wieder zur Verfügung stellen kann. In der IoT-Welt ist es somit möglich, dass z.B. ein Gerät Informationen an den MQTT-Server sendet (publish), ein anderes IoT-Gerät diese Information abonniert (subscribe) und selbständig auf Grund deren Änderung andere Aktionen ausführt. Somit können sich die IoT-Geräte quasi miteinander „unterhalten“. Der MQTT-Server (Broker) dient hierbei als „Moderator“.

Soweit habe ich es bei der Bewässerungsanlage nicht getrieben. Hier sendet und empfängt der Wemos D1 Mini Informationen, die in FHEM verarbeitet werden. Andere IoT-Geräte, wie z.B. meine Pegelsonde, liefern zwar ihren Beitrag, aber ohne dass die Bewässerungsanlage selbständig auf deren Input reagiert.


Funktionsweise MQTT-Protokoll

Ich möchte mich jetzt nicht in ewig langen Erklärung zu MQTT verlieren, zumal es sie im Netz zur Genüge gibt, z.B. unter https://www.opc-router.de/was-ist-mqtt/. Nichtsdestotrotz möchte ich kurz auf ein paar Begriffe eingehen, damit das Verstehen der folgenden Zeilen leichter fällt.

MQTT basiert auf dem Prinzip von senden (publish) zu einem MQTT-Server (Broker) und abonnieren (subscribe), um von einem MQTT-Server Informationen zu empfangen. Published und subscribed werden Topics. Der Informationsinhalt wird als Payload bezeichnet. Das alles erfolgt mit einem Bezug auf eine eindeutige ClientId.

Konkretes Beispiel eines Topics, welches gesendet oder empfangen werden kann:
IoT3:/aussen/temp 25

  • IoT3 ... ClientId (Name des Gerätes)
  • /aussen/temp ... Topic (frei wählbarer Name, wählbare Struktur)
  • 25 ... Payload (Wert des Topics)

Topics können einzeln oder zu mehreren als JSON Array gesendet und empfangen werden. Wie man sich die Struktur der Topics aufbaut, ist Jedem selbst überlassen. Eine gewisse Systematik ist aber mit Blick auf evtl. zukünftige Erweiterungen ratsam.


Programmierung mit der Arduino-IDE

Die Programmierung des Wemos D1 Mini erfolgte mit der Arduino-IDE. Ich verwendete eine etwas ältere Version (1.8.15), da ich in der aktuellen Version (2.0.4) den Serial Monitor nicht zum Laufen bekam. Den benötigte ich aber für Debug-Ausgaben während der Programmierung.

In der Arduino-IDE hatte ich folgendes Board eingestellt:

  • Tools >> Board >> ESP8266 Boards (2.5.0) >> LOLIN (Wemos) D1 R2 & mini
  • LOLIN (Wemos) D1 R2 & mini Pro oder Wemos D1 R1 funktionieren ebenso

Ich hatte bewusst eine etwas ältere Version der ESP8266 Board Library gewählt (2.5.0), da mein Wemos in der aktuellen Version nicht erkannt wurde. Innerhalb der Wemos-Versionen gibt es diverse Unterschiede. Meine Version (R2) liefert z.B. an den Ausgängen 5V, wodurch ich meine 5V-Relais sicher schalten kann. Neuere Versionen liefern nur noch 3,3V.

Das Board selbst wird an einem USB-Port des Rechners angeschlossen. Der programmierte - sogenannte Sketch - kann letztendlich direkt aus der Arduino-IDE auf das Board geladen werden.

Den Sketch, der die Funktionalitäten wie auf den vorherigen Seiten beschrieben beinhaltet, habe ich auf GitHub geladen. Dort können Sie den Code analysieren oder Code-Sequenzen davon verwenden:

Hier im Folgenden auf dieser Seite sind die wesentlichen Code-Sequenzen detailliert erläutert.


Sketch - Bewässerungsanlage gesteuert über einen Wemos D1 Mini per MQTT - die Details

Ein Sketch besteht im Wesentlichen aus zwei Funktionen. Dies ist einleitend eine Setup-Funktion, die nur einmal beim Start durchlaufen wird. Hierin wird u.a. der Start einzelner Komponenten vorgenommen (WiFi, Webserver, MQTT-Server) und es erfolgt z.B. das Abonnieren (subscribe) verschiedener MQTT-Topics. Die eigentliche Programmlogik liegt in der Loop-Funktion, die fortlaufend durchlaufen wird. Allem gemeinsam vorgelagert ist ein Deklarationsteil. Darin erfolgen z.B. das Laden der benötigten Libraries und das Deklarieren von Variablen, Konstanten und Objekten.

Deklarationsteil

Hier die wesentlichen Auszüge aus dem Deklarationsteil. Zu Beginn werden die notwendigen Libraries geladen:


#include <ESP8266WiFi.h>                                // WiFi library
#include <PubSubClient.h>                               // MQTT library
#include <ESP8266WebServer.h>                           // Webserver library

Es folgt die Deklaration von Konstanten und Variablen etc., z.B. für die WiFi-Verbindung:


const char* SSID = "<<SSID>>";                          // WiFi SID
const char* PASSWORD = "<<geheim>>";           			// WiFi Passwort
const char* MQTT_BROKER = "192.168.0.200";              // IP Adresse des MQTT servers

const char* clientId = "IoT3";                          // eindeutiger Name des Wemos D1 Mini

IPAddress ip(192, 168, 0, 40);                          // feste IP Adresse dieses Gerätes (Wemos D1 Mini)
IPAddress gateway(192, 168, 0, 100);                    // IP Adresse des Gateways (z.B. des Routers)
IPAddress subnet(255, 255, 255, 0);

Bildung der Relation Pin - GPIO:
D8 (GPIO 15) konnte ich nicht verwenden. Bei Anschluß eines Relais an D8 ist der Wemos beim Startvorgang regelmäßig abgestürtzt. Lt. Recherche muss D8 beim Startvorgang auf definiert „low“ stehen, was man durch einen PullDown Widerstand von 10kOhm zwischen D8 und GND erreichen kann. Mir ist dies allerdings nicht gelungen, weshalb ich auf D4 (GPIO2) ausgewichen bin. D4 wird zwar auch für die interne LED verwendet. Das stört mich aber nicht. Ich verwende die LED nur für den Startvorgang im setup() und schenke ihr danach keine Bedeutung mehr.


int const aRelayPin[8]={16,5,4,0,2,14,12,13};           // Array Pin : GPIO
                                                        // GPIO: D0:16, D1:5, D2:4, D3:0, D4:2, D5:14, D6:12, D7:13
                                                        // D4:2 = BUILDIN_LED
int const NoOfPins = 8;
	:
	:

Die Initialisierung der Objekte:


WiFiClient espClient;									// WiFi-Objekt
PubSubClient client(espClient);							// MQTT-Objekt		
ESP8266WebServer webServer(80);							// Webserver-Objekt (startet mit Port 80)

Die Setup-Funktion

In der Setup-Funktion erfolgt i.W. die Initialisierung und der Start notwendiger Dienste und Komponenten. Die Logik für jede einzelne Komponente wurde in einer eigenständigen Funktion gekapselt und letztendlich in der setup() Funktion vereint.

Start WiFi:


void setup_wifi() {

  Serial.println("Connecting to WiFi");

  WiFi.mode(WIFI_STA);                                    // station mode, d.h. er verbindet sich zu einem existierenden AccessPoint, z.B. einem Router
  WiFi.begin(SSID, PASSWORD);

                                                          // Konfiguration der statischen IP-Adresse
  if (!WiFi.config(ip, gateway, subnet)) {
    Serial.println("STA Failed to configure");
  }

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

Start Webserver:


void setup_web() {

  Serial.print("Webserver ");

  webServer.onNotFound([](){                             // Definition der 404-Fehlerseite
    webServer.send(404, "text/plain", "Page not found!");  
  });
 
  webServer.on("/", []() {                              // Definition der WebRoot-Seite
    webServer.send(200, "text/plain", "Webserver is running!");
  });
  
  webServer.on("/restart", []() {                       // Definition der Seite bez. des Kommandos für den Restart des MQTT-Servers
    restartMQTT();
  });
  webServer.begin();                                    // Start Webserver
  Serial.println("Started");
}

Start MQTT-Server:


void setup_mqtt() {
  client.setServer(MQTT_BROKER, 1883);					// Deklaration des MQTT-Servers (Broker) auf Port 1883
  client.setCallback(callback);							// Definition der Callback-Funktion
}

Start MQTT-Server im Detail

Mit der einfachen Deklaration des MQTT-Servers per client.setServer(MQTT_BROKER, 1883) ist noch nicht viel erreicht. Wichtig ist, dass der MQTT-Client sich auch zu dem MQTT-Server verbindet. Hierzu dient die Funktion connect_mqtt(). Sie wird zwar in der loop()-Funktion aufgerufen, gehört aber für mein Verständnis auch mit zum Setup. Sie wird immer dann aufgerufen, wenn keine Verbindung zum MQTT-Server besteht. D.h. beim ersten Durchlauf der loop() Funktion und wenn die Verbindung zum MQTT-Server irgendwann verloren geht. Dann sollte sich der MQTT-Client automatisch wieder verbinden.


void connect_mqtt() {

  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");

												// verbinden und setzen der last will message (mode)
    if (client.connect(clientId,NULL,NULL,"/aussen/mode",0,false,"Offline")) {
      Serial.println("connected");
         
      client.publish("/aussen/mode", "Online");                       			   // publish mode
      client.publish("/aussen/RSSI", String(WiFi.RSSI()).c_str());    			   // publish RSSI
     for (i=0;i<NoOfPins;i++) {
        getRelayStatus(i+1);                              				// subscribe alle Relais (R1 - R8)
        sPayload = "/aussen/R"+String(i+1)+"/set";
        if (client.subscribe(sPayload.c_str())) Serial.println ("Subscribed R"+String(i+1));
      }
      if (client.subscribe("/aussen/P1/set")) Serial.println ("Subscribed P1");    // subscribe Programm 1
      if (client.subscribe("/aussen/STS/get")) Serial.println ("Subscribed STS");  // subscribe Statusabfrage
      
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");

      delay(5000);
    }
  }
}

Auf einige Schritte von connect_mqtt() möchte ich etwas näher eingehen, z.B. auf client.connect():


client.connect(clientId,NULL,NULL,"/aussen/mode",0,false,"Offline")
NameBedeutung
clientIdist der eindeutige Name dieses Gerätes (ich hatte ihn zu Beginn als IoT3 definiert)
NULL,NULLder Verbindungsaufbau erfolgt ohne Username / Passwort
"/aussen/mode"Topic für die LastWill-Message
0Quality of service; 0 = höchstens einmaliges Senden ohne Warten auf Rückmeldung vom MQTT-Server
falseretain flag; false = die LastWill-Message wird nicht für andere Subscriber vorgehalten
"Offline"LastWill-Message

Ein Client, der sich zu einem MQTT-Server verbindet, hat die Möglichkeit, einen sogenannten letzten Willen (LastWill-Message) bekannt zu geben. Dieser besteht aus einem Topic und einer Nachricht. Dieser letzte Wille wird vom MQTT-Server als Standard-MQTT-Nachricht für den Client vorgehalten, sobald der MQTT-Server eine Verbindungsunterbrechung zum Client feststellt. Hierüber lässt sich z.B. erkennen, ob ein MQTT-Client gerade "Online" oder "Offline" ist.

Während des connects wird das Topic "/aussen/mode" mit Payload "Online" gesendet. Damit wird signalisiert, dass der Client online ist. Da das Topic "/aussen/mode" auch gleichzeitig für die LastWill-Message verwendet wird, steht es solange auf "Online", bis der MQTT-Client die Verbindung zum MQTT-Server verliert. Dann schaltet das Topic automatisch auf "Offline" (LastWill-Message).


      client.publish("/aussen/mode", "Online");

"/aussen/RSSI" ist ein „nice to have“ Topic, welches einfach die Information über die WiFi-Signalstärke published.


      client.publish("/aussen/RSSI", String(WiFi.RSSI()).c_str());

Beim Start werden alle verwendeten Pins abonniert (subscribed), d.h. wenn über FHEM Änderungen gesendet werden (Relais auf "ON" oder "OFF"), so reagiert dieser Client entsprechend der implementierten Logik (siehe callback Funktion). Ein entspr. Befehl würde dann wie folgt aussehen:

  • /aussen/R1/set ON
  • /aussen/R1/set OFF

     for (i=0;i<NoOfPins;i++) {
        getRelayStatus(i+1);
        sPayload = "/aussen/R"+String(i+1)+"/set";
        if (client.subscribe(sPayload.c_str())) Serial.println ("Subscribed R"+String(i+1));
      }

Letztendlich wird noch ein Topic subscribed, über das ein implementiertes Gießprogramm gestartet werden kann (/aussen/P1/set) und über das der Status aller Relais abgerufen werden kann(/aussen/STS/get).


      if (client.subscribe("/aussen/P1/set")) Serial.println ("Subscribed P1");
      if (client.subscribe("/aussen/STS/get")) Serial.println ("Subscribed STS");

Die gesamte setup() Funktion sieht dan wie folgt aus:


void setup() {

  Serial.begin(9600);
  Serial.println("start");

  for (i=0;i<NoOfPins;i++) {                      // Initialisierung der verwendeten Pins als inaktiv (OFF)
    digitalWrite(aRelayPin[i], HIGH);
    pinMode(aRelayPin[i], OUTPUT);
  }

  digitalWrite(BUILTIN_LED, LOW);                 // Einschalten der BUILTIN_LED
  pinMode(BUILTIN_LED, OUTPUT);                   // Initialisierung der BUILTIN_LED pin als Output

  setup_wifi();
  setup_web();
  setup_mqtt();

  delay(2000);
  digitalWrite(BUILTIN_LED, HIGH);                 // Ausschalten der BUILTIN_LED
}

Die Loop-Funktion

Die Loop-Funktion wird kontinuierlich durchlaufen, d.h. hierin gehört z.B. eine connect-Funktion für den erstmaligen Connect zu einem MQTT-Server oder um nach einem unerwarteten Verbindungsabbruch ein reconnect zum MQTT-Server zu ermöglichen und die client.loop() Methode, die ständig darüber wacht, ob zu Topics, die subscribed wurden, Informationen gesendet werden. Hierauf wird dann in der callback Funktion reagiert.

Hier ein Beispiel für eine ganz einfache loop() Funktion:


void loop() {

  if (!client.connected()) {
    connect_mqtt();
  }

  client.loop();
}

In meinem Fall hat die simple Loop-Funktion nicht ausgereicht, da ich entsprechend meines Anforderungsportfolios noch weitere Funktionalitäten implementiert habe:


  webServer.handleClient();

Der Webserver wartet ebenfalls auf Input. Falls - weshalb auch immer - der MQTT-Client es nicht schafft, sich zum MQTT-Server zu verbinden, kann der Start zusätzlich über den Webserver getriggert werden.

Des Weiteren enthält die loop() Funktion zwei Emergency-Exits, die ein laufendes Gießprogramm beenden (bProg1), sofern die maximale Programmlaufzeit überschritten oder Laufzeit einzelner Relais überschritten wurden.


  if (bProg1) {                                                      // Start Programm 1
     if (millis() >= lMaxTimeProg1) {                                // Notstop Programm 1
      resetProg1();
      Serial.println("Emergency exit P1");
    } else {
      bProg1 = runProg1(iStepProg1, aTime, lStartTimeProg1);
    }
  }

  if (bIsRelayOn == true && millis() >= lMaxTimeRelay){              // Notstop für ein einzelnes Relais
    for (i=8;i>=1;i--) {
      switchRelayOff(i);
    }
    Serial.println("switch off (max ON time) completed");
    bIsRelayOn = false;
  }

Somit sieht die gesamte loop() Funktion wie folgt aus:


void loop() {

  if (!client.connected()) {
    connect_mqtt();
  }

  client.loop();

  webServer.handleClient();

  if (bProg1) {
     if (millis() >= lMaxTimeProg1) {
      resetProg1();
      Serial.println("Emergency exit P1");
    } else {
      bProg1 = runProg1(iStepProg1, aTime, lStartTimeProg1);
    }
  }

  if (bIsRelayOn == true && millis() >= lMaxTimeRelay){
    for (i=8;i>=1;i--) {
      switchRelayOff(i);
    }
    Serial.println("switch off (max ON time) completed");
    bIsRelayOn = false;
  }

}

Callback-Funktion

In der callback-Funktion wird auf Änderungen an Topics reagiert, die dieser Client subscribed hat. Hier nochmal zusammengefasst alle Topics, die in der Setup-Funktion subscribed wurden:

TopicBedeutung
/aussen/P1/setwartet auf ein Startsignal für das Gießprogramm in der Form n|n|n.
/aussen/STS/getwartet auf ein Signal, um den Relaisstatus aller 8 Relais zurückzugeben
/aussen/R1/setwartet auf ein ON oder OFF Befehl, um das Relais 1 ein- bzw. auszuschalten
/aussen/R2/setwartet auf ein ON oder OFF Befehl, um das Relais 2 ein- bzw. auszuschalten
: :
/aussen/R8/setwartet auf ein ON oder OFF Befehl, um das Relais 8 ein- bzw. auszuschalten

Das Gießprogramm (Payload) wird in der Form n|n|n erwartet, wodurch sich die Zeiten der einzelnen Gießkreise flexibel steuern lassen, z.B.:

PayloadBedeutung
8|8|5entspr. meinem Gießprogramm „Viel“, d.h. Gießkreis 1 und 2 (Rabatte) werden je 8 min. gegossen, Gießkreis 3 (Terassentöpfe) 5 min.
5|5|3entspr. meinem Gießprogramm „Wenig“, d.h. Gießkreis 1 und 2 (Rabatte) werden je 5 min. gegossen, Gießkreis 3 (Terassentöpfe) 3 min.
0|0|3entspr. meinem Gießprogramm „Töpfe“, d.h. Gießkreis 1 und 2 (Rabatte) werden nicht gegossen, Gießkreis 3 (Terassentöpfe) 3 min.
0|0|0entspr. meinem Gießprogramm „Pause“, d.h. es wird nichts gegossen. Im Gegenteil: Ein laufendes Gießprogramm wird sofort gestoppt. Das entspricht quasi einem OFF.

Die Parameter n|n|n sind als Readings in FHEM hinterlegt und können somit jederzeit ohne Programmieraufwand nachjustiert werden.

Die komplette callback-Funktion sieht wie folgt aus:


void callback(char* topic, byte* payload, unsigned int length) {
  char cRelayNo;
  int iRelayNo, aMin[3];
 
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.println("] ");

  sTopic ="";
  for (int i = 0; i < strlen(topic); i++) {
    sTopic += (char)topic[i];
  }
  Serial.println(sTopic);

  sPayload = ""; 
  for (int i = 0; i < length; i++) {
    sPayload += (char)payload[i];
  }
  Serial.println(sPayload);
  
  if (sTopic.substring(8,10) == "P1") {                           // Start Programm P1
 
    lMaxTimeProg1 = 0;
        
    char* aChar = &sPayload[0];
    sscanf(aChar, "%d|%d|%d", &aMin[0], &aMin[1], &aMin[2]);      // Split Payload in Array

    for (int i=0; i < 3; i++) {
      if (aMin[i]<0 || aMin[i]>20) aMin[i]=0;                     // Prüfung auf sinnvollen Wertebereich
      aTime[i] = (uint32_t)aMin[i] * 1000 * 60;
      lMaxTimeProg1 +=aTime[i];
    }

    if (aMin[0] <= 0 && aMin[1] <= 0 && aMin[2] <= 0) {
      resetProg1();
    } else {
      lStartTimeProg1 = millis();
      lMaxTimeProg1 = (uint32_t)lStartTimeProg1 + lMaxTimeProg1 + 10000; // Berechnung Zeitpunkt Notstop
      if (aMin[0] > 0) iStepProg1 = 0;
      else if (aMin[1] > 0) iStepProg1 = 2;
      else if (aMin[2] > 0) iStepProg1 = 4;
      setProgramStatus(1, "ON");
      Serial.println ("Start P1 " + String(iStepProg1));
      bProg1 = true;
    }

  } else if (sTopic.substring(8,9) == "R") {                       // Schalten eines einzelnen Relais
    cRelayNo = sTopic.substring(9,10).charAt(0);
    if(isDigit(cRelayNo)) {
      iRelayNo = cRelayNo - '0';
      relayAction(iRelayNo, sPayload);
      if (sPayload =="ON") {
        lMaxTimeRelay = millis() + MAXMIN;                         // Berechnung Zeitpunkt Notstop
        Serial.println(String(lMaxTimeRelay));
        bIsRelayOn = true;
      }
    }
 
  } else if (sTopic.substring(8,11) == "STS") {                   // Statusabfrage Relais
    getRelayStatusAll();
  }
}

Das Gießprogramm

Um die Sache zu komplettieren, hier noch das implementierte Gießprogramm. Aus Payload 8|8|5 ergibt sich hierbei folgende Schrittfolge:

SchrittBedeutung
0Wenn erste Zahl im Payload > 0, dann wird Gießkreis 1 (Relais 2) gestartet, Startzeit wird sich gemerkt.
1Die Gießdauer für den ersten Gießkreis ist abgelaufen. Relais 2 wird wieder ausgeschaltet.
2Wenn zweite Zahl im Payload > 0, dann wird Gießkreis 2 (Relais 3) gestartet, Startzeit wird sich gemerkt.
3Die Gießdauer für den zweiten Gießkreis ist abgelaufen. Relais 3 wird wieder ausgeschaltet.
4Wenn dritte Zahl im Payload > 0, dann wird Gießkreis 3 (Relais 4) gestartet, Startzeit wird sich gemerkt.
5Die Gießdauer für den dritten Gießkreis ist abgelaufen. Relais 4 wird wieder ausgeschaltet. Da dies der letzte Gießkreis ist, wird hier auch noch das Pumpenrelais (Relais 1) ausgeschaltet.

Durch diesen if / else if Aufbau würde z.B. bei Payload 0|0|3 direkt mit Schritt 4 begonnen.


bool runProg1(int &iStep, uint32_t aTime[3], uint32_t &lStartTime) {
    if (iStep == 0 && aTime[0]>0) { 
      relayAction(2, "ON");                                                            // ON OFF OFF
      lStartTime = millis();
      iStep++;
      Serial.println("P1 start step 1 " + String(aTime[0]));
    } else if (iStep == 1 && aTime[0]>0 && (uint32_t)(millis() - lStartTime) >= aTime[0]) { // weiter zum nächsten Schritt
      relayAction(2,"OFF");
      iStep++;
      Serial.println("P1 end step 1 " + String(lStartTime));
    } else if (iStep == 2 && aTime[1]>0) {                                             // OFF ON OFF
      relayAction(3,"ON");
      lStartTime = millis();
      iStep++;
      Serial.println("P1 start step 2 " + String(lStartTime));
    } else if (iStep == 3 && aTime[1]>0 && (uint32_t)(millis() - lStartTime) >= aTime[1]) { // weiter zum nächsten Schritt
      relayAction(3,"OFF");
      iStep++;
      Serial.println("P1 end step 2 " + String(lStartTime));
    } else if (iStep == 4 && aTime[2]>0) {                                             // OFF OFF ON
      relayAction(4,"ON");
      lStartTime = millis();
      iStep++;
      Serial.println("P1 start step 3 " + String(lStartTime));
    } else if (iStep == 5 && aTime[2]>0 && (uint32_t)(millis() - lStartTime) >= aTime[2]) {
      relayAction(4,"OFF");                                                            // OFF OFF OFF
      relayAction(1,"OFF");                                                            // Pumpe ausschalten
      setProgramStatus(1, "OFF");													   // Programm beenden
      Serial.println ("End P1");
      return false;
    }
    
    return true;
}

setProgramStatus() gibt die Rückmeldung an FHEM, dass das Programm beendet wurde inkl. der aktuellen WiFi-Signal Qualität.


void setProgramStatus(int iProgNo, String sAction) {
  sTopic = "/aussen/P"+String(iProgNo)+"/state";
  if (client.publish(sTopic.c_str(), sAction.c_str())) Serial.println ("Published P"+String(iProgNo)+"  "+sAction);
  client.publish("/aussen/RSSI", String(WiFi.RSSI()).c_str());    						// publish RSSI
}

Ergänzend gehört hierzu auch noch die Reset Funktion, falls die max. Gießzeit überschritten oder das Gießen abgebrochen wurde:


void resetProg1() {

    relayAction(2,"OFF");
    relayAction(3,"OFF");
    relayAction(4,"OFF");
    relayAction(1,"OFF");
    setProgramStatus(1, "OFF");
    Serial.println ("Reset P1");
    bProg1 = false;
}

Schaltung einzelner Relais

Einzelne Relais werden über folgende Funktionen geschaltet. Dabei wird auch berücksichtigt, dass die Pumpe (R1) immer 2 sec. vor einem Ventil eingeschaltet sein muss und max. ein Ventil-Relais (R2-R5) eingeschaltet sein darf:


void relayAction(int iRelayNo, String sAction) {
  if (iRelayNo >= 1 && iRelayNo <= 8  && sAction != "") {
    if (sAction == "ON") {
      switchRelayOn(iRelayNo);
    } else if (sAction == "OFF") {
      switchRelayOff(iRelayNo);
    }
  }
}

void switchPumpOn(int iRelayNo) {
  if (digitalRead(aRelayPin[iRelayNo-1]) == HIGH) {
    digitalWrite(aRelayPin[iRelayNo-1], LOW);
    getRelayStatus(iRelayNo);
    delay(2000);                               				  // 2 sec. Pause zum Druckaufbau
  }
}

void switchRelayOn(int iRelayNo) {
  if (iRelayNo == 1) {                                        // 1 = Pumpen Relay
    switchPumpOn(1);
  } else {
    if (iRelayNo <=5) {
      for (i=2;i<=5;i++) {                                    // ausschalten aller anderen Relais
        if (i!=iRelayNo) {
           switchRelayOff(i);
        }
      }
      switchPumpOn(1);                                        // Pumpe einschalten, falls noch nicht erfolgt
    }
    
    if (digitalRead(aRelayPin[iRelayNo-1]) == HIGH) {
      digitalWrite(aRelayPin[iRelayNo-1], LOW);               // einschalten EINES Relais
      getRelayStatus(iRelayNo);
    }
  }
}

void switchRelayOff(int iRelayNo) {
  if (digitalRead(aRelayPin[iRelayNo-1]) == LOW) {
    digitalWrite(aRelayPin[iRelayNo-1], HIGH);
    getRelayStatus(iRelayNo);
  }
}

Rückmeldung Relaisstatus

Über folgende Funktionen kann der aktuelle Status eines oder aller Relais (ON / OFF) an den MQTT-Server published werden:


void getRelayStatus(int iRelayNo) {
 
  if (digitalRead(aRelayPin[iRelayNo-1]) == LOW) {
    sRelayStatus = "ON";
  } else {
    sRelayStatus = "OFF";
  }
  sTopic = "/aussen/R"+String(iRelayNo)+"/state";
  if (client.publish(sTopic.c_str(), sRelayStatus.c_str())) Serial.println ("Published to R"+String(iRelayNo)+"  "+sRelayStatus);
  client.publish("/aussen/RSSI", String(WiFi.RSSI()).c_str());    // publish RSSI
}

void getRelayStatusAll() {
  for (i=0;i>NoOfPins;i++) {
    getRelayStatus(i+1);
  }
}

Reconnect MQTT-Client, über einen Webserver getriggert

Zum Abschluss noch die Funktion, mit der ein Reconnect des MQTT-Clients über den Webserver-Befehl ausgelöst wird:


void restartMQTT(){ 
  client.disconnect();
  Serial.println ("MQTT disconnected");
  setup_mqtt();
  webServer.send(200, "text/plain", "OK");
}

Zusammenfassung

Ich hoffe, dass die detaillierten Erläuterungen verständlich und anhand der Code-Sequenzen nachvollziehbar waren.

Da eine individuelle Bewässerungsanlage - wie der Name sagt - individuell ist, gehe ich nicht davon aus, dass der komplette Sketch 1:1 in Ihr eigenes Projekt übernommen werden kann. Da ich mich aber beim Aufbau meiner Anlage über jede Inspiration und jeden verwendbaren Codeschnipsel gefreut habe, finden Sie den gesamten Sketch über folgenden Link auf GitHub zum analysieren und ausschlachten.

Wie die Steuerung von FHEM aus erfolgt, finden Sie auf der nächsten Seite.


» Zurück zum Entwurf der Steuerungslogik
» Weiter zur Steuerung mit FHEM

Kontakt

Senden Sie mir Ihre Fragen oder Anregungen über die Kontaktbox oder direkt per Email. Sie können mich natürlich auch über die gängigen sozialen Netze erreichen.

kontakt@kaempf-nk.de

Fragen / Anregungen?

Sicherheitsabfrage:
Datenschutzhinweis: Die eingegebenen Daten werden nicht an Dritte weitergegeben.