From 04c4451f7d7bcb2da910f2f758b08b00bc4ffaf5 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Thu, 6 May 2021 22:58:03 +0200 Subject: [PATCH] Usermods MQTT processing. Multi-relay usermod with MQTT/HTML control. Minor bugfixes. --- usermods/multi_relay/usermod_multi_relay.h | 274 ++++++++++++++---- .../usermod_v2_auto_save.h | 2 +- wled00/fcn_declare.h | 10 +- wled00/mqtt.cpp | 60 ++-- wled00/um_manager.cpp | 5 + wled00/wled.h | 2 +- 6 files changed, 258 insertions(+), 95 deletions(-) diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index 13514c00..8d2861ba 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -6,23 +6,31 @@ #define MULTI_RELAY_MAX_RELAYS 4 #endif +#define ON true +#define OFF false + /* * This usermod handles multiple relay outputs. * These outputs complement built-in relay output in a way that the activation can be delayed. * They can also activate/deactivate in reverse logic independently. */ + +typedef struct relay_t { + int8_t pin; + bool active; + bool mode; + bool state; + bool external; + uint16_t delay; +} Relay; + + class MultiRelay : public Usermod { private: - // pins - int8_t _relayPin[MULTI_RELAY_MAX_RELAYS]; - // delay (in seconds) before relay state changes - uint16_t _relayDelay[MULTI_RELAY_MAX_RELAYS]; - // activation mode (high/low) - bool _relayMode[MULTI_RELAY_MAX_RELAYS]; - // relay active state - bool _relayActive[MULTI_RELAY_MAX_RELAYS]; + // array of relays + Relay _relay[MULTI_RELAY_MAX_RELAYS]; // switch timer start time uint32_t _switchTimerStart = 0; @@ -37,20 +45,10 @@ class MultiRelay : public Usermod { // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; - static const char _relay[]; - static const char _delay[]; + static const char _relay_str[]; + static const char _delay_str[]; static const char _activeHigh[]; - - /** - * switch relay on/off - */ - void switchRelay(uint8_t relay) { - if (relay>=MULTI_RELAY_MAX_RELAYS || _relayPin[relay]<0) return; - pinMode(_relayPin[relay], OUTPUT); - bool mode = bri ? _relayMode[relay] : !_relayMode[relay]; - digitalWrite(_relayPin[relay], mode); - publishMqtt(mode ? "on" : "off", relay); - } + static const char _external[]; void publishMqtt(const char* state, int relay) { @@ -68,25 +66,110 @@ class MultiRelay : public Usermod { void handleOffTimer() { bool activeRelays = false; for (uint8_t i=0; i 0 && millis() - _switchTimerStart > (_relayDelay[i]*1000)) { - switchRelay(i); // toggle relay - _relayActive[i] = false; + if (_relay[i].active && _switchTimerStart > 0 && millis() - _switchTimerStart > (_relay[i].delay*1000)) { + if (!_relay[i].external) toggleRelay(i); + _relay[i].active = false; } - activeRelays = activeRelays || _relayActive[i]; + activeRelays = activeRelays || _relay[i].active; } if (!activeRelays) _switchTimerStart = 0; } + /** + * HTTP API handler + * borrowed from: + * https://github.com/gsieben/WLED/blob/master/usermods/GeoGab-Relays/usermod_GeoGab.h + */ + #define GEOGABVERSION "0.1.3" + void InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsyncWebServer + DEBUG_PRINTLN(F("Relays: Initialize HTML API")); + + server.on("/relays", HTTP_GET, [this](AsyncWebServerRequest *request) { + DEBUG_PRINTLN("Relays: HTML API"); + String janswer; + String error = ""; + int params = request->params(); + janswer = F("{\"NoOfRelays\":"); + janswer += String(MULTI_RELAY_MAX_RELAYS) + ","; + + if (getActiveRelayCount()) { + // Commands + if(request->hasParam("switch")) { + /**** Switch ****/ + AsyncWebParameter* p = request->getParam("switch"); + // Get Values + for (int i=0; ivalue(), ',', i); + if (value==-1) { + error = F("There must be as much arugments as relays"); + } else { + // Switch + if (_relay[i].external) switchRelay(i, (bool)value); + } + } + } else if(request->hasParam("toggle")) { + /**** Toggle ****/ + AsyncWebParameter* p = request->getParam("toggle"); + // Get Values + for (int i=0;ivalue(), ',', i); + if (value==-1) { + error = F("There must be as mutch arugments as relays"); + } else { + // Toggle + if (value && _relay[i].external) toggleRelay(i); + } + } + } else { + error = F("No valid command found"); + } + } else { + error = F("No active relays"); + } + + // Status response + char sbuf[16]; + for (int i=0; isend(200, "application/json", janswer); + }); + } + + int getValue(String data, char separator, int index) { + int found = 0; + int strIndex[] = {0, -1}; + int maxIndex = data.length()-1; + + for(int i=0; i<=maxIndex && found<=index; i++){ + if(data.charAt(i)==separator || i==maxIndex){ + found++; + strIndex[0] = strIndex[1]+1; + strIndex[1] = (i == maxIndex) ? i+1 : i; + } + } + return found>index ? data.substring(strIndex[0], strIndex[1]).toInt() : -1; + } + public: /** * constructor */ MultiRelay() { for (uint8_t i=0; i=MULTI_RELAY_MAX_RELAYS || _relay[relay].pin<0) return; + _relay[relay].state = mode; + pinMode(_relay[relay].pin, OUTPUT); + digitalWrite(_relay[relay].pin, mode ? !_relay[relay].mode : _relay[relay].mode); + publishMqtt(mode ? "on" : "off", relay); + } + + /** + * toggle relay + */ + inline void toggleRelay(uint8_t relay) { + switchRelay(relay, !_relay[relay].state); + } + + uint8_t getActiveRelayCount() { + uint8_t count = 0; + for (uint8_t i=0; i=0) count++; + return count; + } //Functions called by WLED + /** + * handling of MQTT message + * topic only contains stripped topic (part after /wled/MAC) + * topic should look like: /relay/X/command; where X is relay number, 0 based + */ + bool onMqttMessage(char* topic, char* payload) { + if (strlen(topic) > 8 && strncmp_P(topic, PSTR("/relay/"), 7) == 0 && strncmp_P(topic+8, PSTR("/command"), 8) == 0) { + uint8_t relay = strtoul(topic+7, NULL, 10); + if (relaysubscribe(subuf, 0); + } + } + /** * setup() is called once at boot. WiFi is not yet connected at this point. * You can use it to initialize variables, sensors or similar. @@ -112,12 +257,12 @@ class MultiRelay : public Usermod { void setup() { // pins retrieved from cfg.json (readFromConfig()) prior to running setup() for (uint8_t i=0; i=0) _relayActive[i] = true; + if (_relay[i].pin>=0) _relay[i].active = true; } } @@ -157,15 +304,12 @@ class MultiRelay : public Usermod { */ void addToJsonInfo(JsonObject &root) { if (enabled) { - uint8_t count = 0; - for (uint8_t i=0; i=0) count++; - JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray infoArr = user.createNestedArray(F("Number of relays")); //name - infoArr.add(String(count)); + infoArr.add(String(getActiveRelayCount())); } } @@ -191,10 +335,11 @@ class MultiRelay : public Usermod { top[FPSTR(_enabled)] = enabled; for (uint8_t i=0; i())); + oldPin[i] = _relay[i].pin; + if (top[parName+"pin"] != nullptr) _relay[i].pin = min(39,max(-1,top[parName+"pin"].as())); if (top[parName+FPSTR(_activeHigh)] != nullptr) { if (top[parName+FPSTR(_activeHigh)].is()) { - _relayMode[i] = top[parName+FPSTR(_activeHigh)].as(); // reading from cfg.json + _relay[i].mode = top[parName+FPSTR(_activeHigh)].as(); // reading from cfg.json } else { // change from settings page String str = top[parName+FPSTR(_activeHigh)]; // checkbox -> off or on - _relayMode[i] = (bool)(str!="off"); // off is guaranteed to be present + _relay[i].mode = (bool)(str!="off"); // off is guaranteed to be present } } - _relayDelay[i] = min(600,max(0,abs(top[parName+FPSTR(_delay)].as()))); + if (top[parName+FPSTR(_external)] != nullptr) { + if (top[parName+FPSTR(_external)].is()) { + _relay[i].external = top[parName+FPSTR(_external)].as(); // reading from cfg.json + } else { + // change from settings page + String str = top[parName+FPSTR(_external)]; // checkbox -> off or on + _relay[i].external = (bool)(str!="off"); // off is guaranteed to be present + } + } + + _relay[i].delay = min(600,max(0,abs(top[parName+FPSTR(_delay_str)].as()))); } if (!initDone) { @@ -249,12 +404,12 @@ class MultiRelay : public Usermod { } // allocate new pins for (uint8_t i=0; i=0 && pinManager.allocatePin(_relayPin[i],true)) { - switchRelay(i); + if (_relay[i].pin>=0 && pinManager.allocatePin(_relay[i].pin,true)) { + if (!_relay[i].external) switchRelay(i, _relay[i].state = (bool)bri); } else { - _relayPin[i] = -1; + _relay[i].pin = -1; } - _relayActive[i] = false; + _relay[i].active = false; } DEBUG_PRINTLN(F("MultiRelay config (re)loaded.")); } @@ -273,6 +428,7 @@ class MultiRelay : public Usermod { // strings to reduce flash memory usage (used more than twice) const char MultiRelay::_name[] PROGMEM = "MultiRelay"; const char MultiRelay::_enabled[] PROGMEM = "enabled"; -const char MultiRelay::_relay[] PROGMEM = "relay"; -const char MultiRelay::_delay[] PROGMEM = "delay-s"; -const char MultiRelay::_activeHigh[] PROGMEM = "active-high"; \ No newline at end of file +const char MultiRelay::_relay_str[] PROGMEM = "relay"; +const char MultiRelay::_delay_str[] PROGMEM = "delay-s"; +const char MultiRelay::_activeHigh[] PROGMEM = "active-high"; +const char MultiRelay::_external[] PROGMEM = "external"; diff --git a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h index 432e6bb7..caf542e5 100644 --- a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -97,7 +97,7 @@ class AutoSaveUsermod : public Usermod { * Da loop. */ void loop() { - if (!autoSaveAfterSec && !enabled) return; // setting 0 as autosave seconds disables autosave + if (!autoSaveAfterSec || !enabled) return; // setting 0 as autosave seconds disables autosave unsigned long now = millis(); uint8_t currentMode = strip.getMode(); diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index e44ab155..7110160a 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -187,27 +187,27 @@ class Usermod { virtual void readFromJsonState(JsonObject& obj) {} virtual void addToConfig(JsonObject& obj) {} virtual void readFromConfig(JsonObject& obj) {} + virtual void onMqttConnect(bool sessionPresent) {} + virtual bool onMqttMessage(char* topic, char* payload) { return false; } virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;} }; -class UsermodManager { +class UsermodManager : public Usermod { private: Usermod* ums[WLED_MAX_USERMODS]; byte numMods = 0; public: void loop(); - void setup(); void connected(); - void addToJsonState(JsonObject& obj); void addToJsonInfo(JsonObject& obj); void readFromJsonState(JsonObject& obj); - void addToConfig(JsonObject& obj); void readFromConfig(JsonObject& obj); - + void onMqttConnect(bool sessionPresent); + bool onMqttMessage(char* topic, char* payload); bool add(Usermod* um); Usermod* lookup(uint16_t mod_id); byte getModCount(); diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index 01c62450..9e04ebe5 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -25,28 +25,28 @@ void onMqttConnect(bool sessionPresent) //(re)subscribe to required topics char subuf[38]; - if (mqttDeviceTopic[0] != 0) - { + if (mqttDeviceTopic[0] != 0) { strcpy(subuf, mqttDeviceTopic); mqtt->subscribe(subuf, 0); - strcat(subuf, "/col"); + strcat_P(subuf, PSTR("/col")); mqtt->subscribe(subuf, 0); strcpy(subuf, mqttDeviceTopic); - strcat(subuf, "/api"); + strcat_P(subuf, PSTR("/api")); mqtt->subscribe(subuf, 0); } - if (mqttGroupTopic[0] != 0) - { + if (mqttGroupTopic[0] != 0) { strcpy(subuf, mqttGroupTopic); mqtt->subscribe(subuf, 0); - strcat(subuf, "/col"); + strcat_P(subuf, PSTR("/col")); mqtt->subscribe(subuf, 0); strcpy(subuf, mqttGroupTopic); - strcat(subuf, "/api"); + strcat_P(subuf, PSTR("/api")); mqtt->subscribe(subuf, 0); } + usermods.onMqttConnect(sessionPresent); + doPublishMqtt = true; DEBUG_PRINTLN(F("MQTT ready")); } @@ -66,25 +66,24 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties size_t topicPrefixLen = strlen(mqttDeviceTopic); if (strncmp(topic, mqttDeviceTopic, topicPrefixLen) == 0) { - topic += topicPrefixLen; + topic += topicPrefixLen; } else { - topicPrefixLen = strlen(mqttGroupTopic); - if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) { - topic += topicPrefixLen; - } else { - // Topic not used here. Probably a usermod subscribed to this topic. - return; - } + topicPrefixLen = strlen(mqttGroupTopic); + if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) { + topic += topicPrefixLen; + } else { + // Non-Wled Topic used here. Probably a usermod subscribed to this topic. + usermods.onMqttMessage(topic, payload); + return; + } } //Prefix is stripped from the topic at this point - if (strcmp(topic, "/col") == 0) - { + if (strcmp_P(topic, PSTR("/col")) == 0) { colorFromDecOrHexString(col, (char*)payload); colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE); - } else if (strcmp(topic, "/api") == 0) - { + } else if (strcmp_P(topic, PSTR("/api")) == 0) { if (payload[0] == '{') { //JSON API DynamicJsonDocument doc(JSON_BUFFER_SIZE); deserializeJson(doc, payload); @@ -94,8 +93,11 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties apireq += (char*)payload; handleSet(nullptr, apireq); } - } else if (strcmp(topic, "") == 0) - { + } else if (strlen(topic) != 0) { + // non standard topic, check with usermods + usermods.onMqttMessage(topic, payload); + } else { + // topmost topic (just wled/MAC) parseMQTTBriPayload(payload); } } @@ -110,24 +112,24 @@ void publishMqtt() char s[10]; char subuf[38]; - sprintf(s, "%u", bri); + sprintf_P(s, PSTR("%u"), bri); strcpy(subuf, mqttDeviceTopic); - strcat(subuf, "/g"); + strcat_P(subuf, PSTR("/g")); mqtt->publish(subuf, 0, true, s); - sprintf(s, "#%06X", (col[3] << 24) | (col[0] << 16) | (col[1] << 8) | (col[2])); + sprintf_P(s, PSTR("#%06X"), (col[3] << 24) | (col[0] << 16) | (col[1] << 8) | (col[2])); strcpy(subuf, mqttDeviceTopic); - strcat(subuf, "/c"); + strcat_P(subuf, PSTR("/c")); mqtt->publish(subuf, 0, true, s); strcpy(subuf, mqttDeviceTopic); - strcat(subuf, "/status"); + strcat_P(subuf, PSTR("/status")); mqtt->publish(subuf, 0, true, "online"); char apires[1024]; XML_response(nullptr, apires); strcpy(subuf, mqttDeviceTopic); - strcat(subuf, "/v"); + strcat_P(subuf, PSTR("/v")); mqtt->publish(subuf, 0, true, apires); } @@ -157,7 +159,7 @@ bool initMqtt() if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass); strcpy(mqttStatusTopic, mqttDeviceTopic); - strcat(mqttStatusTopic, "/status"); + strcat_P(mqttStatusTopic, PSTR("/status")); mqtt->setWill(mqttStatusTopic, 0, true, "offline"); mqtt->setKeepAlive(MQTT_KEEP_ALIVE_TIME); mqtt->connect(); diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index 4d07226e..e516be17 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -14,6 +14,11 @@ void UsermodManager::addToJsonInfo(JsonObject& obj) { for (byte i = 0; i < n void UsermodManager::readFromJsonState(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->readFromJsonState(obj); } void UsermodManager::addToConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->addToConfig(obj); } void UsermodManager::readFromConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->readFromConfig(obj); } +void UsermodManager::onMqttConnect(bool sessionPresent) { for (byte i = 0; i < numMods; i++) ums[i]->onMqttConnect(sessionPresent); } +bool UsermodManager::onMqttMessage(char* topic, char* payload) { + for (byte i = 0; i < numMods; i++) if (ums[i]->onMqttMessage(topic, payload)) return true; + return false; +} /* * Enables usermods to lookup another Usermod. diff --git a/wled00/wled.h b/wled00/wled.h index 6de54b9b..4b5e7f8c 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -8,7 +8,7 @@ */ // version code in format yymmddb (b = daily build) -#define VERSION 2105041 +#define VERSION 2105061 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG