diff --git a/docs/use/actuators.md b/docs/use/actuators.md index 6cda383..c3aaa6b 100644 --- a/docs/use/actuators.md +++ b/docs/use/actuators.md @@ -39,7 +39,13 @@ Goes ON for 25 ms, then back to OFF: Goes OFF for 25 ms, then back to ON: `mosquitto_pub -t home/OpenMQTTGateway_MEGA/commands/MQTTtoONOFF -m '{"gpio":15,"cmd":"low_pulse","pulse_length":25}'` -Be aware that outputs are OFF by default when the board first start. +Behavior at start in case of power loss (ESP32 only): +The module will record per default the last state of the actuator and attempt to recover it upon restart. +Example: if your relay is ON and a power outage occurs, when the power comes back the firmware will attempt to switch ON the relay again. + +If you don't want this behavior you can set the macro `USE_LAST_STATE_ON_RESTART` to `false` at build time or use the command below at runtime: +`mosquitto_pub -t home/OpenMQTTGateway_MEGA/commands/MQTTtoONOFF/config -m '{"uselaststate":false,"save":true}"'` +The key `save` will persist the configuration upon restarts. ## FASTLED ### The FASTLED module support 2 different operation modes diff --git a/main/ZactuatorONOFF.ino b/main/ZactuatorONOFF.ino index 6523a8d..baf834a 100644 --- a/main/ZactuatorONOFF.ino +++ b/main/ZactuatorONOFF.ino @@ -30,22 +30,90 @@ #ifdef ZactuatorONOFF +# ifdef ESP32 +// Global struct to store live ONOFF configuration data +ONOFFConfig_s ONOFFConfig; + +void ONOFFConfig_init() { + ONOFFConfig.ONOFFState = !ACTUATOR_ON; + ONOFFConfig.useLastStateOnStart = USE_LAST_STATE_ON_RESTART; +} + +void ONOFFConfig_fromJson(JsonObject& ONOFFdata) { + Config_update(ONOFFdata, "uselaststate", ONOFFConfig.useLastStateOnStart); + Config_update(ONOFFdata, "cmd", ONOFFConfig.ONOFFState); + + if (ONOFFdata.containsKey("erase") && ONOFFdata["erase"].as()) { + // Erase config from NVS (non-volatile storage) + preferences.begin(Gateway_Short_Name, false); + preferences.remove("ONOFFConfig"); + preferences.end(); + Log.notice(F("ONOFF config erased" CR)); + return; // Erase prevails on save, so skipping save + } + if (ONOFFdata.containsKey("save") && ONOFFdata["save"].as()) { + StaticJsonDocument jsonBuffer; + JsonObject jo = jsonBuffer.to(); + jo["uselaststate"] = ONOFFConfig.useLastStateOnStart; + jo["cmd"] = ONOFFConfig.ONOFFState; + // Save config into NVS (non-volatile storage) + String conf = ""; + serializeJson(jsonBuffer, conf); + preferences.begin(Gateway_Short_Name, false); + preferences.putString("ONOFFConfig", conf); + preferences.end(); + Log.notice(F("ONOFF config saved" CR)); + } +} + +void ONOFFConfig_load() { + StaticJsonDocument jsonBuffer; + preferences.begin(Gateway_Short_Name, true); + auto error = deserializeJson(jsonBuffer, preferences.getString("ONOFFConfig", "{}")); + preferences.end(); + if (error) { + Log.error(F("ONOFF config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity()); + return; + } + if (jsonBuffer.isNull()) { + Log.warning(F("ONOFF config is null" CR)); + return; + } + JsonObject jo = jsonBuffer.as(); + ONOFFConfig_fromJson(jo); + Log.notice(F("ONOFF config loaded" CR)); +} +# else +void ONOFFConfig_init(){}; +void ONOFFConfig_fromJson(JsonObject& ONOFFdata){}; +void ONOFFConfig_load(){}; +# endif + void setupONOFF() { # ifdef MAX_CURRENT_ACTUATOR - xTaskCreate(overLimitCurrent, "overLimitCurrent", 2500, NULL, 10, NULL); + xTaskCreate(overLimitCurrent, "overLimitCurrent", 4000, NULL, 10, NULL); # endif # ifdef MAX_TEMP_ACTUATOR - xTaskCreate(overLimitTemp, "overLimitTemp", 2500, NULL, 10, NULL); + xTaskCreate(overLimitTemp, "overLimitTemp", 4000, NULL, 10, NULL); +# endif +# ifdef ESP32 + ONOFFConfig_init(); + ONOFFConfig_load(); + Log.notice(F("Target state on restart: %T" CR), ONOFFConfig.ONOFFState); + Log.notice(F("Use last state on restart: %T" CR), ONOFFConfig.useLastStateOnStart); # endif pinMode(ACTUATOR_ONOFF_GPIO, OUTPUT); +# ifdef ACTUATOR_ONOFF_DEFAULT + digitalWrite(ACTUATOR_ONOFF_GPIO, ACTUATOR_ONOFF_DEFAULT); +# elif defined(ESP32) + if (ONOFFConfig.useLastStateOnStart) + digitalWrite(ACTUATOR_ONOFF_GPIO, ONOFFConfig.ONOFFState); +# endif if (digitalRead(ACTUATOR_ONOFF_GPIO) == ACTUATOR_ON) { PowerIndicatorON(); } else { PowerIndicatorOFF(); } -# ifdef ACTUATOR_ONOFF_DEFAULT - digitalWrite(ACTUATOR_ONOFF_GPIO, ACTUATOR_ONOFF_DEFAULT); -# endif Log.trace(F("ZactuatorONOFF setup done" CR)); } @@ -65,6 +133,13 @@ void MQTTtoONOFF(char* topicOri, JsonObject& ONOFFdata) { } else { PowerIndicatorOFF(); } +# ifdef ESP32 + if (ONOFFConfig.useLastStateOnStart) { + ONOFFdata["save"] = true; + ONOFFConfig_fromJson(ONOFFdata); + ONOFFdata.remove("save"); + } +# endif // we acknowledge the sending by publishing the value to an acknowledgement topic pub(subjectGTWONOFFtoMQTT, ONOFFdata); } else { @@ -91,6 +166,25 @@ void MQTTtoONOFF(char* topicOri, JsonObject& ONOFFdata) { } } } + if (cmpToMainTopic(topicOri, subjectMQTTtoONOFFset)) { + Log.trace(F("MQTTtoONOFF json set" CR)); + /* + * Configuration modifications priorities: + * First `init=true` and `load=true` commands are executed (if both are present, INIT prevails on LOAD) + * Then parameters included in json are taken in account + * Finally `erase=true` and `save=true` commands are executed (if both are present, ERASE prevails on SAVE) + */ + if (ONOFFdata.containsKey("init") && ONOFFdata["init"].as()) { + // Restore the default (initial) configuration + ONOFFConfig_init(); + } else if (ONOFFdata.containsKey("load") && ONOFFdata["load"].as()) { + // Load the saved configuration, if not initialised + ONOFFConfig_load(); + } + + // Load config from json if available + ONOFFConfig_fromJson(ONOFFdata); + } } # endif @@ -135,7 +229,7 @@ void overLimitTemp(void* pvParameters) { Log.trace(F("Internal temperature of the ESP32 %F" CR), internalTempc); // We switch OFF the actuator if the temperature of the ESP32 is more than MAX_TEMP_ACTUATOR two consecutive times, so as to avoid false single readings to trigger the relay OFF. if (internalTempc > MAX_TEMP_ACTUATOR && previousInternalTempc > MAX_TEMP_ACTUATOR) { - if (digitalRead(ACTUATOR_ONOFF_GPIO) == ACTUATOR_ON) { // This could be with thew previous condition, but it is better to trigger the digitalRead only if the previous condition is met + if (digitalRead(ACTUATOR_ONOFF_GPIO) == ACTUATOR_ON) { // This could be with the previous condition, but it is better to trigger the digitalRead only if the previous condition is met to avoid the digitalRead Log.error(F("[ActuatorONOFF] OverTemperature detected ( %F > %F ) switching OFF Actuator" CR), internalTempc, MAX_TEMP_ACTUATOR); ActuatorTrigger(); CriticalIndicatorON(); @@ -152,16 +246,18 @@ void overLimitTemp(void* pvParameters) { # ifdef MAX_CURRENT_ACTUATOR void overLimitCurrent(void* pvParameters) { for (;;) { + static float previousCurrent = 0; float current = getRN8209current(); Log.trace(F("RN8209 Current %F" CR), current); // We switch OFF the actuator if the current of the RN8209 is more than MAX_CURRENT_ACTUATOR. - if (current > MAX_CURRENT_ACTUATOR) { - if (digitalRead(ACTUATOR_ONOFF_GPIO) == ACTUATOR_ON) { // This could be with thew previous condition, but it is better to trigger the digitalRead only if the previous condition is met + if (current > MAX_CURRENT_ACTUATOR && previousCurrent > MAX_CURRENT_ACTUATOR) { + if (digitalRead(ACTUATOR_ONOFF_GPIO) == ACTUATOR_ON) { // This could be with the previous condition, but it is better to trigger the digitalRead only if the previous condition is met to avoid the digitalRead Log.error(F("[ActuatorONOFF] OverCurrent detected ( %F > %F ) switching OFF Actuator" CR), current, MAX_CURRENT_ACTUATOR); ActuatorTrigger(); CriticalIndicatorON(); } } + previousCurrent = current; vTaskDelay(TimeBetweenReadingCurrent); } } @@ -185,6 +281,13 @@ void ActuatorTrigger() { StaticJsonDocument jsonBuffer; JsonObject ONOFFdata = jsonBuffer.to(); ONOFFdata["cmd"] = (int)level; +# ifdef ESP32 + if (ONOFFConfig.useLastStateOnStart) { + ONOFFdata["save"] = true; + ONOFFConfig_fromJson(ONOFFdata); + ONOFFdata.remove("save"); + } +# endif pub(subjectGTWONOFFtoMQTT, ONOFFdata); } diff --git a/main/ZgatewayBT.ino b/main/ZgatewayBT.ino index 2d73fe0..4a1d536 100644 --- a/main/ZgatewayBT.ino +++ b/main/ZgatewayBT.ino @@ -101,20 +101,6 @@ void BTConfig_init() { unsigned long timeBetweenConnect = 0; unsigned long timeBetweenActive = 0; -template // Declared here to avoid pre-compilation issue (missing "template" in auto declaration by pio) -void BTConfig_update(JsonObject& data, const char* key, T& var); -template -void BTConfig_update(JsonObject& data, const char* key, T& var) { - if (data.containsKey(key)) { - if (var != data[key].as()) { - var = data[key].as(); - Log.notice(F("BT config %s changed: %s" CR), key, data[key].as()); - } else { - Log.notice(F("BT config %s unchanged: %s" CR), key, data[key].as()); - } - } -} - void stateBTMeasures(bool start) { StaticJsonDocument jsonBuffer; JsonObject jo = jsonBuffer.to(); @@ -150,7 +136,7 @@ void stateBTMeasures(bool start) { void BTConfig_fromJson(JsonObject& BTdata, bool startup = false) { // Attempts to connect to eligible devices or not - BTConfig_update(BTdata, "bleconnect", BTConfig.bleConnect); + Config_update(BTdata, "bleconnect", BTConfig.bleConnect); // Identify AdaptiveScan deactivation to pass to continuous mode if (startup == false) { if (BTdata.containsKey("adaptivescan") && BTdata["adaptivescan"] == false && BTConfig.adaptiveScan == true) { @@ -161,37 +147,37 @@ void BTConfig_fromJson(JsonObject& BTdata, bool startup = false) { BTdata["intervalacts"] = TimeBtwActive; } } - BTConfig_update(BTdata, "adaptivescan", BTConfig.adaptiveScan); + Config_update(BTdata, "adaptivescan", BTConfig.adaptiveScan); // Time before before active scan // Scan interval set if (BTdata.containsKey("interval") && BTdata["interval"] != 0) - BTConfig_update(BTdata, "interval", BTConfig.BLEinterval); + Config_update(BTdata, "interval", BTConfig.BLEinterval); // Define if the scan is adaptive or not - BTConfig_update(BTdata, "intervalacts", BTConfig.intervalActiveScan); + Config_update(BTdata, "intervalacts", BTConfig.intervalActiveScan); // Time before a connect set - BTConfig_update(BTdata, "intervalcnct", BTConfig.intervalConnect); + Config_update(BTdata, "intervalcnct", BTConfig.intervalConnect); // publish all BLE devices discovered or only the identified sensors (like temperature sensors) - BTConfig_update(BTdata, "onlysensors", BTConfig.pubOnlySensors); + Config_update(BTdata, "onlysensors", BTConfig.pubOnlySensors); // Home Assistant presence message - BTConfig_update(BTdata, "hasspresence", BTConfig.presenceEnable); + Config_update(BTdata, "hasspresence", BTConfig.presenceEnable); // Home Assistant presence message topic - BTConfig_update(BTdata, "presenceTopic", BTConfig.presenceTopic); + Config_update(BTdata, "presenceTopic", BTConfig.presenceTopic); // Home Assistant presence message use iBeacon UUID - BTConfig_update(BTdata, "presenceUseBeaconUuid", BTConfig.presenceUseBeaconUuid); + Config_update(BTdata, "presenceUseBeaconUuid", BTConfig.presenceUseBeaconUuid); // MinRSSI set - BTConfig_update(BTdata, "minrssi", BTConfig.minRssi); + Config_update(BTdata, "minrssi", BTConfig.minRssi); // Send undecoded device data - BTConfig_update(BTdata, "extDecoderEnable", BTConfig.extDecoderEnable); + Config_update(BTdata, "extDecoderEnable", BTConfig.extDecoderEnable); // Topic to send undecoded device data - BTConfig_update(BTdata, "extDecoderTopic", BTConfig.extDecoderTopic); + Config_update(BTdata, "extDecoderTopic", BTConfig.extDecoderTopic); // Sets whether to filter publishing - BTConfig_update(BTdata, "filterConnectable", BTConfig.filterConnectable); + Config_update(BTdata, "filterConnectable", BTConfig.filterConnectable); // Publish advertisment data - BTConfig_update(BTdata, "pubadvdata", BTConfig.pubAdvData); + Config_update(BTdata, "pubadvdata", BTConfig.pubAdvData); // Use iBeacon UUID as topic, instead of sender (random) MAC address - BTConfig_update(BTdata, "pubBeaconUuidForTopic", BTConfig.pubBeaconUuidForTopic); + Config_update(BTdata, "pubBeaconUuidForTopic", BTConfig.pubBeaconUuidForTopic); // Disable Whitelist & Blacklist - BTConfig_update(BTdata, "ignoreWBlist", (BTConfig.ignoreWBlist)); + Config_update(BTdata, "ignoreWBlist", (BTConfig.ignoreWBlist)); stateBTMeasures(startup); diff --git a/main/config_ONOFF.h b/main/config_ONOFF.h index c3ee1ff..ea34458 100644 --- a/main/config_ONOFF.h +++ b/main/config_ONOFF.h @@ -33,9 +33,14 @@ extern void MQTTtoONOFF(char* topicOri, JsonObject& RFdata); /*-------------DEFINE YOUR MQTT PARAMETERS BELOW----------------*/ #define subjectMQTTtoONOFF "/commands/MQTTtoONOFF" #define subjectGTWONOFFtoMQTT "/ONOFFtoMQTT" +#define subjectMQTTtoONOFFset "/commands/MQTTtoONOFF/config" #define ONKey "setON" #define OFFKey "setOFF" + +#ifndef USE_LAST_STATE_ON_RESTART +# define USE_LAST_STATE_ON_RESTART true // Define if we use the last state upon a power restart +#endif #ifndef ACTUATOR_ON # define ACTUATOR_ON LOW // LOW or HIGH, set to the output level of the GPIO pin to turn the actuator on. #endif @@ -71,4 +76,15 @@ extern void MQTTtoONOFF(char* topicOri, JsonObject& RFdata); # endif #endif +#ifdef ESP32 +/*----------------CONFIGURABLE PARAMETERS-----------------*/ +struct ONOFFConfig_s { + bool ONOFFState; // Recorded actuator state + bool useLastStateOnStart; // Do we use the recorded actuator state on start +}; + +// Global struct to store live ONOFF configuration data +extern ONOFFConfig_s ONOFFConfig; +#endif + #endif diff --git a/main/main.ino b/main/main.ino index 7af024f..61465ef 100644 --- a/main/main.ino +++ b/main/main.ino @@ -279,6 +279,20 @@ ESP8266WiFiMulti wifiMulti; // client link to pubsub MQTT PubSubClient client; +template // Declared here to avoid pre-compilation issue (missing "template" in auto declaration by pio) +void Config_update(JsonObject& data, const char* key, T& var); +template +void Config_update(JsonObject& data, const char* key, T& var) { + if (data.containsKey(key)) { + if (var != data[key].as()) { + var = data[key].as(); + Log.notice(F("Config %s changed: %s" CR), key, data[key].as()); + } else { + Log.notice(F("Config %s unchanged: %s" CR), key, data[key].as()); + } + } +} + void revert_hex_data(const char* in, char* out, int l) { //reverting array 2 by 2 to get the data in good order int i = l - 2, j = 0;