WLAN Aktor Markise
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.
- 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.
---
- 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 |
---
- 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.
---
- 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
---
- 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.
---
- 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.
---
- 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.
---
- 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.
---
- 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.