WLAN Aktor Markise

Aus TippvomTibb
Zur Navigation springen Zur Suche springen

Allgemeines

Leider hatte ich keinen Port mehr an den KNX-Rollladenaktoren zur Verfuegung, daher bin ich auf eine MQTT-WLAN-ESP-DuoRelais-Schaltung ausgewischen.

Hardware

Software

 
//ShutterBlindsAwningFunction with LC-Relay-ESP01-2R-5V
//tested 2022-08-06

#include <ESP8266WiFi.h>        // Include the Wi-Fi library
#include <PubSubClient.h>       // MQTT
#include "ESPDateTime.h"        // NTP
#include <ESP_EEPROM.h>         // to store non-volatile variables
#include "CRC.h"                // to check the reliability of the EEPROM-stored variables
#include "CRC8.h"

//CONFIGURATION
#define WLAN_SSID "XXX"
#define WLAN_PASSWORD "YYY"
#define MQTT_SERVER_IP "ZZZ"  // better to use the ip, DNS will slow down the speed
#define MQTT_MSG_BUFFER_SIZE	50
#define MQTT_DEBUG_MSG_BUFFER_SIZE 50
#define MQTTT_BASE_TOPIC "home/basement/0_terrace/awning/"  // my rooms are numbered so there is a consideration to replace the _ with / to use the 0 also for e.g. garden
#define MQTT_OUT_STATE MQTTT_BASE_TOPIC "state"
#define MQTT_OUT_RAW MQTTT_BASE_TOPIC "raw"
#define MQTT_OUT_ALIVE MQTTT_BASE_TOPIC "alive"
#define MQTT_OUT_RSSI MQTTT_BASE_TOPIC "RSSI"
#define MQTT_IN_TRIGGER MQTTT_BASE_TOPIC "trigger"
#define MQTT_IN_DIRECTION MQTTT_BASE_TOPIC "direction"
#define MQTT_IN_CONFIG MQTTT_BASE_TOPIC "config"

//#define SERIAL_DEBUG          // used in an early stage of development; conflicts with UART output for relais control
#define MQTT_DEBUG_OUTPUT     // the raw topic serves to output the values for debugging // TODO struct config
#define ALIVEINTERVAL 10000     // UpdateInterval in milliseconds of the topics 'alive' // TODO struct config

//WIFI
WiFiClient espClient;
const char * ssid     = WLAN_SSID;         // The SSID (name) of the Wi-Fi network you want to connect to
const char * password = WLAN_PASSWORD;     // The password of the Wi-Fi network
String mac;

//MQTT
const char * MQTTServer = MQTT_SERVER_IP;
PubSubClient MQTTClient(espClient); //Constructor
unsigned long lastMsg = 0;  // to remember the last timestamp of the 'ALVIE-publish' (heartbeat)
char msg[MQTT_MSG_BUFFER_SIZE]; // old style; sorry for mixing c and c++
char msgConfig[MQTT_DEBUG_MSG_BUFFER_SIZE]; // to store the config parameters; in setup there is no possibility to send out, so store it to send it in loop
long int value = 0; // don't needed anymore; its a counter replaced by DateTime

String topicBase = MQTTT_BASE_TOPIC ;
String inConfigTopic = MQTT_IN_CONFIG ;
String inTriggerTopic = MQTT_IN_TRIGGER ;
String inDirectionTopic = MQTT_IN_DIRECTION;
String outStateTopic = MQTT_OUT_STATE ;
String outRawTopic = MQTT_OUT_RAW ;
String outAliveTopic = MQTT_OUT_ALIVE ;
String outRSSITopic = MQTT_OUT_RSSI ;
String clientId;

//RELAIS
void sendRelaisCommand(unsigned int relais,boolean onoff); // forward declaration
volatile byte state; // 0 STOP (0,X); 1 EXTEND (1,1); 2 STOP (0,X); 3 RETRACT (1,0)
volatile boolean relaisState[] = {false,false};
struct EEPROMValues{ // use a struct to store and retrieve to/from EEPROM
  //const char* mark="#"; // Before C++11, members of a struct could not be default initialized. Instead they must be initialized after an instance struct is created.
  //EEPROM.put/get probably have issues with strings
  byte mark;  //or char mark;
  unsigned long timeActiveDir1; // Milliseconds active till relaisstate[] falls back to {false,false}
  unsigned long timeActiveDir2; // Milliseconds active till relaisstate[] false back to {false,false}
  unsigned long checksum;
} settings;

//TIMER
unsigned long startTimeDir1 = 0;  // Starttime if relais1 goes active
unsigned long startTimeDir2 = 0;  // Starttime if relais1 goes active

//CRC
CRC8 crc;
unsigned int eepromAddr = 0;  // address to store the config variables, mark and checksum (struct settings)

//CONFIG
boolean sendOutOneTime = false; // flag to output the stored config only once after powerup/reset


//CALLBACK The function is called when a new message arrives. All code to react within this function and/or transfer the parameter to outer variables.
// mosquitto_pub -h 192.168.178.x -t 'home/basement/0_terrace/awning/trigger' -m '1' // or -m 1 or -m 0
// mosquitto_pub -h 192.168.178.x -t 'home/basement/0_terrace/awning/config' -m '7000 8000'
// mosquitto_pub -h 192.168.178.x -t 'home/basement/0_terrace/awning/direction' -m 'up'
void callback(char* topic, byte* payload, unsigned int length) { // topic ends with direction or config // payload from -m within apostrophes // lentgh integer number of bytes of payload
  #ifdef SERIAL_DEBUG
    Serial.print("Message arrived [");
    Serial.print(topic);
    Serial.print("] ");
    for (unsigned int i = 0; i < length; i++) {
      Serial.print((char)payload[i]);  // print out the payload array byte by byte
    }
    Serial.println();
  #endif

  String payloadStr; // to transfer the byte-array to a String
  char* p = (char*)malloc(length); // copy the payload to the new buffer; actually not necessary
  memcpy(p,payload,length); // play safe, it does no harm
  payloadStr.concat(p,length); // String is better to do things like .toLowerCase();

  // Trigger CONTROL
  if (strcmp(inTriggerTopic.c_str(),topic) == 0 && (char)payload[0] == '1') { //1 for trigger // 0 or other are not handled
    boolean stateSet = false; // if the state is detected and changed ignore all following state detections
    // Relais 1 determines the direction
    // Relais 2 switches on/off respectively move/stop
    // activating means first switch on relay 1, pause, activate relay 2
    // deactivating means first switch off relay 2, pause and only then relay 1 if necessary switch
    // state 0 both relays off (relay 2 then 1) start condition
    // state 1 relay 1 set relay 1 to move out/down postition, pause, activate relay 2
    // state 2 both relays off (relay 2 then 1) intermediate condition
    // state 3 relay 1 set relay 1 to move in/up postition, pause, activate relay 2
    // recommendation for pausetime is 500 ms
    if (!relaisState[0] && !relaisState[1] && state != 0 && state !=2 ){ //asynchron!!! correct it
      state = 0;
      stateSet = true;
    }
    if (!relaisState[0] && !relaisState[1] && !stateSet){
      if(state==0){ // State 0->1
        sendRelaisCommand(1,true); // extend
        delay(100);
        sendRelaisCommand(2,true); // activate
        state=1;
        startTimeDir1 = millis();
      }
      if(state==2){ // state 2->3
        sendRelaisCommand(1,false); // retract
        delay(100);
        sendRelaisCommand(2,true);  // activate
        state=3;
        startTimeDir2 = millis();
      }
      stateSet = true;
    }
    if (relaisState[0] && relaisState[1] && !stateSet){ // state 1->2
      sendRelaisCommand(2,false);
      delay(100);
      sendRelaisCommand(1,false); //at this point the relais could be untouched, in state 1 and 3 it will be set again, to save energy switch both relays to off
      state=2;
      stateSet = true;
    }
    if (!relaisState[0] && relaisState[1] && !stateSet){ // state 3->0
      sendRelaisCommand(2,false);
      delay(100);
      sendRelaisCommand(1,false); //at this point the relais could be untouched, in state 1 and 3 it will be set again, to save energy switch both relays to off
      state=0;
      stateSet = true;
    }
    snprintf (msg, MQTT_DEBUG_MSG_BUFFER_SIZE, "%d",state);
    #ifdef SERIAL_DEBUG
     Serial.print("Publish message: ");
     Serial.println(msg);
    #endif
    MQTTClient.publish(outStateTopic.c_str(), msg);
  }

  // Direction CONTROL
  if (strcmp(inDirectionTopic.c_str(),topic) == 0) {
    uint8_t direction=0; // 1 means down(out); 2 means up(in)
    payloadStr.toLowerCase();
    //MQTTClient.publish(outRawTopic.c_str(), payloadStr.c_str());


    const char directions1[3][6]={"down","out","on"}; // to react to all of this commands
    const char directions2[3][6]={"up","in","off"};
    const char directions3[3][6]={"halt","stop","stopp"};

    uint8_t number; // -Wunused-but-set-variable
    number=sizeof(directions1)/sizeof(directions1[0]); // calculate the number of array members  // Test!!! can be deleted
    (void)number; // to suppress the warning
    #ifdef MQTT_DEBUG_OUTPUT
      snprintf (msg, MQTT_DEBUG_MSG_BUFFER_SIZE, "%d",number);
      MQTTClient.publish(outRawTopic.c_str(), msg);
    #endif

    // only one of this tests can be true // after this block direction is 1, 2 or 3 depending on command
    for(uint8_t i=1;i<=(sizeof(directions1)/sizeof(directions1[0]));i++){
      if (strcmp(payloadStr.c_str(),directions1[i-1])==0)direction=1;
    }
    for(uint8_t i=1;i<=(sizeof(directions2)/sizeof(directions2[0]));i++){
      if (strcmp(payloadStr.c_str(),directions2[i-1])==0)direction=2;
    }
    for(uint8_t i=1;i<=(sizeof(directions3)/sizeof(directions3[0]));i++){
      if (strcmp(payloadStr.c_str(),directions3[i-1])==0)direction=3;
    }


    if (direction == 1){
      MQTTClient.publish(outRawTopic.c_str(), "DIR1");
      sendRelaisCommand(2,false);
      delay(100);
      sendRelaisCommand(1,false);
      delay(500);
      sendRelaisCommand(1,true); // move out/down
      delay(100);
      sendRelaisCommand(2,true); // activate
      startTimeDir1 = millis();
    }
    if (direction == 2){
      MQTTClient.publish(outRawTopic.c_str(), "DIR2");
      sendRelaisCommand(2,false);
      delay(100);
      sendRelaisCommand(1,false);
      delay(500);
      sendRelaisCommand(1,false);
      delay(100);
      sendRelaisCommand(2,true);
      startTimeDir2 = millis();
    }
    if (direction == 3){
      MQTTClient.publish(outRawTopic.c_str(), "DIR3");
      sendRelaisCommand(2,false);
      delay(100);
      sendRelaisCommand(1,false);
      delay(500);
    }
  }

  // config CONTROL
  if (strcmp(inConfigTopic.c_str(),topic) == 0){
    unsigned long maxTime[] = {180,180};
    const char* pL = payloadStr.c_str();  // get pointer to payload string // the payload is the combination of two integer space separated
    char* end;                            // declare a pointer
    for (uint8_t i = 1; i <= 2; i++){
      maxTime[i-1] = strtol(pL,&end,10); // &end is the reference to an object of type char*, whose value is set by the function to the next character in str after the numerical value.
      if (pL == end) break;
      pL = end; // the space at the beginning of second number is seemingly not a problem
    }
    snprintf (msg, MQTT_DEBUG_MSG_BUFFER_SIZE, "{%ld:%ld}",maxTime[0],maxTime[1]);
    MQTTClient.publish(outRawTopic.c_str(), msg);
    if (maxTime[0]>=0 && maxTime[0]<=1440000 && maxTime[1]>=0 && maxTime[1]<=1440000){
      settings.timeActiveDir1=maxTime[0];
      settings.timeActiveDir2=maxTime[1];
      crc.reset();
      crc.add(settings.timeActiveDir1);
      crc.add(settings.timeActiveDir2);
      settings.checksum=crc.getCRC();
      #ifdef MQTT_DEBUG_OUTPUT
        snprintf (msg, MQTT_DEBUG_MSG_BUFFER_SIZE, "{EEPROMput:%c:%ld:%ld:%ld}",settings.mark,settings.timeActiveDir1,settings.timeActiveDir2,settings.checksum);
        MQTTClient.publish(outRawTopic.c_str(), msg);
      #endif
      EEPROM.put(eepromAddr, settings); //write data to array in ram
      EEPROM.commit();  //write data from ram to flash memory. Do nothing if there are no changes to EEPROM data in ram
    }
  }
}


void reconnect() {
  // Loop until we're reconnected
  while (!MQTTClient.connected()) {
    #ifdef SERIAL_DEBUG
      Serial.print("Attempting MQTT connection...");
    #endif

    // Attempt to connect
    if (MQTTClient.connect(mac.c_str()))  {
      #ifdef SERIAL_DEBUG
          Serial.println("connected");
      #endif
      // Once connected, publish an announcement...
      MQTTClient.publish(topicBase.c_str(), "CON");
      // ... and resubscribe
      MQTTClient.subscribe(inTriggerTopic.c_str());
      MQTTClient.subscribe(inDirectionTopic.c_str());
      MQTTClient.subscribe(inConfigTopic.c_str());
    } else {
      #ifdef SERIAL_DEBUG
        Serial.print("failed, rc=");
        Serial.print(MQTTClient.state());
        Serial.println(" try again in 5 seconds");
      #endif
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void sendRelaisCommand(unsigned int relais,boolean onoff){
  //default for relais 1
  byte commandOn[4]={160,1,1,162};  // A0 (160 Non-breaking space) command initiate, relay 1/2, on/off, checksum (Addition)
  byte commandOff[4]={160,1,0,161};

  //change commands for relais 2
  if (relais==2){
      commandOn[1]=2;
      commandOff[1]=2;
      commandOn[3]=163;
      commandOff[3]=162;
  }

  if (onoff){
      Serial.write(commandOn,4);
      relaisState[relais-1]=true;
  }else{
      Serial.write(commandOff,4);
      relaisState[relais-1]=false;
  }

  #ifdef MQTT_DEBUG_OUTPUT
    snprintf (msg, MQTT_DEBUG_MSG_BUFFER_SIZE, "RELAIS-%d_SET_%s",relais,onoff?"ON":"OFF");
    #ifdef SERIAL_DEBUG
     Serial.print("Publish message: ");
     Serial.println(msg);
    #endif
    MQTTClient.publish(outRawTopic.c_str(),msg);
  #endif
}

void setupDateTime() {
  // setup this after wifi connected
  // you can use custom timeZone,server and timeout
  // DateTime.setTimeZone(-4);
  //   DateTime.setServer("asia.pool.ntp.org");
  //   DateTime.begin(15 * 1000);
  DateTime.setServer("de.pool.ntp.org");
  DateTime.setTimeZone("CEST");
  DateTime.begin(); // ( 	const unsigned int  	timeOutMs = DEFAULT_TIMEOUT	)
  #ifdef SERIAL_DEBUG
    if (!DateTime.isTimeValid()) {
      Serial.println("Failed to get time from server.");
    } else {
      Serial.printf("Date Now is %s\n", DateTime.toISOString().c_str());
      Serial.printf("Timestamp is %lld\n", DateTime.now());
    }
  #endif
}

//SETUP
void setup() {

   //SERIAL
  Serial.begin(115200);         // Start the Serial communication to send messages to the computer
  delay(10);
  #ifdef SERIAL_DEBUG
    Serial.println('\n');
  #endif

  //WIFI
  WiFi.mode(WIFI_STA);// ist scheinbar der defaultwert wird in letzter Version nicht benutzt
  WiFi.begin(ssid, password);             // Connect to the network
  #ifdef SERIAL_DEBUG
    Serial.print("Connecting to ");
    Serial.print(ssid); Serial.println(" ...");
  #endif

  while (WiFi.status() != WL_CONNECTED) { // Wait for the Wi-Fi to connect
    delay(1000);
    #ifdef SERIAL_DEBUG
      int i = 0;
      Serial.print(++i); Serial.print("->");
    #endif
  }
  mac=WiFi.macAddress();
  //clientId += String(random(0xffff), HEX);
  clientId = "ESP01-" + mac;

  randomSeed(micros());

  #ifdef SERIAL_DEBUG
    Serial.println('\n');
    Serial.println("Connection established!");
    Serial.print("IP address:\t");
    Serial.println(WiFi.localIP());         // Send the IP address of the ESP8266 to the computer
  #endif

  //MQTT
  MQTTClient.setServer(MQTTServer, 1883);
  MQTTClient.setCallback(callback);

  //DATETIME
  setupDateTime();

  //TIMESETTING
  EEPROM.begin(sizeof(settings)); // 2 + 8 + 8 + 8
  boolean reliable1 = false, reliable2 = false;
  EEPROM.get(eepromAddr, settings); //read data from array in ram and cast it into struct called settings
  #ifdef MQTT_DEBUG_OUTPUT
    // in setup() MQTT isn't connected so store the values and publish it in loop()
    snprintf (msgConfig, 50, "{EEPROMget:%c:%ld:%ld:%#lx}",settings.mark,settings.timeActiveDir1,settings.timeActiveDir2,settings.checksum);
  #endif
  //if (strcmp(settings.mark,"#") == 0){
  if (settings.mark == 35){
    crc.reset();
    crc.add(settings.timeActiveDir1);
    crc.add(settings.timeActiveDir2);
    if (crc.getCRC()==settings.checksum) reliable1 = true;
  }
  if (settings.timeActiveDir1 >= 50 &&     // values less than 50 ms  and greater than 24 hours are senseless
      settings.timeActiveDir1 <= 1440000 &&
      settings.timeActiveDir2 >= 50 &&
      settings.timeActiveDir2 <= 1440000) reliable2 = true;

if (!(reliable1 && reliable2)){ // if the Content of EEPROM is undefined, set it
  settings.mark = 35;
  settings.timeActiveDir1 = 180 * 1000; // default 3 minutes to switch off
  settings.timeActiveDir2 = 180 * 1000;
  crc.reset();
  crc.add(settings.timeActiveDir1);
  crc.add(settings.timeActiveDir2);
  settings.checksum=crc.getCRC();
  EEPROM.put(eepromAddr, settings); //write data structure to ram
  EEPROM.commit();  //write data from ram to flash memory. Do nothing if there are no changes to EEPROM data in ram
}

  //RELAIS setup
  Serial.write(0); // initialize the uContoller that controls the relays
}

// LOOP
void loop() {

  if (!MQTTClient.connected()) {
    reconnect();
  }
  MQTTClient.loop();

  // send out one time after setup
  if (!sendOutOneTime) {
    #ifdef MQTT_DEBUG_OUTPUT
      MQTTClient.publish(outRawTopic.c_str(), msgConfig);
      snprintf (msgConfig, 50, "{config:%c:%ld:%ld:%#lx}",settings.mark,settings.timeActiveDir1,settings.timeActiveDir2,settings.checksum);
      MQTTClient.publish(outRawTopic.c_str(), msgConfig);
      sendOutOneTime = true;
    #endif
  }
  unsigned long now = millis();

   if (now - lastMsg > ALIVEINTERVAL) {
     lastMsg = now;
     ++value;
     snprintf (msg, MQTT_MSG_BUFFER_SIZE, "%s",DateTime.toISOString().c_str());
    #ifdef SERIAL_DEBUG
      Serial.print("Publish message: ");
      Serial.println(msg);
    #endif
     MQTTClient.publish(outAliveTopic.c_str(), msg);
     snprintf (msg, MQTT_MSG_BUFFER_SIZE, "%d",WiFi.RSSI());
    #ifdef SERIAL_DEBUG
      Serial.print("Publish message: ");
      Serial.println(msg);
    #endif
     MQTTClient.publish(outRSSITopic.c_str(), msg);
   }


   if ((now - startTimeDir1 > settings.timeActiveDir1) && (startTimeDir1 != 0)){   // switch both relays to off after milliseconds defined in config (energy saving and safety aspects)
     #ifdef MQTT_DEBUG_OUTPUT
      snprintf (msg, MQTT_DEBUG_MSG_BUFFER_SIZE, "{Fallback1:%ld:%ld:%ld}",now,startTimeDir1,settings.timeActiveDir1);
      MQTTClient.publish(outRawTopic.c_str(), msg);
    #endif
    sendRelaisCommand(2,false);
    delay(100);
    sendRelaisCommand(1,false);
    delay(500);
    startTimeDir1 = 0;
   }

   if ((now - startTimeDir2 > settings.timeActiveDir2) && (startTimeDir2 != 0)){    // switch both relays to off after milliseconds defined in config (energy saving and safety aspects)
     #ifdef MQTT_DEBUG_OUTPUT
      snprintf (msg, MQTT_DEBUG_MSG_BUFFER_SIZE, "{Fallback2:%ld:%ld:%ld}",now,startTimeDir2,settings.timeActiveDir2);
      MQTTClient.publish(outRawTopic.c_str(), msg);
    #endif
    sendRelaisCommand(2,false);
    delay(100);
    sendRelaisCommand(1,false);
    delay(500);
    startTimeDir2 = 0;
   }

}

Beschreibung

Die Software ist eine Firmware für einen **ESP8266 (ESP-01)**, die über MQTT eine **motorisierte Markise** oder einen ähnlichen Sonnenschutz steuert.

    1. Hauptfunktion

Der ESP8266 verbindet sich mit WLAN und einem MQTT-Broker und steuert über eine serielle Relaisplatine zwei Relais:

  • Relais 1 = Fahrtrichtung (Ausfahren / Einfahren)
  • Relais 2 = Motor EIN/AUS

Die Steuerung erfolgt per MQTT-Befehlen.

---

    1. MQTT-Schnittstelle

Basis-Topic:

```text home/basement/0_terrace/awning/ ```

Eingehende Befehle:

| Topic | Funktion | | --------- | ------------------------ | | trigger | Zustandswechsel | | direction | Richtung vorgeben | | config | Laufzeiten konfigurieren |

Ausgehende Statusmeldungen:

| Topic | Inhalt | | ----- | ------------------------- | | state | aktueller Zustand | | raw | Debug-Ausgaben | | alive | Heartbeat mit Zeitstempel | | RSSI | WLAN-Empfangsstärke |


---

    1. Zustandsautomat

Es existieren vier Zustände:

| Zustand | Bedeutung | | ------- | --------- | | 0 | Stopp | | 1 | Ausfahren | | 2 | Stopp | | 3 | Einfahren |

Bei jedem MQTT-Trigger wird zum nächsten Zustand gewechselt.

```text 0 → 1 → 2 → 3 → 0 ```

Dadurch genügt ein einziger Taster oder MQTT-Befehl zum Steuern der Markise.

---

    1. Richtungssteuerung

Per MQTT können direkte Befehle gesendet werden:

```text down out on ```

→ Markise ausfahren

```text up in off ```

→ Markise einfahren

```text halt stop stopp ```

→ Motor stoppen


---

    1. Sicherheitsschaltung

Für beide Richtungen existiert eine maximale Laufzeit:

```cpp timeActiveDir1 timeActiveDir2 ```

Nach Ablauf dieser Zeit werden beide Relais automatisch abgeschaltet.

Standard:

```text 180 Sekunden ```

Damit wird verhindert, dass ein defekter MQTT-Befehl oder ein Softwarefehler den Motor dauerhaft laufen lässt.

---

    1. EEPROM-Speicherung

Die Laufzeiten werden dauerhaft gespeichert:

```cpp struct EEPROMValues ```

Inhalt:

  • Kennzeichen (`mark`)
  • Laufzeit Richtung 1
  • Laufzeit Richtung 2
  • CRC-Prüfsumme

Beim Start prüft die Firmware die Daten auf Plausibilität und Integrität. Falls ungültig, werden Standardwerte erzeugt.

---

    1. Netzwerkfunktionen

Die Firmware:

  • verbindet sich mit WLAN
  • verbindet sich mit MQTT
  • synchronisiert die Uhr per NTP
  • sendet alle 10 Sekunden einen Heartbeat

Beispiel:

```text 2025-03-01T12:34:56+01:00 ```

sowie die aktuelle WLAN-Feldstärke.

---

    1. Hardware

Aus dem Code lässt sich folgende Hardware ableiten:

```text ESP8266 (ESP-01)

UART

2-Kanal Relaiscontroller

Markisenmotor ```

Die Relaisplatine wird über ein proprietäres 4-Byte-Protokoll gesteuert:

```text A0 Relais Zustand Checksumme ```

Beispiel:

```text A0 01 01 A2 ```

= Relais 1 EIN.

---

    1. Zusammenfassung

Die Software ist ein **MQTT-gesteuerter Markisencontroller auf Basis eines ESP8266**, der:

  • WLAN und MQTT nutzt,
  • eine Markise ein- und ausfährt,
  • Fahrzeiten im EEPROM speichert,
  • automatisch abschaltet,
  • Status- und Debuginformationen veröffentlicht,
  • per Home Assistant, Node-RED oder FHEM steuerbar ist.

Der Aufbau wirkt wie ein typisches Smart-Home-Projekt zur Nachrüstung einer vorhandenen Funk- oder Tastersteuerung einer Terrasse-Markise.

Fehlerquellen

Ja, es gibt einige relevante Fehlerquellen:

1. **Speicherleck in `callback()`**

  `malloc(length)` wird nie mit `free(p)` freigegeben. Bei jedem MQTT-Befehl geht RAM verloren. Außerdem wird kein Byte für `\0` reserviert. Besser ganz ohne `malloc`:

```cpp String payloadStr((char*)payload, length); ```


2. **`state` ist nicht initialisiert**

  `volatile byte state;` bekommt keinen Startwert. Beim ersten Trigger kann der Zustand undefiniert sein. Besser:

```cpp volatile byte state = 0; ```


3. **Richtungsbefehl setzt `state` nicht passend**

  Bei `direction == 1/2/3` werden Relais geschaltet, aber `state` wird nicht auf 1, 3 oder 0/2 gesetzt. Danach kann der nächste `trigger` falsch reagieren. 

4. **Alte Timer laufen weiter**

  Wenn Richtung 1 gestartet wurde und danach Richtung 2 kommt, wird `startTimeDir1` nicht gelöscht. Später kann der alte Fallback unerwartet beide Relais ausschalten. Gleiches umgekehrt. 

5. **Prüfung `maxTime[0] >= 0` ist wirkungslos**

  `maxTime` ist `unsigned long`, also nie negativ. Wenn `strtol()` einen negativen Wert liefert, wird er beim Speichern in `unsigned long` zu einem großen Wert. 

6. **`sendRelaisCommand()` prüft Relaisnummer nicht**

  Bei versehentlich `sendRelaisCommand(0, ...)` oder `3` greift `relaisState[relais-1]` außerhalb des Arrays. Besser am Anfang:

```cpp if (relais < 1 || relais > 2) return; ```


7. **Blockierende `delay()`-Aufrufe**

  Während `delay(500)` oder beim MQTT-Reconnect mit `delay(5000)` reagiert der Controller verzögert. Für eine Markise ist das noch akzeptabel, aber ein sofortiger Stopp kann dadurch verspätet kommen. 

8. **MQTT ohne Authentifizierung**

  Der Code verbindet sich nur mit `MQTTClient.connect(mac.c_str())`. Jeder im Netz, der die Topics kennt, könnte die Markise steuern. 

Die wichtigsten Sofortkorrekturen wären: `state = 0`, `malloc` entfernen, Timer gegenseitig zurücksetzen, `state` bei Direktbefehlen aktualisieren und MQTT mit Benutzer/Passwort absichern.