/*
  OpenMQTTGateway  - ESP8266 or Arduino program for home automation

   Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker
   Send and receiving command by MQTT

  This program enables to:
 - receive MQTT data from a topic and send signal (RF, IR, BLE, GSM)  corresponding to the received MQTT data
 - publish MQTT data to a different topic related to received signals (RF, IR, BLE, GSM)

  Copyright: (c)Florian ROBERT

    This file is part of OpenMQTTGateway.

    OpenMQTTGateway is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    OpenMQTTGateway is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#include "User_config.h"

enum GatewayState {
  WAITING_ONBOARDING,
  ONBOARDING,
  OFFLINE,
  NTWK_CONNECTED,
  BROKER_CONNECTED,
  PROCESSING,
  NTWK_DISCONNECTED,
  BROKER_DISCONNECTED,
  LOCAL_OTA_IN_PROGRESS,
  REMOTE_OTA_IN_PROGRESS,
  SLEEPING,
  ERROR
};
GatewayState gatewayState = GatewayState::WAITING_ONBOARDING;

// Macros and structure to enable the duplicates removing on the following gateways
#if defined(ZgatewayRF) || defined(ZgatewayIR) || defined(ZgatewaySRFB) || defined(ZgatewayWeatherStation) || defined(ZgatewayRTL_433)
// array to store previous received RFs, IRs codes and their timestamps
struct ReceivedSignal {
  uint64_t value;
  uint32_t time;
};

ReceivedSignal receivedSignal[] = {{0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}};

#  define struct_size (sizeof(receivedSignal) / sizeof(ReceivedSignal))
#endif

//Time used to wait for an interval before measures
unsigned long timer_sys_measures = 0;

// Time used to wait before system checkings
unsigned long timer_sys_checks = 0;

// First Start of offline mode modules
bool firstStart = true;

#define ARDUINOJSON_USE_LONG_LONG     1
#define ARDUINOJSON_ENABLE_STD_STRING 1

#include <queue>
int queueLength = -1; // We want to give one cycle of the loop before starting modules that are big msgs producers (example BT), to avoid queue overloading
unsigned long queueLengthSum = 0;
unsigned long blockedMessages = 0;
unsigned long receivedMessages = 0;
int maxQueueLength = 0;
#ifndef QueueSize
#  define QueueSize 18
#endif

/**
 * Deep-sleep for the ESP8266 & ESP32 we need some form of indicator that we have posted the measurements and am ready to deep sleep.
 * Set this to true in the sensor code after publishing the measurement.
 */
bool ready_to_sleep = false;

#include <ArduinoJson.h>
#include <ArduinoLog.h>
#include <PicoMQTT.h>

#include <memory>

#include "LEDManager.h"
#include "TheengsUtils.h"

LEDManager ledManager;

struct JsonBundle {
  StaticJsonDocument<JSON_MSG_BUFFER> doc;
};

std::queue<std::string> jsonQueue;

#ifdef ESP32
#  include <driver/adc.h>
// Mutex  to protect the queue
SemaphoreHandle_t xQueueMutex;
// Mutex to protect mqtt publish
SemaphoreHandle_t xMqttMutex;
#endif

StaticJsonDocument<JSON_MSG_BUFFER> modulesBuffer;
JsonArray modules = modulesBuffer.to<JsonArray>();
bool ethConnected = false;

#ifndef ZgatewayGFSunInverter
// Arduino IDE compiles, it automatically creates all the header declarations for all the functions you have in your *.ino file.
// Unfortunately it ignores #if directives.
// This is a simple workaround for this problem.
struct GfSun2000Data {};
#endif

// Modules config inclusion
#if defined(ZwebUI) && defined(ESP32)
#  include "config_WebUI.h"
#endif
#if defined(ZgatewayRF) || defined(ZgatewayRF2) || defined(ZgatewayPilight) || defined(ZactuatorSomfy) || defined(ZgatewayRTL_433)
#  include "config_RF.h"
#endif
#ifdef ZgatewayWeatherStation
#  include "config_WeatherStation.h"
#endif
#ifdef ZgatewayGFSunInverter
#  include "config_GFSunInverter.h"
#endif
#ifdef ZgatewayLORA
#  include "config_LORA.h"
#endif
#ifdef ZgatewaySRFB
#  include "config_SRFB.h"
#endif
#ifdef ZgatewayBT
#  include "config_BT.h"
#endif
#ifdef ZgatewayIR
#  include "config_IR.h"
#endif
#ifdef Zgateway2G
#  include "config_2G.h"
#endif
#ifdef ZactuatorONOFF
#  include "config_ONOFF.h"
#endif
#ifdef ZsensorINA226
#  include "config_INA226.h"
#endif
#ifdef ZsensorHCSR501
#  include "config_HCSR501.h"
#endif
#ifdef ZsensorADC
#  include "config_ADC.h"
#endif
#ifdef ZsensorBH1750
#  include "config_BH1750.h"
#endif
#ifdef ZsensorMQ2
#  include "config_MQ2.h"
#endif
#ifdef ZsensorTEMT6000
#  include "config_TEMT6000.h"
#endif
#ifdef ZsensorTSL2561
#  include "config_TSL2561.h"
#endif
#ifdef ZsensorBME280
#  include "config_BME280.h"
#endif
#ifdef ZsensorHTU21
#  include "config_HTU21.h"
#endif
#ifdef ZsensorLM75
#  include "config_LM75.h"
#endif
#ifdef ZsensorAHTx0
#  include "config_AHTx0.h"
#endif
#ifdef ZsensorRN8209
#  include "config_RN8209.h"
#endif
#ifdef ZsensorHCSR04
#  include "config_HCSR04.h"
#endif
#ifdef ZsensorC37_YL83_HMRD
#  include "config_C37_YL83_HMRD.h"
#endif
#ifdef ZsensorDHT
#  include "config_DHT.h"
#endif
#ifdef ZsensorSHTC3
#  include "config_SHTC3.h"
#endif
#ifdef ZsensorDS1820
#  include "config_DS1820.h"
#endif
#ifdef ZgatewayRFM69
#  include "config_RFM69.h"
#endif
#ifdef ZsensorGPIOInput
#  include "config_GPIOInput.h"
#endif
#ifdef ZsensorGPIOKeyCode
#  include "config_GPIOKeyCode.h"
#endif
#ifdef ZsensorTouch
#  include "config_Touch.h"
#endif
#ifdef ZmqttDiscovery
#  include "config_mqttDiscovery.h"
#endif
#ifdef ZactuatorFASTLED
#  include "config_FASTLED.h"
#endif
#ifdef ZactuatorPWM
#  include "config_PWM.h"
#endif
#ifdef ZactuatorSomfy
#  include "config_Somfy.h"
#endif
#if defined(ZboardM5STICKC) || defined(ZboardM5STICKCP) || defined(ZboardM5STACK) || defined(ZboardM5TOUGH)
#  include "config_M5.h"
#endif
#if defined(ZdisplaySSD1306)
#  include "config_SSD1306.h"
#endif
#if defined(ZgatewaySERIAL)
#  include "config_SERIAL.h"
#endif
/*------------------------------------------------------------------------*/

void setupTLS(int index = CNT_DEFAULT_INDEX);

char ota_pass[parameters_size] = gw_password;
#ifdef USE_MAC_AS_GATEWAY_NAME
#  undef WifiManager_ssid
#  undef ota_hostname
#  define MAC_NAME_MAX_LEN 30
char WifiManager_ssid[MAC_NAME_MAX_LEN];
char ota_hostname[MAC_NAME_MAX_LEN];
#endif
int failure_number_ntwk = 0; // number of failure connecting to network
int failure_number_mqtt = 0; // number of failure connecting to MQTT

static unsigned long last_ota_activity_millis = 0;
// Global struct to store live SYS configuration data
SYSConfig_s SYSConfig;

bool failSafeMode = false;
bool ProcessLock = true; // Process lock when we want to use a critical function like OTA for example
static bool mqttSetupPending = true;
static int cnt_index = CNT_DEFAULT_INDEX;

#ifdef ESP32
#  include <ArduinoOTA.h>
#  include <FS.h>
#  include <SPIFFS.h>
#  include <esp_task_wdt.h>
#  include <nvs.h>
#  include <nvs_flash.h>

bool BTProcessLock = true; // Process lock when we want to use a critical function like OTA for example, at start to true so as to wait for critical functions to be performed before BLE start

#  if !defined(NO_INT_TEMP_READING)
// ESP32 internal temperature reading
#    include <stdio.h>

#    include "rom/ets_sys.h"
#    include "soc/rtc_cntl_reg.h"
#    include "soc/sens_reg.h"
#  endif
#  ifdef ESP32_ETHERNET
#    include <ETH.h>
void WiFiEvent(WiFiEvent_t event);
#  endif

#  include <WiFiClientSecure.h>
#  include <WiFiMulti.h>
WiFiMulti wifiMulti;
#  include <WiFiManager.h>
#  ifdef MDNS_SD
#    include <ESPmDNS.h>
#  endif

#elif defined(ESP8266)
#  include <ArduinoOTA.h>
#  include <DNSServer.h>
#  include <ESP8266WebServer.h>
#  include <ESP8266WiFi.h>
#  include <ESP8266WiFiMulti.h>
#  include <FS.h>
#  include <WiFiManager.h>
X509List caCert;
#  if MQTT_SECURE_SIGNED_CLIENT
X509List* pClCert = nullptr;
PrivateKey* pClKey = nullptr;
#  endif
ESP8266WiFiMulti wifiMulti;
#  ifdef MDNS_SD
#    include <ESP8266mDNS.h>
#  endif

#else
#  include <Ethernet.h>
#endif

void handle_autodiscovery() {
#ifdef ZmqttDiscovery
  static bool connectedOnce = false;
  const unsigned long now = millis();

  // at first connection we publish the discovery payloads
  // or, when we have just re-connected (only when discovery_republish_on_reconnect is enabled)
  const bool publishDiscovery = SYSConfig.discovery && (!connectedOnce || discovery_republish_on_reconnect);

  if (publishDiscovery) {
    pubMqttDiscovery();
#  ifdef ZgatewayLORA
    launchLORADiscovery(true);
#  endif
#  ifdef ZgatewayBT
    launchBTDiscovery(true);
#  endif
#  ifdef ZgatewayRTL_433
    launchRTL_433Discovery(true);
#  endif
  }

  connectedOnce = true;
#endif
}

#if MQTT_BROKER_MODE

class MQTTServer : public PicoMQTT::Server {
public:
  size_t get_client_count() const {
    return clients.size();
  }

  bool connected() const {
    return !clients.empty();
  }

protected:
  virtual void on_subscribe(const char* client_id, const char* topic) override {
    // Whenever a client subscribes successfully to some topic, see if this is likely a subscription to a
    // autodiscovery topic.  If it is, fire handle_autodiscovery().
    const String pattern(topic);
    const bool is_autodiscovery_subscription = (pattern == "#") || (pattern.startsWith(String(discovery_prefix) + "/"));
    if (is_autodiscovery_subscription)
      handle_autodiscovery();
  }
};

std::unique_ptr<MQTTServer> mqtt;
#else
std::unique_ptr< ::Client> eClient;
std::unique_ptr<PicoMQTT::Client> mqtt;
#endif

template <typename T> // 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 <typename T>
void Config_update(JsonObject& data, const char* key, T& var) {
  if (data.containsKey(key)) {
    if (var != data[key].as<T>()) {
      var = data[key].as<T>();
      Log.notice(F("Config %s changed to: %T" CR), key, data[key].as<T>());
    } else {
      Log.notice(F("Config %s unchanged, currently: %T" CR), key, data[key].as<T>());
    }
  }
}

/*
 * Dispatch json messages towards the communication layer
 *
*/
bool jsonDispatch(JsonObject& data) {
  bool res = false;
  if (data.containsKey("origin") || data.containsKey("topic")) {
    GatewayState previousGatewayState = gatewayState;
    gatewayState = GatewayState::PROCESSING;
#if message_UTCtimestamp == true
    data["UTCtime"] = TheengsUtils::UTCtimestamp();
#endif
#if message_unixtimestamp == true
    data["unixtime"] = TheengsUtils::unixtimestamp();
#endif
    if (data.containsKey("origin")) {
      pubWebUI((char*)data["origin"].as<const char*>(), data);
    }
    if (SYSConfig.mqtt && !SYSConfig.offline) {
      res = pub(data);
    }
#ifdef ZgatewaySERIAL
    if (SYSConfig.serial) {
      char jsonStr[JSON_MSG_BUFFER_MAX];
      serializeJson(data, jsonStr);
      receivingDATA("", jsonStr);
      res = true; // Return the state from receivingDATA
    }
#endif
    gatewayState = previousGatewayState; // restore the previous state
  } else {
    Log.error(F("No origin or topic in JSON filtered" CR));
    gatewayState = GatewayState::ERROR;
  }
  return res;
}

// Add a document to the queue
boolean enqueueJsonObject(const StaticJsonDocument<JSON_MSG_BUFFER>& jsonDoc, int timeout) {
  receivedMessages++;
  if (jsonDoc.size() == 0) {
    Log.error(F("Empty JSON, skipping" CR));
    gatewayState = GatewayState::ERROR;
    return true;
  }
  if (queueLength >= QueueSize) {
    Log.warning(F("%d Doc(s) in queue, doc blocked" CR), queueLength);
    blockedMessages++;
    return false;
  }
  Log.trace(F("Enqueue JSON" CR));
  std::string jsonString;
  serializeJson(jsonDoc, jsonString);
#ifdef ESP32
  // Semaphore check before enqueueing a document
  if (xSemaphoreTake(xQueueMutex, pdMS_TO_TICKS(timeout)) == pdFALSE) {
    Log.error(F("xQueueMutex not taken" CR));
    gatewayState = GatewayState::ERROR;
    blockedMessages++;
    return false;
  }
#endif
  jsonQueue.push(jsonString);
#ifdef ESP32
  xSemaphoreGive(xQueueMutex);
#endif
  Log.trace(F("Queue length: %d" CR), jsonQueue.size());
  return true;
}

// Semaphore check before enqueueing a document with default timeout QueueSemaphoreTimeOutLoop
bool enqueueJsonObject(const StaticJsonDocument<JSON_MSG_BUFFER>& jsonDoc) {
  return enqueueJsonObject(jsonDoc, QueueSemaphoreTimeOutLoop);
}

#ifdef ESP32
#  include "mbedtls/sha256.h"

std::string generateHash(const std::string& input) {
  unsigned char hash[32];
  mbedtls_sha256((unsigned char*)input.c_str(), input.length(), hash, 0);

  char hashString[65]; // Room for null terminator
  for (int i = 0; i < 32; ++i) {
    sprintf(&hashString[i * 2], "%02x", hash[i]);
  }

  return std::string(hashString);
}
#else
std::string generateHash(const std::string& input) {
  return "Not implemented for ESP8266";
}
#endif

/*
 * Add the jsonObject id as a topic to the jsonObject origin
 *
*/
void buildTopicFromId(JsonObject& Jsondata, const char* origin) {
  if (!Jsondata.containsKey("id")) {
    Log.error(F("No id in Jsondata" CR));
    gatewayState = GatewayState::ERROR;
    return;
  }

  std::string topic = Jsondata["id"].as<std::string>();

  // Replace ":" in topic
  size_t pos = topic.find(":");
  while (pos != std::string::npos) {
    topic.erase(pos, 1);
    pos = topic.find(":", pos);
  }
#ifdef ZgatewayBT
  if (BTConfig.pubBeaconUuidForTopic && !BTConfig.extDecoderEnable && Jsondata.containsKey("model_id") && Jsondata["model_id"].as<std::string>() == "IBEACON") {
    if (Jsondata.containsKey("uuid")) {
      topic = Jsondata["uuid"].as<std::string>();
    } else {
      Log.error(F("No uuid in Jsondata" CR));
      gatewayState = GatewayState::ERROR;
    }
  }

  if (BTConfig.extDecoderEnable && !Jsondata.containsKey("model"))
    topic = BTConfig.extDecoderTopic.c_str();
#endif
  std::string subjectStr(origin);
  topic = subjectStr + "/" + topic;

  Jsondata["origin"] = topic;

  Log.trace(F("Origin: %s" CR), Jsondata["origin"].as<const char*>());
}

// Empty the documents queue
void emptyQueue() {
  queueLength = jsonQueue.size();
  if (queueLength > maxQueueLength) {
    maxQueueLength = queueLength;
  }
  if (queueLength <= 0) {
    return;
  }
  Log.trace(F("Dequeue JSON" CR));
  DynamicJsonDocument jsonBuffer(JSON_MSG_BUFFER_MAX);
  JsonObject obj = jsonBuffer.to<JsonObject>();
#ifdef ESP32
  if (xSemaphoreTake(xQueueMutex, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdFALSE) {
    Log.error(F("xQueueMutex not taken" CR));
    gatewayState = GatewayState::ERROR;
    return;
  }
#endif
  auto error = deserializeJson(jsonBuffer, jsonQueue.front());
  jsonQueue.pop();
#ifdef ESP32
  xSemaphoreGive(xQueueMutex);
#endif
  if (error) {
    Log.error(F("deserialize jsonQueue.front() failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity());
    gatewayState = GatewayState::ERROR;
  } else {
    if (jsonDispatch(obj))
      queueLengthSum++;
  }
}

/**
 * @brief Publish the payload on default MQTT topic.
 *
 * @param topicori suffix to add on default MQTT Topic
 * @param payload  the message to sends
 * @param retainFlag true if you what a retain
 */
bool pub(const char* topicori, const char* payload, bool retainFlag) {
  String topic = String(mqtt_topic) + String(gateway_name) + String(topicori);
  return pubMQTT(topic.c_str(), payload, retainFlag);
}

/**
 * @brief Publish the payload on default MQTT topic
 *
 * @param topicori suffix to add on default MQTT Topic
 * @param data The Json Object that represents the message
 */
bool pub(JsonObject& data) {
  bool res = false;
  bool ret = sensor_Retain;
  if (data.containsKey("retain") && data["retain"].is<bool>()) {
    ret = data["retain"];
    data.remove("retain");
  }
  if (data.size() == 0) {
    Log.error(F("Empty JSON, not published" CR));
    gatewayState = GatewayState::ERROR;
    return res;
  }
  String topic;
  if (data.containsKey("origin") && data["origin"].is<const char*>()) {
    topic = String(mqtt_topic) + String(gateway_name) + String(data["origin"].as<const char*>());
    data.remove("origin");
  } else if (data.containsKey("topic") && data["topic"].is<const char*>()) {
    topic = data["topic"].as<const char*>();
    if (data.containsKey("info_topic") && data["info_topic"].is<const char*>()) {
      // Sometimes it is necessary to provide information about the publishing topic, not just use it.
      // This is the case, for example, for the RF2MQTT device trigger announcement message where the
      // temporary variable info_topic provides information about the topic that will be used to publish the message,
      // and it can be different of current message topic (This is a clever pun, I hope it's clear).
      data["topic"].set(data["info_topic"]);
      data.remove("info_topic");
    } else {
      data.remove("topic");
    }

  } else {
    Log.error(F("No topic or origin in JSON, not published" CR));
    gatewayState = GatewayState::ERROR;
    return res;
  }

#if valueAsATopic
#  ifdef ZgatewayPilight
  String value = data["value"];
  String protocol = data["protocol"];
  if (value != "null" && value != 0) {
    topic = topic + "/" + protocol + "/" + value;
  }
#  else
  uint64_t value = data["value"];
  if (value != 0) {
    topic = topic + "/" + TheengsUtils::toString(value);
  }
#  endif
#endif

#if jsonPublishing
  String dataAsString = "";
  serializeJson(data, dataAsString);
  res = pubMQTT(topic.c_str(), dataAsString.c_str(), ret);
#endif

#if simplePublishing
  Log.trace(F("simplePub - ON" CR));
  // Loop through all the key-value pairs in obj
  for (JsonPair p : data) {
#  if defined(ESP8266)
    yield();
#  endif
    if (p.value().is<uint64_t>() && strcmp(p.key().c_str(), "rssi") != 0) { //test rssi , bypass solution due to the fact that a int is considered as an uint64_t
      if (strcmp(p.key().c_str(), "value") == 0) { // if data is a value we don't integrate the name into the topic
        res = pubMQTT(topic, p.value().as<uint64_t>());
      } else { // if data is not a value we integrate the name into the topic
        res = pubMQTT(topic + "/" + String(p.key().c_str()), p.value().as<uint64_t>());
      }
    } else if (p.value().is<int>()) {
      res = pubMQTT(topic + "/" + String(p.key().c_str()), p.value().as<int>());
    } else if (p.value().is<float>()) {
      res = pubMQTT(topic + "/" + String(p.key().c_str()), p.value().as<float>());
    } else if (p.value().is<char*>()) {
      res = pubMQTT(topic + "/" + String(p.key().c_str()), p.value().as<const char*>());
    }
  }
#endif
  return res;
}

/**
 * @brief Publish the payload on default MQTT topic
 *
 * @param topicori suffix to add on default MQTT Topic
 * @param payload the message to sends
 */
bool pub(const char* topicori, const char* payload) {
  String topic = String(mqtt_topic) + String(gateway_name) + String(topicori);
  return pubMQTT(topic, payload);
}

/**
 * @brief Low level MQTT functions without retain
 *
 * @param topic  the topic
 * @param payload  the payload
 */
bool pubMQTT(const char* topic, const char* payload) {
  return pubMQTT(topic, payload, sensor_Retain);
}

/**
 * @brief Very Low level MQTT functions with retain Flag
 *
 * @param topic the topic
 * @param payload the payload
 * @param retainFlag  true if retain the retain Flag
 */
bool pubMQTT(const char* topic, const char* payload, bool retainFlag) {
  bool res = false;
  if (SYSConfig.mqtt && !SYSConfig.offline) {
#ifdef ESP32
    if (xSemaphoreTake(xMqttMutex, pdMS_TO_TICKS(QueueSemaphoreTimeOutTask)) == pdFALSE) {
      Log.error(F("xMqttMutex not taken" CR));
      gatewayState = GatewayState::ERROR;
      return res;
    }
#endif
    if (mqtt && mqtt->connected()) {
      Log.notice(F("[ OMG->MQTT ] topic: %s msg: %s " CR), topic, payload);
      res = mqtt->publish(topic, payload, 0, retainFlag);
    } else {
      Log.warning(F("MQTT not connected, aborting the publication" CR));
    }
#ifdef ESP32
    xSemaphoreGive(xMqttMutex);
#endif
  } else {
    Log.notice(F("[ OMG->MQTT deactivated or offline] topic: %s msg: %s " CR), topic, payload);
  }
  return res;
}

bool pubMQTT(String topic, const char* payload) {
  return pubMQTT(topic.c_str(), payload);
}

bool pubMQTT(const char* topic, unsigned long payload) {
  char val[11];
  sprintf(val, "%lu", payload);
  return pubMQTT(topic, val);
}

bool pubMQTT(const char* topic, unsigned long long payload) {
  char val[21];
  sprintf(val, "%llu", payload);
  return pubMQTT(topic, val);
}

bool pubMQTT(const char* topic, String payload) {
  return pubMQTT(topic, payload.c_str());
}

bool pubMQTT(String topic, String payload) {
  return pubMQTT(topic.c_str(), payload.c_str());
}

bool pubMQTT(String topic, int payload) {
  char val[12];
  sprintf(val, "%d", payload);
  return pubMQTT(topic.c_str(), val);
}

bool pubMQTT(String topic, unsigned long long payload) {
  char val[21];
  sprintf(val, "%llu", payload);
  return pubMQTT(topic.c_str(), val);
}

bool pubMQTT(String topic, float payload) {
  char val[12];
  dtostrf(payload, 3, 1, val);
  return pubMQTT(topic.c_str(), val);
}

bool pubMQTT(const char* topic, float payload) {
  char val[12];
  dtostrf(payload, 3, 1, val);
  return pubMQTT(topic, val);
}

bool pubMQTT(const char* topic, int payload) {
  char val[12];
  sprintf(val, "%d", payload);
  return pubMQTT(topic, val);
}

bool pubMQTT(const char* topic, unsigned int payload) {
  char val[12];
  sprintf(val, "%u", payload);
  return pubMQTT(topic, val);
}

bool pubMQTT(const char* topic, long payload) {
  char val[11];
  sprintf(val, "%ld", payload);
  return pubMQTT(topic, val);
}

bool pubMQTT(const char* topic, double payload) {
  char val[16];
  sprintf(val, "%f", payload);
  return pubMQTT(topic, val);
}

bool pubMQTT(String topic, unsigned long payload) {
  char val[11];
  sprintf(val, "%lu", payload);
  return pubMQTT(topic.c_str(), val);
}

void delayWithOTA(long waitMillis) {
  long waitStep = 100;
  for (long waitedMillis = 0; waitedMillis < waitMillis; waitedMillis += waitStep) {
#ifndef ESPWifiManualSetup
    checkButton(); // check if a reset of wifi/mqtt settings is asked
#endif
    ArduinoOTA.handle();
#if defined(ZwebUI) && defined(ESP32)
    WebUILoop();
#endif
#ifdef ESP32
    //esp_task_wdt_reset();
#endif
    delay(waitStep);
  }
}

void SYSConfig_init() {
  SYSConfig.mqtt = DEFAULT_MQTT;
  SYSConfig.serial = DEFAULT_SERIAL;
  SYSConfig.offline = DEFAULT_OFFLINE;
#if USE_BLUFI
  SYSConfig.blufi = DEFAULT_BLUFI;
#endif
#ifdef ZmqttDiscovery
  SYSConfig.discovery = DEFAULT_DISCOVERY;
  SYSConfig.ohdiscovery = OpenHABDiscovery;
#endif
#ifdef LED_ADDRESSABLE
  SYSConfig.rgbbrightness = DEFAULT_ADJ_BRIGHTNESS;
#endif
  SYSConfig.powerMode = DEFAULT_LOW_POWER_MODE;
}

void SYSConfig_fromJson(JsonObject& SYSdata) {
  Config_update(SYSdata, "mqtt", SYSConfig.mqtt);
  Config_update(SYSdata, "serial", SYSConfig.serial);
  Config_update(SYSdata, "offline", SYSConfig.offline);
#if USE_BLUFI
  Config_update(SYSdata, "blufi", SYSConfig.blufi);
#endif
#ifdef ZmqttDiscovery
  Config_update(SYSdata, "disc", SYSConfig.discovery);
  Config_update(SYSdata, "ohdisc", SYSConfig.ohdiscovery);
#endif
#ifdef LED_ADDRESSABLE
  Config_update(SYSdata, "rgbb", SYSConfig.rgbbrightness);
#endif
  Config_update(SYSdata, "powermode", SYSConfig.powerMode);
}

#ifdef ESP32
void SYSConfig_save() {
  StaticJsonDocument<JSON_MSG_BUFFER> jsonBuffer;
  JsonObject SYSdata = jsonBuffer.to<JsonObject>();
  SYSdata["mqtt"] = SYSConfig.mqtt;
  SYSdata["serial"] = SYSConfig.serial;
  SYSdata["offline"] = SYSConfig.offline;
  SYSdata["powermode"] = SYSConfig.powerMode;
#  if USE_BLUFI
  SYSdata["blufi"] = SYSConfig.blufi;
#  endif
#  ifdef ZmqttDiscovery
  SYSdata["disc"] = SYSConfig.discovery;
  SYSdata["ohdisc"] = SYSConfig.ohdiscovery;
#  endif
#  ifdef LED_ADDRESSABLE
  SYSdata["rgbb"] = SYSConfig.rgbbrightness;
#  endif
  String conf = "";
  serializeJson(jsonBuffer, conf);
  preferences.begin(Gateway_Short_Name, false);
  int result = preferences.putString("SYSConfig", conf);
  preferences.end();
  Log.notice(F("SYS Config_save: %s, result: %d" CR), conf.c_str(), result);
}
#else // Function not available for ESP8266
void SYSConfig_save() {}
#endif

bool cmpToMainTopic(const char* topicOri, const char* toAdd) {
  if (strcmp(topicOri, toAdd) == 0)
    return true;
  // Is string "<mqtt_topic><gateway_name><toAdd>" equal to "<topicOri>"?
  // Compare first part with first chunk
  if (strncmp(topicOri, mqtt_topic, strlen(mqtt_topic)) != 0)
    return false;
  // Move pointer of sizeof chunk
  topicOri += strlen(mqtt_topic);
  // And so on...
  if (strncmp(topicOri, gateway_name, strlen(gateway_name)) != 0)
    return false;
  topicOri += strlen(gateway_name);
  if (strncmp(topicOri, toAdd, strlen(toAdd)) != 0)
    return false;
  return true;
}

#if defined(ESP32)
void SYSConfig_load() {
  StaticJsonDocument<JSON_MSG_BUFFER> jsonBuffer;
  preferences.begin(Gateway_Short_Name, true);
  if (preferences.isKey("SYSConfig")) {
    auto error = deserializeJson(jsonBuffer, preferences.getString("SYSConfig", "{}"));
    preferences.end();
    if (error) {
      Log.error(F("SYS config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity());
      gatewayState = GatewayState::ERROR;
      return;
    }
    if (jsonBuffer.isNull()) {
      Log.warning(F("SYS config is null" CR));
      return;
    }
    JsonObject jo = jsonBuffer.as<JsonObject>();
    SYSConfig_fromJson(jo);
    Log.notice(F("SYS config loaded" CR));
  } else {
    preferences.end();
    Log.notice(F("SYS config not found" CR));
  }
}
#else // Function not available for ESP8266
void SYSConfig_load() {}
#endif

#if defined(MDNS_SD)
std::pair<String, uint16_t> discoverMQTTbroker() {
  Log.trace(F("Browsing for MQTT service" CR));
  int n = MDNS.queryService("mqtt", "tcp");
  if (n == 0) {
    Log.warning(F("no services found" CR));
  } else {
    Log.trace(F("%d service(s) found" CR), n);
    for (int i = 0; i < n; ++i) {
      Log.trace(F("Service %d %s found" CR), i, MDNS.hostname(i).c_str());
      Log.trace(F("IP %s Port %d" CR), MDNS.IP(i).toString().c_str(), MDNS.port(i));
    }
    if (n == 1) {
      Log.trace(F("One MQTT server found setting parameters" CR));
      return {MDNS.IP(0).toString(), uint16_t(MDNS.port(0))};
    } else {
      Log.warning(F("Several MQTT servers found, please deactivate mDNS and set your default server" CR));
    }
  }
  return {"", 0};
}
#endif

#if MQTT_BROKER_MODE
void setupMQTT() {
  Log.notice(F("Reconfiguring MQTT broker..." CR));

  mqtt.reset(new MQTTServer());
  mqtt->begin();
#  ifdef ZgatewayBT
  BTProcessLock = !BTConfig.enabled; // Release BLE processes at start if enabled
#  endif
}
#else

struct ss_cnt_parameters_backup {
  ss_cnt_parameters parameters;
  int cnt_index;
  bool saveOnSuccess;
};

static std::unique_ptr<ss_cnt_parameters_backup> cnt_parameters_backup;

void setupMQTT() {
  Log.notice(F("Reconfiguring MQTT client..." CR));

  const auto& parameters = cnt_parameters_array[cnt_index];

  // free the old MQTT client and underlying connection
  mqtt.reset();
  eClient.reset();
  failure_number_mqtt = 0;

  // configure socket
  if (parameters.isConnectionSecure) {
    eClient.reset(new WiFiClientSecure);
    if (parameters.isCertValidate) {
      setupTLS(cnt_index);
    } else {
      static_cast<WiFiClientSecure*>(eClient.get())->setInsecure();
    }
  } else {
    eClient.reset(new WiFiClient);
  }

#  if defined(MDNS_SD)
  Log.trace(F("Connecting to MQTT by mDNS without MQTT hostname" CR));
  const auto discovered_broker = discoverMQTTbroker();
  const auto broker_host = discovered_broker.first.c_str();
  const auto broker_port = discovered_broker.second;
#  else
  const auto broker_host = parameters.mqtt_server;
  const auto broker_port = String(parameters.mqtt_port).toInt();
#  endif

  Log.trace(F("Mqtt server: %s" CR), broker_host);
  Log.trace(F("Port: %u" CR), broker_port);

  mqtt.reset(new PicoMQTT::Client(*eClient, broker_host, broker_port, gateway_name,
                                  parameters.mqtt_user, parameters.mqtt_pass,
                                  0, // minimum reconnect attempt interval [ms]
                                  60 * 1000, // keep alive interval [ms]
                                  (GeneralTimeOut - 1) * 1000 // socket timeout [ms]
                                  ));

#  if AWS_IOT
  // AWS doesn't support will topic for the moment
  mqtt->will.topic = "";
  mqtt->will.payload = "";
  mqtt->will.qos = 0;
  mqtt->will.retain = false;
#  else
  mqtt->will.topic = String(mqtt_topic) + gateway_name + will_Topic;
  mqtt->will.payload = will_Message;
  mqtt->will.qos = will_QoS;
  mqtt->will.retain = will_Retain;
#  endif

  mqtt->connected_callback = [] {
#  if AWS_IOT
    {
      // Define a threshold for instability detection
      constexpr unsigned int reconnection_threshold = 5;
      constexpr unsigned long reconnection_window_millis = 120000; // Consider unstable if more than 5 reconnections in 2 minutes

      static unsigned int reconnection_count = 0;
      static unsigned long start_reconnection_window_millis = 0;

      const unsigned long current_millis = millis();
      if (start_reconnection_window_millis == 0 || current_millis - start_reconnection_window_millis > reconnection_window_millis) {
        // Reset the count and timestamp at the start of a new window
        reconnection_count = 0;
        start_reconnection_window_millis = current_millis;
      }
      reconnection_count++; // Increment reconnection count
      Log.trace(F("MQTT connection count: %d" CR), reconnection_count);
      if (reconnection_count > reconnection_threshold && SYSConfig.mqtt) {
        // Detected instability in MQTT connection
        Log.warning(F("MQTT connection instability detected: %d reconnections in the last %d seconds" CR), reconnection_count, reconnection_window_millis / 1000);
        // Stop xtoMQTT to see if it helps and still enables to receive data
        SYSConfig.mqtt = false;
      }
    }
#  endif
#  ifdef ZgatewayBT
    BTProcessLock = !BTConfig.enabled; // Release BLE processes at start if enabled
#  endif
    ProcessLock = false; // Release the loop process
    displayPrint("MQTT connected");
    Log.notice(F("Connected to broker" CR));
    gatewayState = GatewayState::BROKER_CONNECTED;
    failure_number_mqtt = 0;
    // Once connected, publish an announcement...
    pub(will_Topic, Gateway_AnnouncementMsg, will_Retain);

    if (cnt_parameters_backup) {
      // this was the first attempt to connect to a new server and it succeeded
      Log.notice(F("MQTT connection parameters %d successful" CR), cnt_index);
      cnt_parameters_array[cnt_index].validConnection = true;
      readCntParameters(cnt_index);

#  ifndef ESPWifiManualSetup
      if (cnt_parameters_backup->saveOnSuccess) // Save the new parameters to the flash
        saveConfig();
#  endif

      cnt_parameters_backup.reset();
      ESPRestart(7);
    }
    handle_autodiscovery();
  };

  mqtt->connection_failure_callback = [] {
    if ((WiFi.status() != WL_CONNECTED && ethConnected == false) || SYSConfig.offline) {
      // No network connection or offline, ignore this failure
      return;
    }
    failure_number_mqtt++; // we count the failure
    gatewayState = GatewayState::BROKER_DISCONNECTED;
    Log.warning(F("failure_number_mqtt: %d" CR), failure_number_mqtt);

    const auto& parameters = cnt_parameters_array[cnt_index];

    if (parameters.isConnectionSecure) {
      WiFiClientSecure* client = static_cast<WiFiClientSecure*>(eClient.get());
#  if defined(ESP32)
      Log.warning(F("failed, ssl error code=%d" CR), client->lastError(nullptr, 0));
#  elif defined(ESP8266)
      Log.warning(F("failed, ssl error code=%d" CR), client->getLastSSLError());
#  endif
    }

    if (cnt_parameters_backup) {
      // this was the first attempt to connect to a new server and it failed, revert to old settings
      Log.error(F("MQTT connection failed, reverting to previous settings" CR));
      gatewayState = GatewayState::ERROR;
      cnt_parameters_array[cnt_index] = cnt_parameters_backup->parameters;
      cnt_index = cnt_parameters_backup->cnt_index;
      mqttSetupPending = true;
      cnt_parameters_backup.reset();
      ESPRestart(7);
      return;
    }

    delayWithOTA(10000);

    if (failure_number_mqtt > maxRetryWatchDog) {
#  ifndef ESPWifiManualSetup
      // Look for the next valid connection
      for (int i = 0; i < cnt_parameters_array_size; i++) {
        cnt_index++;
        if (cnt_index >= cnt_parameters_array_size) {
          cnt_index = 0;
        }
        if (cnt_parameters_array[cnt_index].validConnection) {
          Log.notice(F("Connection %d valid, switching" CR), cnt_index);
          saveConfig();
          break;
        } else {
          Log.notice(F("Connection %d not valid" CR), cnt_index);
        }
      }
#  endif
      unsigned long millis_since_last_ota;
      while (
          // When
          // ...incomplete OTA in progress
          (last_ota_activity_millis != 0)
          // ...AND last OTA activity fairly recently
          && ((millis_since_last_ota = millis() - last_ota_activity_millis) < ota_timeout_millis)) {
        // ... We consider that OTA might be still active, and we sleep for a while, and giving
        // OTA chance to proceed (ArduinoOTA.handle())
        Log.warning(F("OTA might be still active (activity %d ms ago)" CR), millis_since_last_ota);
        ArduinoOTA.handle();
        delay(100);
      }
      ESPRestart(1);
    }
  };

  mqtt->subscribe(String(mqtt_topic) + gateway_name + subjectMQTTtoX, receivingDATA, mqtt_max_payload_size);

#  ifdef ZgatewayRF
  // subject on which other OMG will publish, this OMG will store these msg and by the way don't republish them if they have been already published
  mqtt->subscribe(subjectMultiGTWRF, receivingDATA, mqtt_max_payload_size);
#  endif

#  ifdef ZgatewayIR
  // subject on which other OMG will publish, this OMG will store these msg and by the way don't republish them if they have been already published
  mqtt->subscribe(subjectMultiGTWIR, receivingDATA, mqtt_max_payload_size);
#  endif

#  if enableMultiGTWSync
  mqtt->subscribe(String(mqtt_topic) + subjectTrackerSync, receivingDATA, mqtt_max_payload_size);
#  endif

  mqtt->begin();
}
#endif

#if defined(ESP32) && (defined(WifiGMode) || defined(WifiPower))
void setESPWifiProtocolTxPower() {
  //Reduce WiFi interference when using ESP32 using custom WiFi mode and tx power
  //https://github.com/espressif/arduino-esp32/search?q=WIFI_PROTOCOL_11G
  //https://www.letscontrolit.com/forum/viewtopic.php?t=671&start=20
#  if WifiGMode == true
  if (esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G) != ESP_OK) {
    Log.error(F("Failed to change WifiMode." CR));
    gatewayState = GatewayState::ERROR;
  }
#  endif

#  if WifiGMode == false
  if (esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N) != ESP_OK) {
    Log.error(F("Failed to change WifiMode." CR));
    gatewayState = GatewayState::ERROR;
  }
#  endif

  uint8_t getprotocol;
  esp_err_t err;
  err = esp_wifi_get_protocol(WIFI_IF_STA, &getprotocol);

  if (err != ESP_OK) {
    Log.notice(F("Could not get protocol!" CR));
  }
  if (getprotocol & WIFI_PROTOCOL_11N) {
    Log.notice(F("WiFi_Protocol_11n" CR));
  }
  if (getprotocol & WIFI_PROTOCOL_11G) {
    Log.notice(F("WiFi_Protocol_11g" CR));
  }
  if (getprotocol & WIFI_PROTOCOL_11B) {
    Log.notice(F("WiFi_Protocol_11b" CR));
  }

#  ifdef WifiPower
  Log.notice(F("Requested WiFi power level: %i" CR), WifiPower);
  WiFi.setTxPower(WifiPower);
#  endif
  Log.notice(F("Operating WiFi power level: %i" CR), WiFi.getTxPower());
}
#endif

#if defined(ESP8266) && (defined(WifiGMode) || defined(WifiPower))
void setESPWifiProtocolTxPower() {
#  if WifiGMode == true
  if (!wifi_set_phy_mode(PHY_MODE_11G)) {
    Log.error(F("Failed to change WifiMode." CR));
    gatewayState = GatewayState::ERROR;
  }
#  endif

#  if WifiGMode == false
  if (!wifi_set_phy_mode(PHY_MODE_11N)) {
    Log.error(F("Failed to change WifiMode." CR));
    gatewayState = GatewayState::ERROR;
  }
#  endif

  phy_mode_t getprotocol = wifi_get_phy_mode();
  if (getprotocol == PHY_MODE_11N) {
    Log.notice(F("WiFi_Protocol_11n" CR));
  }
  if (getprotocol == PHY_MODE_11G) {
    Log.notice(F("WiFi_Protocol_11g" CR));
  }
  if (getprotocol == PHY_MODE_11B) {
    Log.notice(F("WiFi_Protocol_11b" CR));
  }

#  ifdef WifiPower
  Log.notice(F("Requested WiFi power level: %i dBm" CR), WifiPower);

  int i_dBm = int(WifiPower * 4.0f);

  // i_dBm 82 == 20.5 dBm
  if (i_dBm > 82) {
    i_dBm = 82;
  } else if (i_dBm < 0) {
    i_dBm = 0;
  }

  system_phy_set_max_tpw((uint8_t)i_dBm);
#  endif
}
#endif

#ifdef ESP32
void updateAndHandleLEDsTask(void* pvParameters) {
  for (;;) {
    updateAndHandleLEDsTask();
    vTaskDelay(pdMS_TO_TICKS(100));
  }
}
#endif

void updateAndHandleLEDsTask() {
  static GatewayState previousGatewayState;
  if (previousGatewayState != gatewayState) {
#ifdef LED_POWER
    ledManager.setMode(STRIP_POWER, LED_POWER, LEDManager::STATIC, LED_POWER_COLOR, -1);
#endif
    switch (gatewayState) {
      case PROCESSING:
#ifdef LED_PROCESSING
        ledManager.setMode(STRIP_PROCESSING, LED_PROCESSING, LEDManager::BLINK, LED_PROCESSING_COLOR, 3);
#endif
        break;
      case WAITING_ONBOARDING:
#ifdef LED_BROKER
        ledManager.setMode(STRIP_BROKER, LED_BROKER, LEDManager::STATIC, LED_WAITING_ONBOARD_COLOR, -1);
#endif
        break;
      case ONBOARDING:
#ifdef LED_BROKER
        ledManager.setMode(STRIP_BROKER, LED_BROKER, LEDManager::STATIC, LED_ONBOARD_COLOR, -1);
#endif
        break;
      case NTWK_CONNECTED:
#ifdef LED_NETWORK
        ledManager.setMode(STRIP_NETWORK, LED_NETWORK, LEDManager::STATIC, LED_NETWORK_OK_COLOR, -1);
#endif
        break;
      case BROKER_CONNECTED:
#ifdef LED_BROKER
        ledManager.setMode(STRIP_BROKER, LED_BROKER, LEDManager::STATIC, LED_BROKER_OK_COLOR, -1);
#endif
        break;
      case NTWK_DISCONNECTED:
#ifdef LED_NETWORK
        ledManager.setMode(STRIP_NETWORK, LED_NETWORK, LEDManager::BLINK, LED_NETWORK_ERROR_COLOR, -1);
#endif
        break;
      case BROKER_DISCONNECTED:
#ifdef LED_BROKER
        ledManager.setMode(STRIP_BROKER, LED_BROKER, LEDManager::BLINK, LED_BROKER_ERROR_COLOR, -1);
#endif
        break;
      case SLEEPING:
        ledManager.setMode(-1, -1, LEDManager::OFF, 0, -1);
        break;
      case OFFLINE:
#ifdef LED_NETWORK
        ledManager.setMode(STRIP_NETWORK, LED_NETWORK, LEDManager::BLINK, LED_OFFLINE_COLOR, -1);
#endif
        break;
      case LOCAL_OTA_IN_PROGRESS:
#ifdef LED_PROCESSING
        ledManager.setMode(STRIP_PROCESSING, LED_PROCESSING, LEDManager::BLINK, LED_OTA_LOCAL_COLOR, -1);
#endif
        break;
      case REMOTE_OTA_IN_PROGRESS:
#ifdef LED_PROCESSING
        ledManager.setMode(STRIP_PROCESSING, LED_PROCESSING, LEDManager::BLINK, LED_OTA_REMOTE_COLOR, -1);
#endif
        break;
      case ERROR:
#ifdef LED_ERROR
        ledManager.setMode(STRIP_ERROR, LED_ERROR, LEDManager::BLINK, LED_ERROR_COLOR, 3);
#endif
        break;
      default:
        break;
    }
    previousGatewayState = gatewayState;
  }
  ledManager.update();
}

void setup() {
  //Launch serial for debugging purposes
  Serial.begin(SERIAL_BAUD);
  Log.begin(LOG_LEVEL, &Serial);
  Log.notice(F(CR "************* WELCOME TO OpenMQTTGateway **************" CR));
#if defined(TRIGGER_GPIO) && !defined(ESPWifiManualSetup)
  pinMode(TRIGGER_GPIO, INPUT_PULLUP);
  checkButton();
#endif

  delay(100); //give time to start the flash and avoid issue when reading the preferences

  SYSConfig_init();
  SYSConfig_load();

  if (SYSConfig.offline) {
    gatewayState = GatewayState::OFFLINE;
  }

#ifdef LED_ADDRESSABLE
#  ifdef LED_ADDRESSABLE_PIN1
  ledManager.addLEDStrip(LED_ADDRESSABLE_PIN1, LED_ADDRESSABLE_NUM);
#  endif
#  ifdef LED_ADDRESSABLE_PIN2
  ledManager.addLEDStrip(LED_ADDRESSABLE_PIN2, LED_ADDRESSABLE_NUM);
#  endif
#  ifdef LED_ADDRESSABLE_PIN3
  ledManager.addLEDStrip(LED_ADDRESSABLE_PIN3, LED_ADDRESSABLE_NUM);
#  endif
  ledManager.setBrightness(SYSConfig.rgbbrightness);
#elif LED_PIN
  ledManager.addLEDStrip(LED_PIN, 1);
#endif

#ifdef ESP8266
#  ifndef ZgatewaySRFB // if we are not in sonoff rf bridge case we apply the ESP8266 GPIO optimization
  Serial.end();
  Serial.begin(SERIAL_BAUD, SERIAL_8N1, SERIAL_TX_ONLY); // enable on ESP8266 to free some pin
#  endif
#elif ESP32
  xTaskCreate(updateAndHandleLEDsTask, "updateAndHandleLEDsTask", 2500, NULL, 1, NULL);
  xQueueMutex = xSemaphoreCreateMutex();
  xMqttMutex = xSemaphoreCreateMutex();
#  if defined(ZboardM5STICKC) || defined(ZboardM5STICKCP) || defined(ZboardM5STACK) || defined(ZboardM5TOUGH)
  setupM5();
#  endif
#  if defined(ZdisplaySSD1306)
  setupSSD1306();
  modules.add(ZdisplaySSD1306);
#  endif
#endif

  Log.notice(F("OpenMQTTGateway Version: " OMG_VERSION CR));

#ifdef ESP32_EXT0_WAKE_PIN
  Log.notice(F("Setting EXT0 Wakeup for deep sleep." CR));
  gpio_num_t wake_pin0 = static_cast<gpio_num_t>(ESP32_EXT0_WAKE_PIN);
  if (esp_sleep_enable_ext0_wakeup(wake_pin0, ESP32_EXT0_WAKE_PIN_STATE) != ESP_OK) {
    Log.error(F("Failed to set deep sleep EXT0 Wakeup." CR));
    gatewayState = GatewayState::ERROR;
  }
#endif
#ifdef ESP32_EXT1_WAKE_PIN
  Log.notice(F("Setting EXT1 Wakeup for deep sleep." CR));
  uint64_t wake_pin_bitmask = 1ULL << ESP32_EXT1_WAKE_PIN; // Adjust this line if multiple pins are used.
  esp_sleep_ext1_wakeup_mode_t wake_state1 = static_cast<esp_sleep_ext1_wakeup_mode_t>(ESP32_EXT1_WAKE_PIN_STATE);
  if (esp_sleep_enable_ext1_wakeup(wake_pin_bitmask, wake_state1) != ESP_OK) {
    Log.error(F("Failed to set deep sleep EXT1 Wakeup." CR));
    gatewayState = GatewayState::ERROR;
  }
#endif
/*
 Note that the ONOFF module need to start after the RN8209 so that the overCurrent function is launched after the setup of the sensor
*/
#ifdef ZsensorRN8209
  setupRN8209();
  modules.add(ZsensorRN8209);
#endif
#ifdef ZactuatorONOFF
  setupONOFF();
  modules.add(ZactuatorONOFF);
#endif
#ifdef ZgatewaySERIAL
  setupSERIAL();
  modules.add(ZgatewaySERIAL);
#endif

#if defined(ESP32) && defined(USE_BLUFI)
  if (SYSConfig.blufi)
    startBlufi();
#endif

#if defined(ESPWifiManualSetup)
  setupWiFiFromBuild();
#else
  if (loadConfigFromFlash()) { // Config present
    Log.notice(F("Config loaded from flash" CR));
#  ifdef ESP32_ETHERNET
    setup_ethernet_esp32();
#  endif
    // If not in failSafeMode and no connection to the network with Ethernet, launch the wifi manager
    if (!failSafeMode && !ethConnected) setupWiFiManager();
  } else { // No config in flash
#  ifdef ESP32_ETHERNET
    setup_ethernet_esp32();
#  endif

#  if SELF_TEST
    // Check serial input to trigger a Self Test sequence if required
    checkSerial();
#  endif

    Log.notice(F("No config in flash, launching wifi manager" CR));
    // In failSafeMode we don't want to setup wifi manager as it has already been done before
    if (!failSafeMode) setupWiFiManager();
  }

#endif
  Log.trace(F("OpenMQTTGateway mac: %s" CR), WiFi.macAddress().c_str());
  Log.trace(F("OpenMQTTGateway ip: %s" CR), WiFi.localIP().toString().c_str());
  Log.trace(F("OpenMQTTGateway index %d" CR), cnt_index);
  Log.trace(F("OpenMQTTGateway mqtt topic: %s" CR), mqtt_topic);
#ifdef ZmqttDiscovery
  Log.trace(F("OpenMQTTGateway mqtt discovery prefix: %s" CR), discovery_prefix);
#endif
  Log.trace(F("OpenMQTTGateway gateway name: %s" CR), gateway_name);
#if !MQTT_BROKER_MODE
  Log.trace(F("OpenMQTTGateway mqtt server: %s" CR), cnt_parameters_array[cnt_index].mqtt_server);
  Log.trace(F("OpenMQTTGateway mqtt port: %s" CR), cnt_parameters_array[cnt_index].mqtt_port);
  Log.trace(F("OpenMQTTGateway mqtt user: %s" CR), cnt_parameters_array[cnt_index].mqtt_user);
  Log.trace(F("OpenMQTTGateway secure connection: %s" CR), cnt_parameters_array[cnt_index].isConnectionSecure ? "true" : "false");
  Log.trace(F("OpenMQTTGateway validate cert: %s" CR), cnt_parameters_array[cnt_index].isCertValidate ? "true" : "false");
#endif

  setOTA();

#if defined(ZwebUI) && defined(ESP32)
  WebUISetup();
  modules.add(ZwebUI);
#endif

  delay(1500);
#if defined(ZgatewayRF) || defined(ZgatewayPilight) || defined(ZgatewayRTL_433) || defined(ZgatewayRF2) || defined(ZactuatorSomfy)
  setupCommonRF();
#endif
#ifdef ZsensorBME280
  setupZsensorBME280();
  modules.add(ZsensorBME280);
#endif
#ifdef ZsensorHTU21
  setupZsensorHTU21();
  modules.add(ZsensorHTU21);
#endif
#ifdef ZsensorLM75
  setupZsensorLM75();
  modules.add(ZsensorLM75);
#endif
#ifdef ZsensorAHTx0
  setupZsensorAHTx0();
  modules.add(ZsensorAHTx0);
#endif
#ifdef ZsensorBH1750
  setupZsensorBH1750();
  modules.add(ZsensorBH1750);
#endif
#ifdef ZsensorMQ2
  setupZsensorMQ2();
  modules.add(ZsensorMQ2);
#endif
#ifdef ZsensorTEMT6000
  setupZsensorTEMT6000();
  modules.add(ZsensorTEMT6000);
#endif
#ifdef ZsensorTSL2561
  setupZsensorTSL2561();
  modules.add(ZsensorTSL2561);
#endif
#ifdef Zgateway2G
  setup2G();
  modules.add(Zgateway2G);
#endif
#ifdef ZgatewayIR
  setupIR();
  modules.add(ZgatewayIR);
#endif
#ifdef ZgatewayLORA
  setupLORA();
  modules.add(ZgatewayLORA);
#endif
#ifdef ZgatewayRF
  modules.add(ZgatewayRF);
#  define ACTIVE_RECEIVER ACTIVE_RF
#endif
#ifdef ZgatewayRF2
  modules.add(ZgatewayRF2);
#  ifdef ACTIVE_RECEIVER
#    undef ACTIVE_RECEIVER
#  endif
#  define ACTIVE_RECEIVER ACTIVE_RF2
#endif
#ifdef ZgatewayPilight
  modules.add(ZgatewayPilight);
#  ifdef ACTIVE_RECEIVER
#    undef ACTIVE_RECEIVER
#  endif
#  define ACTIVE_RECEIVER ACTIVE_PILIGHT
#endif
#ifdef ZgatewayWeatherStation
  setupWeatherStation();
  modules.add(ZgatewayWeatherStation);
#endif
#ifdef ZgatewayGFSunInverter
  setupGFSunInverter();
  modules.add(ZgatewayGFSunInverter);
#endif
#ifdef ZgatewaySRFB
  setupSRFB();
  modules.add(ZgatewaySRFB);
#endif
#ifdef ZgatewayBT
  setupBT();
  modules.add(ZgatewayBT);
#endif
#ifdef ZgatewayRFM69
  setupRFM69();
  modules.add(ZgatewayRFM69);
#endif
#ifdef ZsensorINA226
  setupINA226();
  modules.add(ZsensorINA226);
#endif
#ifdef ZsensorHCSR501
  setupHCSR501();
  modules.add(ZsensorHCSR501);
#endif
#ifdef ZsensorHCSR04
  setupHCSR04();
  modules.add(ZsensorHCSR04);
#endif
#ifdef ZsensorGPIOInput
  setupGPIOInput();
  modules.add(ZsensorGPIOInput);
#endif
#ifdef ZsensorGPIOKeyCode
  setupGPIOKeyCode();
  modules.add(ZsensorGPIOKeyCode);
#endif
#ifdef ZactuatorFASTLED
  setupFASTLED();
  modules.add(ZactuatorFASTLED);
#endif
#ifdef ZactuatorPWM
  setupPWM();
  modules.add(ZactuatorPWM);
#endif
#ifdef ZactuatorSomfy
#  ifdef ACTIVE_RECEIVER
#    undef ACTIVE_RECEIVER
#  endif
#  define ACTIVE_RECEIVER ACTIVE_NONE
  setupSomfy();
  modules.add(ZactuatorSomfy);
#endif
#ifdef ZsensorDS1820
  setupZsensorDS1820();
  modules.add(ZsensorDS1820);
#endif
#ifdef ZsensorADC
  setupADC();
  modules.add(ZsensorADC);
#endif
#ifdef ZsensorTouch
  setupTouch();
  modules.add(ZsensorTouch);
#endif
#ifdef ZsensorC37_YL83_HMRD
  setupZsensorC37_YL83_HMRD();
  modules.add(ZsensorC37_YL83_HMRD);
#endif
#ifdef ZsensorDHT
  setupDHT();
  modules.add(ZsensorDHT);
#endif
#ifdef ZsensorSHTC3
  setupSHTC3();
#endif
#ifdef ZgatewayRTL_433
#  ifdef ACTIVE_RECEIVER
#    undef ACTIVE_RECEIVER
#  endif
#  define ACTIVE_RECEIVER ACTIVE_RTL
  setupRTL_433();
  modules.add(ZgatewayRTL_433);
#endif
  Log.trace(F("mqtt_max_payload_size: %d" CR), mqtt_max_payload_size);
  SYSConfig.offline ? Log.notice(F("Offline enabled" CR)) : Log.notice(F("Offline disabled" CR));
  char jsonChar[100];
  serializeJson(modules, jsonChar, measureJson(modules) + 1);
  Log.notice(F("OpenMQTTGateway modules: %s" CR), jsonChar);
  Log.notice(F("************** Setup OpenMQTTGateway end **************" CR));
}

// Bypass for ESP not reconnecting automaticaly the second time https://github.com/espressif/arduino-esp32/issues/2501
bool wifi_reconnect_bypass() {
#if defined(ESP32) && defined(USE_BLUFI)
  extern bool omg_blufi_ble_connected;
  if (omg_blufi_ble_connected) {
    Log.notice(F("BLUFI is connected, bypassing wifi reconnect" CR));
    gatewayState = GatewayState::ONBOARDING;
    return true;
  }
#endif
  uint8_t wifi_autoreconnect_cnt = 0;
#ifdef ESP32
  while (WiFi.status() != WL_CONNECTED && wifi_autoreconnect_cnt < maxConnectionRetryNetwork) {
#else
  while (WiFi.waitForConnectResult() != WL_CONNECTED && wifi_autoreconnect_cnt < maxConnectionRetryNetwork) {
#endif
    Log.notice(F("Attempting Wifi connection with saved AP: %d" CR), wifi_autoreconnect_cnt);

    WiFi.begin();
#if defined(WifiGMode) || defined(WifiPower)
    setESPWifiProtocolTxPower();
#endif
    delay(1000);
    wifi_autoreconnect_cnt++;
  }
  if (wifi_autoreconnect_cnt < maxConnectionRetryNetwork) {
    return true;
  } else {
    return false;
  }
}

void setOTA() {
  // Port defaults to 8266
  ArduinoOTA.setPort(ota_port);

  // Hostname defaults to esp8266-[ChipID]
  ArduinoOTA.setHostname(ota_hostname);

  // No authentication by default
  ArduinoOTA.setPassword(ota_pass);

  ArduinoOTA.onStart([]() {
    Log.trace(F("Start OTA, lock other functions" CR));
    last_ota_activity_millis = millis();
#ifdef ESP32
    ProcessLock = true;
#  ifdef ZgatewayBT
    stopProcessing(true);
#  endif
#endif
    lpDisplayPrint("OTA in progress");
  });
  ArduinoOTA.onEnd([]() {
    Log.trace(F("\nOTA done" CR));
    last_ota_activity_millis = 0;
    lpDisplayPrint("OTA done");
    ESPRestart(6);
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Log.trace(F("Progress: %u%%\r" CR), (progress / (total / 100)));
    gatewayState = GatewayState::LOCAL_OTA_IN_PROGRESS;
    last_ota_activity_millis = millis();
  });
  ArduinoOTA.onError([](ota_error_t error) {
    last_ota_activity_millis = millis();
    Serial.printf("Error[%u]: ", error);
    gatewayState = GatewayState::ERROR;
    if (error == OTA_AUTH_ERROR)
      Log.error(F("Auth Failed" CR));
    else if (error == OTA_BEGIN_ERROR)
      Log.error(F("Begin Failed" CR));
    else if (error == OTA_CONNECT_ERROR)
      Log.error(F("Connect Failed" CR));
    else if (error == OTA_RECEIVE_ERROR)
      Log.error(F("Receive Failed" CR));
    else if (error == OTA_END_ERROR)
      Log.error(F("End Failed" CR));
    ESPRestart(6);
  });
  ArduinoOTA.begin();
}

#if !MQTT_BROKER_MODE
void setupTLS(int index) {
  configTime(0, 0, NTP_SERVER);
  WiFiClientSecure* sClient = static_cast<WiFiClientSecure*>(eClient.get());
  Log.notice(F("cnt index used: %d" CR), index);
  if (!cnt_parameters_array[index].isCertValidate) {
    Log.notice(F("Disabling cert validation" CR));
    sClient->setInsecure();
  } else {
    Log.notice(F("Enabling cert validation" CR));
#  if defined(ESP32)
    if (cnt_parameters_array[index].server_cert.length() > MIN_CERT_LENGTH) {
      sClient->setCACert(cnt_parameters_array[index].server_cert.c_str());
      Log.notice(F("Server cert found from cert array" CR));
    } else if (strlen(ss_server_cert) > MIN_CERT_LENGTH) {
      sClient->setCACert(ss_server_cert);
      Log.notice(F("Server cert found from ss_server_cert" CR));
    } else {
      Log.error(F("No server cert found" CR));
      gatewayState = GatewayState::ERROR;
    }

#    if AWS_IOT
    if (strcmp(cnt_parameters_array[index].mqtt_port, "443") == 0) {
      Log.notice(F("Using ALPN" CR));
      sClient->setAlpnProtocols(alpnProtocols);
    }
#    endif
#    if MQTT_SECURE_SIGNED_CLIENT
    if (cnt_parameters_array[index].client_cert.length() > MIN_CERT_LENGTH) {
      sClient->setCertificate(cnt_parameters_array[index].client_cert.c_str());
      Log.notice(F("Client cert found from cert array" CR));
    } else if (strlen(ss_client_cert) > MIN_CERT_LENGTH) {
      sClient->setCertificate(ss_client_cert);
      Log.notice(F("Client cert found from ss_client_cert" CR));
    } else {
      Log.error(F("No client cert found" CR));
      gatewayState = GatewayState::ERROR;
    }
    if (cnt_parameters_array[index].client_key.length() > MIN_CERT_LENGTH) {
      sClient->setPrivateKey(cnt_parameters_array[index].client_key.c_str());
      Log.notice(F("Client key found from cert array" CR));
    } else if (strlen(ss_client_key) > MIN_CERT_LENGTH) {
      sClient->setPrivateKey(ss_client_key);
      Log.notice(F("Client key found from ss_client_key" CR));
    } else {
      Log.error(F("No client key found" CR));
      gatewayState = GatewayState::ERROR;
    }
#    endif
#  elif defined(ESP8266)
    caCert.append(cnt_parameters_array[index].server_cert.c_str());
    sClient->setTrustAnchors(&caCert);
    sClient->setBufferSizes(512, 512);
#    if MQTT_SECURE_SIGNED_CLIENT
    if (pClCert != nullptr) {
      delete pClCert;
    }
    if (pClKey != nullptr) {
      delete pClKey;
    }
    pClCert = new X509List(cnt_parameters_array[index].client_cert.c_str());
    pClKey = new PrivateKey(cnt_parameters_array[index].client_key.c_str());
    sClient->setClientRSACert(pClCert, pClKey);
#    endif
#  endif
  }
}
#endif

/*
  Reboot for Reason Codes
  0 - Erase and Restart
  1 - Repeated MQTT Connection Failure
  2 - Repeated WiFi Connection Failure
  3 - Failed WiFiManager configuration portal
  4 - BLE Scan watchdog
  5 - User requested reboot
  6 - OTA Update
  7 - Parameters changed
  8 - not enough memory to pursue
  9 - SELFTEST end
*/
void ESPRestart(byte reason) {
#ifdef SecondaryModule
  // Erase the secondary module config
  String restartCmdStr = "{\"cmd\":\"" + String(restartCmd) + "\"}";
  Log.notice(F("Restarting secondary module : %s" CR), restartCmdStr.c_str());
  receivingDATA(subjectMQTTtoSYSsetSecondaryModule, restartCmdStr.c_str());
  delay(2000);
#endif
  StaticJsonDocument<128> jsonBuffer;
  JsonObject jsondata = jsonBuffer.to<JsonObject>();
  jsondata["reason"] = reason;
  jsondata["retain"] = true;
  jsondata["uptime"] = uptime();
  jsondata["origin"] = subjectLOGtoMQTT;
  pub(jsondata); // We go to MQTT bypassing the queue to ensure the message is sent
  // Clean queue
  while (!jsonQueue.empty()) {
    jsonQueue.pop();
  }
  Log.warning(F("Rebooting for reason code %d" CR), reason);
#if defined(ESP32)
  ESP.restart();
#elif defined(ESP8266)
  ESP.reset();
#endif
}

#if defined(ESPWifiManualSetup)
void setupWiFiFromBuild() {
  WiFi.mode(WIFI_STA);
  wifiMulti.addAP(wifi_ssid, wifi_password);
  Log.trace(F("Connecting to %s" CR), wifi_ssid);
#  ifdef wifi_ssid1
  wifiMulti.addAP(wifi_ssid1, wifi_password1);
  Log.trace(F("Connecting to %s" CR), wifi_ssid1);
#  endif
  delay(10);

  // We start by connecting to a WiFi network

#  ifdef NetworkAdvancedSetup
  IPAddress ip_adress;
  IPAddress gateway_adress;
  IPAddress subnet_adress;
  IPAddress dns_adress;
  ip_adress.fromString(NET_IP);
  gateway_adress.fromString(NET_GW);
  subnet_adress.fromString(NET_MASK);
  dns_adress.fromString(NET_DNS);

  if (!WiFi.config(ip_adress, gateway_adress, subnet_adress, dns_adress)) {
    Log.error(F("Wifi STA Failed to configure" CR));
    gatewayState = GatewayState::ERROR;
  }

#  endif

  while (wifiMulti.run() != WL_CONNECTED) {
    delay(500);
    Log.trace(F("." CR));
    failure_number_ntwk++;
#  if defined(ESP32) && defined(ZgatewayBT)
    if (SYSConfig.powerMode) {
      if (failure_number_ntwk > maxConnectionRetryNetwork) {
        sleep();
      }
    } else {
      if (failure_number_ntwk > maxRetryWatchDog) {
        ESPRestart(2);
      }
    }
#  else
    if (failure_number_ntwk > maxRetryWatchDog) {
      ESPRestart(2);
    }
#  endif
  }
  Log.notice(F("WiFi ok with manual config credentials" CR));
  displayPrint("Wifi connected");
}

#else

WiFiManager wifiManager;

//flag for saving data
bool shouldSaveConfig = false;
//do we have been connected once to MQTT

//callback notifying us of the need to save config
void saveConfigCallback() {
  Log.trace(F("Should save config" CR));
  shouldSaveConfig = true;
}

#  ifdef TRIGGER_GPIO
/**
 * Identify a long button press to trigger a reset or a failsafe mode
* */
void blockingWaitForReset() {
  if (digitalRead(TRIGGER_GPIO) == LOW) {
    delay(50);
    if (digitalRead(TRIGGER_GPIO) == LOW) {
      Log.trace(F("Trigger button Pressed" CR));
      delay(3000); // reset delay hold
      if (digitalRead(TRIGGER_GPIO) == LOW) {
        Log.notice(F("Button Held" CR));
// Switching off the relay during reset or failsafe operations
#    ifdef ZactuatorONOFF
        uint8_t level = digitalRead(ACTUATOR_ONOFF_GPIO);
        if (level == ACTUATOR_ON) {
          ActuatorTrigger();
        }
#    endif
        gatewayState = GatewayState::WAITING_ONBOARDING;
        // Checking if the flash has already been erased to identify if we erase it or go into failsafe mode
        // going to failsafe mode is done by doing a long button press from a state where the flash has already been erased
        if (SPIFFS.begin()) {
          Log.trace(F("mounted file system" CR));
          if (SPIFFS.exists("/config.json")) {
            Log.notice(F("Erasing ESP Config, restarting" CR));
            eraseConfig();
          }
        }
        delay(30000);
        if (digitalRead(TRIGGER_GPIO) == LOW) {
          Log.notice(F("Going into failsafe mode without peripherals" CR));
          // Failsafe mode enable to connect to Wifi or change the firmware without the peripherals setup
          failSafeMode = true;
          setupWiFiManager();
        }
      }
    }
  }
}

/**
 * Check if button is pressed so as to reset the credentials and parameters stored into the flash
* */
void checkButton() {
  unsigned long timeFromStart = millis();
  // Identify if the reset button is pushed at start
  if (timeFromStart < TimeToResetAtStart) {
    blockingWaitForReset();
  } else { // When we are not at start we either check the button as a regular input (ZsensorGPIOInput used) or for a reset
#    if defined(INPUT_GPIO) && defined(ZsensorGPIOInput) && INPUT_GPIO == TRIGGER_GPIO
    MeasureGPIOInput();
#    else
    blockingWaitForReset();
#    endif
  }
}
#  else
void checkButton() {}
#  endif

void saveConfig() {
  Log.trace(F("saving configs" CR));

  int totalSize = 512;
#  if !MQTT_BROKER_MODE
  for (int i = 0; i < 3; ++i) { // index 0 contains the default values from the build, these values can't be changed at runtime
    if (cnt_parameters_array[i].validConnection) {
      totalSize += cnt_parameters_array[i].server_cert.length();
      totalSize += cnt_parameters_array[i].client_cert.length();
      totalSize += cnt_parameters_array[i].client_key.length();
      totalSize += cnt_parameters_array[i].ota_server_cert.length();
    }
  }
#  endif

  Log.notice(F("Total size: %d" CR), totalSize);

  DynamicJsonDocument json(512 + totalSize);

#  if !MQTT_BROKER_MODE
  for (int i = 0; i < 3; ++i) {
    if (cnt_parameters_array[i].validConnection) {
      char index_suffix[2];
      if (i == 0) {
        index_suffix[0] = '\0';
      } else {
        index_suffix[0] = '0' + i;
        index_suffix[1] = '\0';
      }
      char key[mqtt_key_max_size];
      if (cnt_parameters_array[i].server_cert.length() > MIN_CERT_LENGTH) {
        strcpy(key, "mqtt_broker_cert");
        strcat(key, index_suffix);
        json[key] = cnt_parameters_array[i].server_cert;
      }
      if (cnt_parameters_array[i].client_cert.length() > MIN_CERT_LENGTH) {
        strcpy(key, "mqtt_client_cert");
        strcat(key, index_suffix);
        json[key] = cnt_parameters_array[i].client_cert;
      }
      if (cnt_parameters_array[i].client_key.length() > MIN_CERT_LENGTH) {
        strcpy(key, "mqtt_client_key");
        strcat(key, index_suffix);
        json[key] = cnt_parameters_array[i].client_key;
      }
      if (cnt_parameters_array[i].ota_server_cert.length() > MIN_CERT_LENGTH) {
        strcpy(key, "ota_server_cert");
        strcat(key, index_suffix);
        json[key] = cnt_parameters_array[i].ota_server_cert;
      }
      strcpy(key, "mqtt_server");
      strcat(key, index_suffix);
      json[key] = cnt_parameters_array[i].mqtt_server;
      strcpy(key, "mqtt_port");
      strcat(key, index_suffix);
      json[key] = cnt_parameters_array[i].mqtt_port;
      strcpy(key, "mqtt_user");
      strcat(key, index_suffix);
      json[key] = cnt_parameters_array[i].mqtt_user;
      strcpy(key, "mqtt_pass");
      strcat(key, index_suffix);
      json[key] = cnt_parameters_array[i].mqtt_pass;
      strcpy(key, "mqtt_broker_secure");
      strcat(key, index_suffix);
      json[key] = cnt_parameters_array[i].isConnectionSecure;
      strcpy(key, "mqtt_iscertvalid");
      strcat(key, index_suffix);
      json[key] = cnt_parameters_array[i].isCertValidate;
      strcpy(key, "valid_cnt");
      strcat(key, index_suffix);
      json[key] = cnt_parameters_array[i].validConnection;
    }
  }

  if (cnt_parameters_array[cnt_index].validConnection) {
    json["cnt_index"] = cnt_index;
  }
#  endif

  json["mqtt_topic"] = mqtt_topic;
#  ifdef ZmqttDiscovery
  json["discovery_prefix"] = discovery_prefix;
#  endif
  json["gateway_name"] = gateway_name;
  json["ota_pass"] = ota_pass;

  File configFile = SPIFFS.open("/config.json", "w");
  if (!configFile) {
    Log.error(F("failed to open config file for writing" CR));
    gatewayState = GatewayState::ERROR;
  }

  serializeJson(json, configFile);
  configFile.close();
}

bool loadConfigFromFlash() {
  Log.trace(F("mounting FS..." CR));
  bool result = false;

  if (SPIFFS.begin()) {
    Log.trace(F("mounted file system" CR));
  } else {
    Log.warning(F("failed to mount FS -> formating" CR));
    SPIFFS.format();
    if (SPIFFS.begin())
      Log.trace(F("mounted file system after formating" CR));
  }
  if (SPIFFS.exists("/config.json")) {
    //file exists, reading and loading
    Log.trace(F("reading config file" CR));
    File configFile = SPIFFS.open("/config.json", "r");
    if (configFile) {
      Log.trace(F("opened config file" CR));
      DynamicJsonDocument json(configFile.size() * 2);
      auto error = deserializeJson(json, configFile);
      if (error) {
        Log.error(F("deserialize config failed: %s, buffer capacity: %u" CR), error.c_str(), json.capacity());
        gatewayState = GatewayState::ERROR;
      }
      if (!json.isNull()) {
        Log.trace(F("\nparsed json, size: %u" CR), json.memoryUsage());
        // Print json to serial port
        //serializeJsonPretty(json, Serial);

#  if !MQTT_BROKER_MODE
        for (int i = 0; i < 3; ++i) {
          char index_suffix[2]; // Large enough for 0, 1, or 2 and the null terminator
          if (i == 0) {
            index_suffix[0] = '\0'; // Empty string
          } else {
            index_suffix[0] = '0' + i; // Convert int to char
            index_suffix[1] = '\0'; // Null terminator
          }
          char key[mqtt_key_max_size];
          strcpy(key, "mqtt_broker_cert");
          strcat(key, index_suffix);
          if (json.containsKey(key)) {
            cnt_parameters_array[i].server_cert = json[key].as<const char*>();
          }
          strcpy(key, "mqtt_client_cert");
          strcat(key, index_suffix);
          if (json.containsKey(key)) {
            cnt_parameters_array[i].client_cert = json[key].as<const char*>();
          }
          strcpy(key, "mqtt_client_key");
          strcat(key, index_suffix);
          if (json.containsKey(key)) {
            cnt_parameters_array[i].client_key = json[key].as<const char*>();
          }
          strcpy(key, "ota_server_cert");
          strcat(key, index_suffix);
          if (json.containsKey(key)) {
#    ifdef ESP32
            // Read hash from the file
            std::string hash = generateHash(json["ota_server_cert"]);
            // Compare the hash with the expected hash
            if (hash == GITHUB_OTA_SERVER_CERT_HASH) {
              // Do nothing
              Log.warning(F("Old Github OTA server detected, skipping" CR));
            } else {
              Log.notice(F("OTA server cert hash: %s" CR), hash.c_str());
              cnt_parameters_array[i].ota_server_cert = json["ota_server_cert"].as<const char*>();
            }
#    else
            cnt_parameters_array[i].ota_server_cert = json["ota_server_cert"].as<const char*>();
#    endif
          }
          strcpy(key, "mqtt_server");
          strcat(key, index_suffix);
          if (json.containsKey(key)) {
            strcpy(cnt_parameters_array[i].mqtt_server, json[key].as<const char*>());
          }
          strcpy(key, "mqtt_port");
          strcat(key, index_suffix);
          if (json.containsKey(key)) {
            strcpy(cnt_parameters_array[i].mqtt_port, json[key].as<const char*>());
          }
          strcpy(key, "mqtt_user");
          strcat(key, index_suffix);
          if (json.containsKey(key)) {
            strcpy(cnt_parameters_array[i].mqtt_user, json[key].as<const char*>());
          }
          strcpy(key, "mqtt_pass");
          strcat(key, index_suffix);
          if (json.containsKey(key)) {
            strcpy(cnt_parameters_array[i].mqtt_pass, json[key].as<const char*>());
          }
          strcpy(key, "mqtt_broker_secure");
          strcat(key, index_suffix);
          if (json.containsKey(key)) {
            cnt_parameters_array[i].isConnectionSecure = json[key].as<bool>();
          }
          strcpy(key, "mqtt_iscertvalid");
          strcat(key, index_suffix);
          if (json.containsKey(key)) {
            cnt_parameters_array[i].isCertValidate = json[key].as<bool>();
          }
          strcpy(key, "valid_cnt");
          strcat(key, index_suffix);
          if (json.containsKey(key)) {
            cnt_parameters_array[i].validConnection = json[key].as<bool>();
          } else if (i == CNT_DEFAULT_INDEX) {
            // For backward compatibility, if valid_cnt is not found, we assume the connection is valid for CNT_DEFAULT_INDEX
            Log.warning(F("valid_cnt not found, assuming connection is valid" CR));
            cnt_parameters_array[i].validConnection = true;
          }
        }
        if (json.containsKey("cnt_index")) {
          cnt_index = json["cnt_index"].as<int>();
        }
#  endif
        if (json.containsKey("mqtt_topic"))
          strcpy(mqtt_topic, json["mqtt_topic"]);
#  ifdef ZmqttDiscovery
        if (json.containsKey("discovery_prefix"))
          strcpy(discovery_prefix, json["discovery_prefix"]);
#  endif
        if (json.containsKey("gateway_name"))
          strcpy(gateway_name, json["gateway_name"]);
        if (json.containsKey("ota_pass")) {
          strcpy(ota_pass, json["ota_pass"]);
#  ifdef WM_PWD_FROM_MAC // From ESP Mac Address, last 8 digits as the password
          // Compare the existing ota_pass if ota_pass = OTAPASSWORD then replace with the last 8 digits of the mac address
          // This enable user migrating from previous version to have the same WiFi portal password as previously unless they changed it
          if (strcmp(ota_pass, "OTAPASSWORD") == 0) {
            String s = WiFi.macAddress();
            sprintf(ota_pass, "%.2s%.2s%.2s%.2s",
                    s.c_str() + 6, s.c_str() + 9, s.c_str() + 12, s.c_str() + 15);
          }
#  endif
        }
        result = true;
      } else {
        Log.warning(F("failed to load json config" CR));
      }
      configFile.close();
    }
  } else {
    Log.notice(F("No config file found defining default values" CR));
#  ifdef USE_MAC_AS_GATEWAY_NAME
    String s = WiFi.macAddress();
    sprintf(gateway_name, "%.2s%.2s%.2s%.2s%.2s%.2s",
            s.c_str(), s.c_str() + 3, s.c_str() + 6, s.c_str() + 9, s.c_str() + 12, s.c_str() + 15);
#  endif
#  ifdef WM_PWD_FROM_MAC // From ESP Mac Address, last 8 digits as the password
    sprintf(ota_pass, "%.2s%.2s%.2s%.2s",
            s.c_str() + 6, s.c_str() + 9, s.c_str() + 12, s.c_str() + 15);
#  endif
  }

  return result;
}

void setupWiFiManager() {
  delay(10);
  WiFi.mode(WIFI_STA);

#  ifdef USE_MAC_AS_GATEWAY_NAME
  String s = WiFi.macAddress();
  snprintf(WifiManager_ssid, MAC_NAME_MAX_LEN, "%s_%.2s%.2s", Gateway_Short_Name, s.c_str(), s.c_str() + 3);
  strcpy(ota_hostname, WifiManager_ssid);
  Log.notice(F("OTA Hostname: %s.local" CR), ota_hostname);
#  endif

  wifiManager.setDebugOutput(WM_DEBUG);

  // The extra parameters to be configured (can be either global or just in the setup)
  // After connecting, parameter.getValue() will get you the configured value
  // id/name placeholder/prompt default
#  ifndef WIFIMNG_HIDE_MQTT_CONFIG
#    if !MQTT_BROKER_MODE
  WiFiManagerParameter custom_mqtt_server("server", "mqtt server", cnt_parameters_array[CNT_DEFAULT_INDEX].mqtt_server, parameters_size, " minlength='1' maxlength='64' required");
  WiFiManagerParameter custom_mqtt_port("port", "mqtt port", cnt_parameters_array[CNT_DEFAULT_INDEX].mqtt_port, 6, " minlength='1' maxlength='5' required");
  WiFiManagerParameter custom_mqtt_user("user", "mqtt user", cnt_parameters_array[CNT_DEFAULT_INDEX].mqtt_user, parameters_size, " maxlength='64'");
  WiFiManagerParameter custom_mqtt_pass("pass", "mqtt pass", MQTT_PASS, parameters_size, " input type='password' maxlength='64'");
  WiFiManagerParameter custom_mqtt_secure("secure", "<br/>mqtt secure", "1", 2, cnt_parameters_array[CNT_DEFAULT_INDEX].isConnectionSecure ? "type=\"checkbox\" checked" : "type=\"checkbox\"");
  WiFiManagerParameter custom_validate_cert("validate", "<br/>validate cert", "1", 2, cnt_parameters_array[CNT_DEFAULT_INDEX].isCertValidate ? "type=\"checkbox\" checked" : "type=\"checkbox\"");
  WiFiManagerParameter custom_mqtt_cert("cert", "<br/>mqtt server cert", "", 4096);
  WiFiManagerParameter custom_ota_server_cert("ota_cert", "<br/>ota server cert", "", 4096);
#      if MQTT_SECURE_SIGNED_CLIENT
  WiFiManagerParameter custom_client_cert("client_cert", "<br/>mqtt client cert", "", 4096);
  WiFiManagerParameter custom_client_key("client_key", "<br/>mqtt client key", "", 4096);
#      endif
#    endif
  WiFiManagerParameter custom_mqtt_topic("topic", "mqtt base topic", mqtt_topic, mqtt_topic_max_size, " minlength='1' maxlength='64' required");
  WiFiManagerParameter custom_gateway_name("name", "gateway name", gateway_name, parameters_size, " minlength='1' maxlength='64' required");
  WiFiManagerParameter custom_ota_pass("ota", "gateway password", ota_pass, parameters_size, " input type='password' minlength='8' maxlength='64' required");
#  endif
  //WiFiManager
  //Local intialization. Once its business is done, there is no need to keep it around

  wifiManager.setConnectTimeout(WiFi_TimeOut);
  //Set timeout before going to portal
  wifiManager.setConfigPortalTimeout(WifiManager_ConfigPortalTimeOut);

  //set config save notify callback
  wifiManager.setSaveConfigCallback(saveConfigCallback);

//set static IP
#  ifdef NetworkAdvancedSetup
  Log.trace(F("Adv wifi cfg" CR));
  IPAddress ip_adress;
  IPAddress gateway_adress;
  IPAddress subnet_adress;
  IPAddress dns_adress;
  ip_adress.fromString(NET_IP);
  gateway_adress.fromString(NET_GW);
  subnet_adress.fromString(NET_MASK);
  dns_adress.fromString(NET_DNS);
  wifiManager.setSTAStaticIPConfig(ip_adress, gateway_adress, subnet_adress, dns_adress);
#  endif

#  ifndef WIFIMNG_HIDE_MQTT_CONFIG
  //add all your parameters here
#    if !MQTT_BROKER_MODE
  wifiManager.addParameter(&custom_mqtt_server);
  wifiManager.addParameter(&custom_mqtt_port);
  wifiManager.addParameter(&custom_mqtt_user);
  wifiManager.addParameter(&custom_mqtt_pass);
  wifiManager.addParameter(&custom_mqtt_secure);
  wifiManager.addParameter(&custom_mqtt_cert);
  wifiManager.addParameter(&custom_validate_cert);
  wifiManager.addParameter(&custom_ota_server_cert);
#      if MQTT_SECURE_SIGNED_CLIENT
  wifiManager.addParameter(&custom_client_cert);
  wifiManager.addParameter(&custom_client_key);
#      endif
#    endif
  wifiManager.addParameter(&custom_gateway_name);
  wifiManager.addParameter(&custom_mqtt_topic);
  wifiManager.addParameter(&custom_ota_pass);
#  endif
  //set minimum quality of signal so it ignores AP's under that quality
  wifiManager.setMinimumSignalQuality(MinimumWifiSignalQuality);

  if (SPIFFS.begin()) {
    Log.trace(F("mounted file system" CR));
    // Check if the config file exists and prevent the portal from showing if yes
    // Showing the portal if the config file exist would enable access to the configuration data and to the ESP update page
    // This is a security risk if an attacker has access to the gateway password
    if (SPIFFS.exists("/config.json")) {
      wifiManager.setEnableConfigPortal(false);
    }
  }

#  ifdef ESP32_ETHERNET
  wifiManager.setBreakAfterConfig(true); // If ethernet is used, we don't want to block the connection by keeping the portal up
#  endif

  if (!SYSConfig.offline && !wifi_reconnect_bypass()) // if we didn't connect with saved credential we start Wifimanager web portal
  {
    Log.notice(F("Connect your phone to WIFI AP: %s with PWD: %s" CR), WifiManager_ssid, ota_pass);
    gatewayState = GatewayState::ONBOARDING;
    //fetches ssid and pass and tries to connect
    //if it does not connect it starts an access point with the specified name
    //and goes into a blocking loop awaiting configuration
    if (!wifiManager.autoConnect(WifiManager_ssid, ota_pass)) {
      Log.warning(F("failed to connect and hit timeout" CR));
      delay(3000);

#  ifdef ESP32
      /* Workaround for bug in arduino core that causes the AP to become unsecure on reboot */
      esp_wifi_set_mode(WIFI_MODE_AP);
      esp_wifi_start();
      wifi_config_t conf;
      esp_wifi_get_config(WIFI_IF_AP, &conf);
      conf.ap.ssid_hidden = 1;
      esp_wifi_set_config(WIFI_IF_AP, &conf);
#  endif

      bool shouldRestart = (gatewayState != GatewayState::BROKER_CONNECTED && !ethConnected && gatewayState != GatewayState::NTWK_CONNECTED);

#  ifdef USE_BLUFI
      shouldRestart = shouldRestart && !isStaConnecting();
#  endif
      // Restart/sleep only if not connected and not serial communication mode
      if (shouldRestart && !SYSConfig.serial) {
#  ifdef DEEP_SLEEP_IN_US
        sleep();
#  endif
        ESPRestart(3);
      }
    }
  }

  displayPrint("Network connected");
  gatewayState = GatewayState::NTWK_CONNECTED;

  if (shouldSaveConfig) {
    //read updated parameters
    cnt_index = CNT_DEFAULT_INDEX;
#  ifndef WIFIMNG_HIDE_MQTT_CONFIG
#    if !MQTT_BROKER_MODE
    strcpy(cnt_parameters_array[cnt_index].mqtt_server, custom_mqtt_server.getValue());
    strcpy(cnt_parameters_array[cnt_index].mqtt_port, custom_mqtt_port.getValue());
    strcpy(cnt_parameters_array[cnt_index].mqtt_user, custom_mqtt_user.getValue());
    // Check if the MQTT password field contains the default value
    if (strcmp(custom_mqtt_pass.getValue(), MQTT_PASS) != 0) {
      // If it's not the default password, update the MQTT password
      strcpy(cnt_parameters_array[cnt_index].mqtt_pass, custom_mqtt_pass.getValue());
    }
    strcpy(mqtt_topic, custom_mqtt_topic.getValue());
    if (mqtt_topic[strlen(mqtt_topic) - 1] != '/' && strlen(mqtt_topic) < parameters_size) {
      strcat(mqtt_topic, "/");
    }

    cnt_parameters_array[cnt_index].isConnectionSecure = *custom_mqtt_secure.getValue();
    cnt_parameters_array[cnt_index].isCertValidate = *custom_validate_cert.getValue();
    if (strlen(custom_mqtt_cert.getValue()) > MIN_CERT_LENGTH) {
      cnt_parameters_array[cnt_index].server_cert = TheengsUtils::processCert(custom_mqtt_cert.getValue());
    }
    if (strlen(custom_ota_server_cert.getValue()) > MIN_CERT_LENGTH) {
      cnt_parameters_array[cnt_index].ota_server_cert = TheengsUtils::processCert(custom_ota_server_cert.getValue());
    }
#      if MQTT_SECURE_SIGNED_CLIENT
    if (strlen(custom_client_cert.getValue()) > MIN_CERT_LENGTH) {
      cnt_parameters_array[cnt_index].client_cert = TheengsUtils::processCert(custom_client_cert.getValue());
    }
    if (strlen(custom_client_key.getValue()) > MIN_CERT_LENGTH) {
      cnt_parameters_array[cnt_index].client_key = TheengsUtils::processCert(custom_client_key.getValue());
    }
#      endif
#    endif
    strcpy(gateway_name, custom_gateway_name.getValue());
    strcpy(ota_pass, custom_ota_pass.getValue());
#  endif

#  if !MQTT_BROKER_MODE
    // We suppose the connection is valid (could be tested before)
    cnt_parameters_array[cnt_index].validConnection = true;
#  endif

    //save the custom parameters to FS
    saveConfig();
  }
}
#  ifdef ESP32_ETHERNET
void setup_ethernet_esp32() {
  bool ethBeginSuccess = false;
  WiFi.onEvent(WiFiEvent);
#    ifdef NetworkAdvancedSetup
  IPAddress ip_adress;
  IPAddress gateway_adress;
  IPAddress subnet_adress;
  IPAddress dns_adress;
  ip.fromString(NET_IP);
  gateway.fromString(NET_GW);
  subnet.fromString(NET_MASK);
  Dns.fromString(NET_DNS);

  Log.trace(F("Adv eth cfg" CR));
  ETH.config(ip, gateway, subnet, Dns);
  ethBeginSuccess = ETH.begin();
#    else
  Log.notice(F("Spl eth cfg" CR));
  ethBeginSuccess = ETH.begin();
#    endif
  if (ethBeginSuccess) {
    Log.notice(F("Ethernet started" CR));
    while (!ethConnected && failure_number_ntwk <= maxConnectionRetryNetwork) {
      delay(500);
      Log.notice(F("." CR));
      failure_number_ntwk++;
    }
  } else {
    Log.error(F("Ethernet not started" CR));
    gatewayState = GatewayState::ERROR;
  }
}

void WiFiEvent(WiFiEvent_t event) {
  switch (event) {
    case ARDUINO_EVENT_ETH_START:
      Log.trace(F("Ethernet Started" CR));
      ETH.setHostname(gateway_name);
      break;
    case ARDUINO_EVENT_ETH_CONNECTED:
      Log.notice(F("Ethernet Connected" CR));
      break;
    case ARDUINO_EVENT_ETH_GOT_IP:
      Log.notice(F("OpenMQTTGateway Ethernet MAC: %s" CR), ETH.macAddress().c_str());
      Log.notice(F("OpenMQTTGateway Ethernet IP: %s" CR), ETH.localIP().toString().c_str());
      Log.notice(F("OpenMQTTGateway Ethernet link speed: %d Mbps" CR), ETH.linkSpeed());
      gatewayState = GatewayState::NTWK_CONNECTED;
      ethConnected = true;
      break;
    case ARDUINO_EVENT_ETH_DISCONNECTED:
      Log.warning(F("Ethernet Disconnected" CR));
      ethConnected = false;
      break;
    case ARDUINO_EVENT_ETH_STOP:
      Log.warning(F("Ethernet Stopped" CR));
      ethConnected = false;
      break;
    default:
      break;
  }
}
#  endif
#endif

#if DEFAULT_LOW_POWER_MODE != DEACTIVATED && defined(ESP32)
/**
 * Deep-sleep for the ESP32 - e.g. DEEP_SLEEP_IN_US 30000000 for 30 seconds / wake by ESP32_EXT0_WAKE_PIN/ESP32_EXT1_WAKE_PIN.
 * Everything is off and (almost) all execution state is lost.
 */
void sleep() {
  if (SYSConfig.powerMode < PowerMode::INTERVAL)
    return;
  Log.notice(F("Entering deep sleep" CR));
  gatewayState = GatewayState::SLEEPING;
  delay(250); // To allow the LEDs to switch off and MQTT message to be sent
#  if defined(ZboardM5STACK) || defined(ZboardM5STICKC) || defined(ZboardM5STICKCP) || defined(ZboardM5TOUGH)
  sleepScreen();
  esp_sleep_enable_ext0_wakeup((gpio_num_t)SLEEP_BUTTON, LOW);
#  endif
  Log.trace(F("Deactivating ESP32 components" CR));
#  ifdef ZgatewayBT
  stopProcessing(true);
  ProcessLock = true;
#  endif
#  pragma GCC diagnostic push
#  pragma GCC diagnostic ignored "-Wdeprecated-declarations"
  adc_power_off();
#  pragma GCC diagnostic pop
  esp_wifi_stop();
#  ifdef ESP32_EXT0_WAKE_PIN
  Log.notice(F("Entering deep sleep, EXT0 Wakeup by pin : %l." CR), ESP32_EXT0_WAKE_PIN);
#  endif
#  ifdef ESP32_EXT1_WAKE_PIN
  Log.notice(F("Entering deep sleep, EXT1 Wakeup by pin : %l." CR), ESP32_EXT1_WAKE_PIN);
#  endif
  if (SYSConfig.powerMode == PowerMode::ACTION) {
    esp_deep_sleep_start();
  } else if (SYSConfig.powerMode == PowerMode::INTERVAL) {
    Log.notice(F("Entering deep sleep for %l us." CR), DEEP_SLEEP_IN_US);
    esp_deep_sleep(DEEP_SLEEP_IN_US);
  }
}
#else
void sleep() {}
#endif

void loop() {
#ifndef ESPWifiManualSetup
  checkButton(); // check if a reset of wifi/mqtt settings is asked
#endif

#ifdef ESP8266
  updateAndHandleLEDsTask(); // With ESP8266 we need to update the LEDs in the loop
#endif
  if (!SYSConfig.offline) { // Online mode
    if (mqttSetupPending) {
      setupMQTT();
      mqttSetupPending = false;
    }
    // When online the MQTT connection callback release the processes
  }
  if (firstStart) {
#ifdef ZgatewaySERIAL
    if (SYSConfig.serial && isSerialReady()) {
#  ifdef ZgatewayBT
      BTProcessLock = !BTConfig.enabled;
#  endif
      ProcessLock = false;
      firstStart = false;
    }
#else
    ProcessLock = false;
    firstStart = false;
#endif
  }
  unsigned long now = millis();

#ifdef ZgatewaySERIAL // Serial is a module and a communication layer so it's always processed
  SERIALtoX();
#endif

  if (ethConnected || WiFi.status() == WL_CONNECTED) {
    if (ethConnected && WiFi.status() == WL_CONNECTED) {
      WiFi.disconnect(); // we disconnect the wifi as we are connected to ethernet
    }
    ArduinoOTA.handle();
    failure_number_ntwk = 0;
    if (now > (timer_sys_checks + (TimeBetweenCheckingSYS * 1000)) || !timer_sys_checks) {
#if message_UTCtimestamp || message_unixtimestamp
      TheengsUtils::syncNTP();
#endif
      if (!timer_sys_checks) { // Update check at start up only
#if defined(ESP32) && defined(MQTT_HTTPS_FW_UPDATE)
        checkForUpdates();
#endif
      }
      timer_sys_checks = millis();
    }
#if defined(ZwebUI) && defined(ESP32)
    WebUILoop();
#endif
    mqtt->loop();
    if (mqtt->connected()) { // MQTT client is still connected
      failure_number_ntwk = 0;

#ifdef ZmqttDiscovery
      // Deactivate autodiscovery after DiscoveryAutoOffTimer.
      // Exception: when discovery_republish_on_reconnect is enabled, we never never automatically disable discovery
      if (!discovery_republish_on_reconnect && SYSConfig.discovery && (now > lastDiscovery + DiscoveryAutoOffTimer))
        SYSConfig.discovery = false;
#endif
    }
  } else if (!SYSConfig.offline && !SYSConfig.serial) { // disconnected from network
    Log.warning(F("Network disconnected" CR));
    gatewayState = GatewayState::NTWK_DISCONNECTED;
    if (!wifi_reconnect_bypass()) {
      sleep();
    } else {
      gatewayState = GatewayState::NTWK_CONNECTED;
    }
  }
  if (!ProcessLock) {
    if (now > (timer_sys_measures + (TimeBetweenReadingSYS * 1000)) || !timer_sys_measures) {
      timer_sys_measures = millis();
      stateMeasures();
#ifdef ZgatewayBT
      stateBTMeasures(false);
#endif
#ifdef ZactuatorONOFF
      stateONOFFMeasures();
#endif
#ifdef ZdisplaySSD1306
      stateSSD1306Display();
#endif
#ifdef ZgatewayLORA
      stateLORAMeasures();
#endif
#if defined(ZgatewayRTL_433) || defined(ZgatewayPilight) || defined(ZgatewayRF) || defined(ZgatewayRF2) || defined(ZactuatorSomfy)
      stateRFMeasures();
#endif
#if defined(ZwebUI) && defined(ESP32)
      stateWebUIStatus();
#endif
    }
// Function that doesn't need an active connection
#if defined(ZboardM5STICKC) || defined(ZboardM5STICKCP) || defined(ZboardM5STACK) || defined(ZboardM5TOUGH)
    loopM5();
#endif
#if defined(ZdisplaySSD1306)
    loopSSD1306();
#endif
#ifdef ZsensorBME280
    MeasureTempHumAndPressure(); //Addon to measure Temperature, Humidity, Pressure and Altitude with a Bosch BME280/BMP280
#endif
#ifdef ZsensorHTU21
    MeasureTempHum(); //Addon to measure Temperature, Humidity, of a HTU21 sensor
#endif
#ifdef ZsensorLM75
    MeasureTemp(); //Addon to measure Temperature of an LM75 sensor
#endif
#ifdef ZsensorAHTx0
    MeasureAHTTempHum(); //Addon to measure Temperature, Humidity, of an 'AHTx0' sensor
#endif
#ifdef ZsensorHCSR04
    MeasureDistance(); //Addon to measure distance with a HC-SR04
#endif
#ifdef ZsensorBH1750
    MeasureLightIntensity(); //Addon to measure Light Intensity with a BH1750
#endif
#ifdef ZsensorMQ2
    MeasureGasMQ2();
#endif
#ifdef ZsensorTEMT6000
    MeasureLightIntensityTEMT6000();
#endif
#ifdef ZsensorTSL2561
    MeasureLightIntensityTSL2561();
#endif
#ifdef ZsensorC37_YL83_HMRD
    MeasureC37_YL83_HMRDWater(); //Addon for leak detection with a C-37 YL-83 H-MRD
#endif
#ifdef ZsensorDHT
    MeasureTempAndHum(); //Addon to measure the temperature with a DHT
#endif
#ifdef ZsensorSHTC3
    MeasureTempAndHum(); //Addon to measure the temperature with a DHT
#endif
#ifdef ZsensorDS1820
    MeasureDS1820Temp(); //Addon to measure the temperature with DS1820 sensor(s)
#endif
#ifdef ZsensorINA226
    MeasureINA226();
#endif
#ifdef ZsensorHCSR501
    MeasureHCSR501();
#endif
#ifdef ZsensorGPIOInput
    MeasureGPIOInput();
#endif
#ifdef ZsensorGPIOKeyCode
    MeasureGPIOKeyCode();
#endif
#ifdef ZsensorADC
    MeasureADC(); //Addon to measure the analog value of analog pin
#endif
#ifdef ZsensorTouch
    MeasureTouch();
#endif
#ifdef ZgatewayLORA
    LORAtoX();
#  ifdef ZmqttDiscovery
    if (SYSConfig.discovery)
      launchLORADiscovery(false);
#  endif
#endif
#ifdef ZgatewayRF
    RFtoX();
#endif
#ifdef ZgatewayRF2
    RF2toX();
#endif
#ifdef ZgatewayWeatherStation
    ZgatewayWeatherStationtoX();
#endif
#ifdef ZgatewayGFSunInverter
    ZgatewayGFSunInverterMQTT();
#endif
#ifdef ZgatewayPilight
    PilighttoX();
#endif
#ifdef ZgatewayBT
#  ifdef ZmqttDiscovery
    if (SYSConfig.discovery)
      launchBTDiscovery(false);
#  endif
#endif
#ifdef ZgatewaySRFB
    SRFBtoX();
#endif
#ifdef ZgatewayIR
    IRtoX();
#endif
#ifdef Zgateway2G
    if (_2GtoX())
      Log.trace(F("2GtoMQTT OK" CR));
#endif
#ifdef ZgatewayRFM69
    if (RFM69toX())
      Log.trace(F("RFM69toMQTT OK" CR));
#endif
#ifdef ZactuatorFASTLED
    FASTLEDLoop();
#endif
#ifdef ZactuatorPWM
    PWMLoop();
#endif
#ifdef ZgatewayRTL_433
    RTL_433Loop();
#  ifdef ZmqttDiscovery
    if (SYSConfig.discovery)
      launchRTL_433Discovery(false);
#  endif
#endif
  }
  // Empty the queue
  emptyQueue();
  // Sleep if ready
  if (ready_to_sleep) {
    sleep();
  }
}

/**
 * Calculate uptime and take into account the millis() rollover
 */
unsigned long uptime() {
  static unsigned long lastUptime = 0;
  static unsigned long uptimeAdd = 0;
  unsigned long uptime = millis() / 1000 + uptimeAdd;
  if (uptime < lastUptime) {
    uptime += 4294967;
    uptimeAdd += 4294967;
  }
  lastUptime = uptime;
  return uptime;
}

/**
 * Calculate internal ESP32 temperature
 */
#if defined(ESP32) && !defined(NO_INT_TEMP_READING)
float intTemperatureRead() {
  SET_PERI_REG_BITS(SENS_SAR_MEAS_WAIT2_REG, SENS_FORCE_XPD_SAR, 3, SENS_FORCE_XPD_SAR_S);
  SET_PERI_REG_BITS(SENS_SAR_TSENS_CTRL_REG, SENS_TSENS_CLK_DIV, 10, SENS_TSENS_CLK_DIV_S);
  CLEAR_PERI_REG_MASK(SENS_SAR_TSENS_CTRL_REG, SENS_TSENS_POWER_UP);
  CLEAR_PERI_REG_MASK(SENS_SAR_TSENS_CTRL_REG, SENS_TSENS_DUMP_OUT);
  SET_PERI_REG_MASK(SENS_SAR_TSENS_CTRL_REG, SENS_TSENS_POWER_UP_FORCE);
  SET_PERI_REG_MASK(SENS_SAR_TSENS_CTRL_REG, SENS_TSENS_POWER_UP);
  ets_delay_us(100);
  SET_PERI_REG_MASK(SENS_SAR_TSENS_CTRL_REG, SENS_TSENS_DUMP_OUT);
  ets_delay_us(5);
  float temp_f = (float)GET_PERI_REG_BITS2(SENS_SAR_SLAVE_ADDR3_REG, SENS_TSENS_OUT, SENS_TSENS_OUT_S);
  float temp_c = (temp_f - 32) / 1.8;
  return temp_c;
}
#endif

/*
 Erase config and restart the ESP
*/
void eraseConfig() {
#ifdef SecondaryModule
  // Erase the secondary module config
  String eraseCmdStr = "{\"cmd\":\"" + String(eraseCmd) + "\"}";
  Log.notice(F("Erasing secondary module config: %s" CR), eraseCmdStr.c_str());
  receivingDATA(subjectMQTTtoSYSsetSecondaryModule, eraseCmdStr.c_str());
  delay(2000);
#endif
  Log.trace(F("Formatting requested, result: %d" CR), SPIFFS.format());

#if defined(ESP8266)
  WiFi.disconnect(true);
  ESP.eraseConfig();
#else
  nvs_flash_erase();
#endif
  ESPRestart(0);
}

String stateMeasures() {
  StaticJsonDocument<JSON_MSG_BUFFER> SYSdata;

  SYSdata["uptime"] = uptime();

  SYSdata["version"] = OMG_VERSION;
#ifdef LED_ADDRESSABLE
  SYSdata["rgbb"] = SYSConfig.rgbbrightness;
#endif
#if USE_BLUFI
  SYSdata["blufi"] = SYSConfig.blufi;
#endif
  SYSdata["mqtt"] = SYSConfig.mqtt;
  SYSdata["serial"] = SYSConfig.serial;
#ifdef ZmqttDiscovery
  SYSdata["disc"] = SYSConfig.discovery;
  SYSdata["ohdisc"] = SYSConfig.ohdiscovery;
#endif
  SYSdata["env"] = ENV_NAME;
  uint32_t freeMem;
  uint32_t minFreeMem;
  freeMem = ESP.getFreeHeap();
#ifdef ZgatewayRTL_433
  // Some RTL_433 decoders have memory leak, this is a temporary workaround
  if (freeMem < MinimumMemory) {
    Log.error(F("Not enough memory %d, restarting" CR), freeMem);
    gatewayState = GatewayState::ERROR;
    ESPRestart(8);
  }
#endif
  SYSdata["freemem"] = freeMem;
#if !MQTT_BROKER_MODE
  SYSdata["mqttp"] = cnt_parameters_array[cnt_index].mqtt_port;
  SYSdata["mqtts"] = cnt_parameters_array[cnt_index].isConnectionSecure;
  SYSdata["mqttv"] = cnt_parameters_array[cnt_index].isCertValidate;
#endif
  SYSdata["msgprc"] = queueLengthSum;
  SYSdata["msgblck"] = blockedMessages;
  SYSdata["msgrcv"] = receivedMessages;
  SYSdata["maxq"] = maxQueueLength;
  SYSdata["cnt_index"] = cnt_index;
#ifdef ESP32
  minFreeMem = ESP.getMinFreeHeap();
  SYSdata["minmem"] = minFreeMem;
#  ifndef NO_INT_TEMP_READING
  SYSdata["tempc"] = TheengsUtils::round2(intTemperatureRead());
#  endif
  SYSdata["freestck"] = uxTaskGetStackHighWaterMark(NULL);
  SYSdata["powermode"] = SYSConfig.powerMode;
#endif

  SYSdata["eth"] = ethConnected;
  if (ethConnected) {
#ifdef ESP32_ETHERNET
    SYSdata["mac"] = (char*)ETH.macAddress().c_str();
    SYSdata["ip"] = TheengsUtils::ip2CharArray(ETH.localIP());
    ETH.fullDuplex() ? SYSdata["fd"] = (bool)"true" : SYSdata["fd"] = (bool)"false";
    SYSdata["linkspeed"] = (int)ETH.linkSpeed();
#endif
  } else {
    SYSdata["rssi"] = (long)WiFi.RSSI();
    SYSdata["SSID"] = (char*)WiFi.SSID().c_str();
    SYSdata["BSSID"] = (char*)WiFi.BSSIDstr().c_str();
    SYSdata["ip"] = TheengsUtils::ip2CharArray(WiFi.localIP());
    SYSdata["mac"] = (char*)WiFi.macAddress().c_str();
  }
#ifdef ZboardM5STACK
  M5.Power.begin();
  SYSdata["m5battlevel"] = (int8_t)M5.Power.getBatteryLevel();
  SYSdata["m5ischarging"] = (bool)M5.Power.isCharging();
  SYSdata["m5ischargefull"] = (bool)M5.Power.isChargeFull();
#endif
#if defined(ZboardM5STICKC) || defined(ZboardM5STICKCP) || defined(ZboardM5TOUGH)
  M5.Axp.EnableCoulombcounter();
  SYSdata["m5batvoltage"] = (float)M5.Axp.GetBatVoltage();
  SYSdata["m5batcurrent"] = (float)M5.Axp.GetBatCurrent();
  SYSdata["m5vinvoltage"] = (float)M5.Axp.GetVinVoltage();
  SYSdata["m5vincurrent"] = (float)M5.Axp.GetVinCurrent();
  SYSdata["m5vbusvoltage"] = (float)M5.Axp.GetVBusVoltage();
  SYSdata["m5vbuscurrent"] = (float)M5.Axp.GetVBusCurrent();
  SYSdata["m5tempaxp"] = (float)M5.Axp.GetTempInAXP192();
  SYSdata["m5batpower"] = (float)M5.Axp.GetBatPower();
  SYSdata["m5batchargecurrent"] = (float)M5.Axp.GetBatChargeCurrent();
  SYSdata["m5apsvoltage"] = (float)M5.Axp.GetAPSVoltage();
#endif
  SYSdata["modules"] = modules;

  SYSdata["origin"] = subjectSYStoMQTT;
  enqueueJsonObject(SYSdata);

  char jsonChar[100];
  serializeJson(modules, jsonChar, 99);

  String _modules = jsonChar;

  _modules.replace(",", ", ");
  _modules.replace("[", "");
  _modules.replace("]", "");
  _modules.replace("\"", "'");

  SYSdata["modules"] = _modules.c_str();

  String output;
  serializeJson(SYSdata, output);
  Log.notice(F("SYS json: %s" CR), output.c_str());
  return output;
}

#if defined(ZgatewayRF) || defined(ZgatewayIR) || defined(ZgatewaySRFB) || defined(ZgatewayWeatherStation) || defined(ZgatewayRTL_433)
/**
 * Store signal values from RF, IR, SRFB or Weather stations so as to avoid duplicates
 */
void storeSignalValue(uint64_t MQTTvalue) {
  unsigned long now = millis();
  // find oldest value of the buffer
  int o = getMin();
  Log.trace(F("Min ind: %d" CR), o);
  // replace it by the new one
  receivedSignal[o].value = MQTTvalue;
  receivedSignal[o].time = now;

  // Casting "receivedSignal[o].value" to (unsigned long) because ArduinoLog doesn't support uint64_t for ESP's
  Log.trace(F("store code : %u / %u" CR), (unsigned long)receivedSignal[o].value, receivedSignal[o].time);
  Log.trace(F("Col: val/timestamp" CR));
  for (int i = 0; i < struct_size; i++) {
    Log.trace(F("mem code : %u / %u" CR), (unsigned long)receivedSignal[i].value, receivedSignal[i].time);
  }
}

/**
 * get oldest time index from the values array from RF, IR, SRFB or Weather stations so as to avoid duplicates
 */
int getMin() {
  unsigned int minimum = receivedSignal[0].time;
  int minindex = 0;
  for (int i = 1; i < struct_size; i++) {
    if (receivedSignal[i].time < minimum) {
      minimum = receivedSignal[i].time;
      minindex = i;
    }
  }
  return minindex;
}

/**
 * Check if signal values from RF, IR, SRFB or Weather stations are duplicates
 */
bool isAduplicateSignal(uint64_t value) {
  Log.trace(F("isAdupl?" CR));
  for (int i = 0; i < struct_size; i++) {
    if (receivedSignal[i].value == value) {
      unsigned long now = millis();
      if (now - receivedSignal[i].time < time_avoid_duplicate) { // change
        Log.trace(F("no pub. dupl" CR));
        return true;
      }
    }
  }
  return false;
}
#endif

void receivingDATA(const char* topicOri, const char* datacallback) {
  std::string strTopicOri = topicOri;
  StaticJsonDocument<JSON_MSG_BUFFER_MAX> jsonBuffer;
  JsonObject jsondata = jsonBuffer.to<JsonObject>();
  DeserializationError error = deserializeJson(jsonBuffer, datacallback);
  if (error || jsondata.isNull()) {
    Log.error(F("deserialize MQTT data failed: %s" CR), error.c_str());
    gatewayState = GatewayState::ERROR;
    return;
  }
  if (topicOri == nullptr || strcmp(topicOri, "") == 0) {
    if (jsondata.containsKey("target") && jsondata["target"].is<const char*>()) {
      strTopicOri = jsondata["target"].as<const char*>();
      Log.trace(F("BUS Msg target: %s" CR), strTopicOri.c_str());
    } else if (jsondata.containsKey("origin") && jsondata["origin"].is<const char*>()) {
      strTopicOri = jsondata["origin"].as<const char*>();
      Log.trace(F("BUS Msg origin: %s" CR), strTopicOri.c_str());
    }
  } else {
#if defined(SecondaryModule) // Redirect certain commands to Serial
    String topicSerial = String(mqtt_topic) + String(gateway_name) + subjectMQTTtoSERIAL;
    const char* cSecondaryModule = SecondaryModule;
    if (strcmp(cSecondaryModule, "BT") == 0) {
      if (cmpToMainTopic(topicOri, subjectMQTTtoBTset)) {
        strTopicOri = topicSerial.c_str();
        jsondata["target"] = subjectMQTTtoBTset;
      } else if (cmpToMainTopic(topicOri, subjectMQTTtoBT)) {
        strTopicOri = topicSerial.c_str();
        jsondata["target"] = subjectMQTTtoBT;
      }
    }
    if (cmpToMainTopic(topicOri, subjectMQTTtoSYSsetSecondaryModule)) {
      strTopicOri = topicSerial.c_str();
      jsondata["target"] = subjectMQTTtoSYSsetSecondaryModule;
    }
#endif
    Log.trace(F("MQTT Msg topic: %s" CR), strTopicOri.c_str());
  }

#if defined(ZgatewayRF) || defined(ZgatewayIR) || defined(ZgatewaySRFB) || defined(ZgatewayWeatherStation)
  if (strstr(strTopicOri.c_str(), subjectMultiGTWKey) != NULL) { // storing received value so as to avoid publishing this value if it has been already sent by this or another OpenMQTTGateway
    uint64_t data = jsondata.isNull() ? strtoull(datacallback, NULL, 10) : jsondata["value"];
    if (data != 0 && !isAduplicateSignal(data)) {
      storeSignalValue(data);
    }
  }
#endif

  if (!jsondata.isNull()) { // json object ok -> json decoding
    // log the received json
    String buffer = "";
    serializeJson(jsondata, buffer);
    //Log.notice(F("[ MQTT->OMG ]: %s" CR), buffer.c_str());

#ifdef ZgatewayPilight // ZgatewayPilight is only defined with json publishing due to its numerous parameters
    XtoPilight(strTopicOri.c_str(), jsondata);
#endif
#if defined(ZgatewayRTL_433) || defined(ZgatewayPilight) || defined(ZgatewayRF) || defined(ZgatewayRF2) || defined(ZactuatorSomfy)
    XtoRFset(strTopicOri.c_str(), jsondata);
#endif
#if jsonReceiving
#  ifdef ZgatewayLORA
    XtoLORA(strTopicOri.c_str(), jsondata);
#  endif
#  ifdef ZgatewayRF
    XtoRF(strTopicOri.c_str(), jsondata);
#  endif
#  ifdef ZgatewayRF2
    XtoRF2(strTopicOri.c_str(), jsondata);
#  endif
#  ifdef Zgateway2G
    Xto2G(strTopicOri.c_str(), jsondata);
#  endif
#  ifdef ZgatewaySRFB
    XtoSRFB(strTopicOri.c_str(), jsondata);
#  endif
#  ifdef ZgatewayIR
    XtoIR(strTopicOri.c_str(), jsondata);
#  endif
#  ifdef ZgatewayRFM69
    XtoRFM69(strTopicOri.c_str(), jsondata);
#  endif
#  ifdef ZgatewayBT
    XtoBT(strTopicOri.c_str(), jsondata);
#  endif
#  ifdef ZactuatorFASTLED
    XtoFASTLED(strTopicOri.c_str(), jsondata);
#  endif
#  ifdef ZactuatorPWM
    XtoPWM(strTopicOri.c_str(), jsondata);
#  endif
#  if defined(ZboardM5STICKC) || defined(ZboardM5STICKCP) || defined(ZboardM5STACK) || defined(ZboardM5TOUGH)
    XtoM5(strTopicOri.c_str(), jsondata);
#  endif
#  if defined(ZdisplaySSD1306)
    XtoSSD1306(strTopicOri.c_str(), jsondata);
#  endif
#  ifdef ZactuatorONOFF
    XtoONOFF(strTopicOri.c_str(), jsondata);
#  endif
#  ifdef ZactuatorSomfy
    XtoSomfy(strTopicOri.c_str(), jsondata);
#  endif
#  ifdef ZgatewaySERIAL
    XtoSERIAL(strTopicOri.c_str(), jsondata);
#  endif
#  ifdef MQTT_HTTPS_FW_UPDATE
    MQTTHttpsFWUpdate(strTopicOri.c_str(), jsondata);
#  endif
#  if defined(ZwebUI) && defined(ESP32)
    XtoWebUI(strTopicOri.c_str(), jsondata);
#  endif
#endif

    XtoSYS(strTopicOri.c_str(), jsondata);
  } else { // not a json object --> simple decoding
#if simpleReceiving
#  ifdef ZgatewayLORA
    XtoLORA(strTopicOri.c_str(), datacallback);
#  endif
#  ifdef ZgatewayRF
    XtoRF(strTopicOri.c_str(), datacallback);
#  endif
#  ifdef ZgatewayRF315
    XtoRF315(strTopicOri.c_str(), datacallback);
#  endif
#  ifdef ZgatewayRF2
    XtoRF2(strTopicOri.c_str(), datacallback);
#  endif
#  ifdef Zgateway2G
    Xto2G(strTopicOri.c_str(), datacallback);
#  endif
#  ifdef ZgatewaySRFB
    XtoSRFB(strTopicOri.c_str(), datacallback);
#  endif
#  ifdef ZgatewayRFM69
    XtoRFM69(strTopicOri.c_str(), datacallback);
#  endif
#  ifdef ZactuatorFASTLED
    XtoFASTLED(strTopicOri.c_str(), datacallback);
#  endif
#endif
#ifdef ZactuatorONOFF
    XtoONOFF(strTopicOri.c_str(), datacallback);
#endif
  }
}

#ifdef MQTT_HTTPS_FW_UPDATE
String latestVersion;
#  ifdef ESP32
#    include <HTTPClient.h>

#    include "zzHTTPUpdate.h"

#    if CHECK_OTA_UPDATE
/**
 * Check on a server the latest version information to build a releaseLink
 * The release link will be used when the user trigger an OTA update command
 * Only available for ESP32
 */
bool checkForUpdates() {
  Log.notice(F("Update check, free heap: %d"), ESP.getFreeHeap());
  HTTPClient http;
  http.setTimeout((GeneralTimeOut - 1) * 1000); // -1 to avoid WDT
  http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);

  std::string ota_cert;
#      if !MQTT_BROKER_MODE
  if (cnt_parameters_array[cnt_index].ota_server_cert.length() > MIN_CERT_LENGTH) {
    Log.notice(F("Using memory cert" CR));
    ota_cert = cnt_parameters_array[cnt_index].ota_server_cert;
  } else
#      endif
  {
    Log.notice(F("Using config cert" CR));
    ota_cert = OTAserver_cert;
  }

  http.begin(OTA_JSON_URL, ota_cert.c_str());
  int httpCode = http.GET();
  StaticJsonDocument<JSON_MSG_BUFFER> jsonBuffer;
  JsonObject jsondata = jsonBuffer.to<JsonObject>();

  if (httpCode > 0) { //Check for the returning code
    String payload = http.getString();
    auto error = deserializeJson(jsonBuffer, payload);
    if (error) {
      Log.error(F("Deserialize MQTT data failed: %s" CR), error.c_str());
      gatewayState = GatewayState::ERROR;
    }
    Log.trace(F("HttpCode %d" CR), httpCode);
    Log.trace(F("Payload %s" CR), payload.c_str());
  } else {
    Log.error(F("Error on HTTP request"));
    gatewayState = GatewayState::ERROR;
  }
  http.end(); //Free the resources
  Log.notice(F("Update check done, free heap: %d"), ESP.getFreeHeap());
  if (jsondata.containsKey("latest_version")) {
    jsondata["installed_version"] = OMG_VERSION;
    jsondata["entity_picture"] = ENTITY_PICTURE;
    if (!jsondata.containsKey("release_summary"))
      jsondata["release_summary"] = "";
    latestVersion = jsondata["latest_version"].as<String>();
    jsondata["origin"] = subjectRLStoMQTT;
    jsondata["retain"] = true;
    enqueueJsonObject(jsondata);

    Log.trace(F("Update file found on server" CR));
    return true;
  } else {
    Log.trace(F("No update file found on server" CR));
    return false;
  }
}

#    else
bool checkForUpdates() {
  return false;
}
#    endif
#  elif ESP8266
#    include <ESP8266httpUpdate.h>
#  endif

void MQTTHttpsFWUpdate(const char* topicOri, JsonObject& HttpsFwUpdateData) {
  if (strstr(topicOri, subjectMQTTtoSYSupdate) != NULL) {
    const char* version = HttpsFwUpdateData["version"] | "latest";
    if (version && ((strlen(version) != strlen(OMG_VERSION)) || strcmp(version, OMG_VERSION) != 0)) {
      const char* url = HttpsFwUpdateData["url"];
      String systemUrl;
      if (url) {
        if (!strstr((url + (strlen(url) - 5)), ".bin")) {
          Log.error(F("Invalid firmware extension" CR));
          gatewayState = GatewayState::ERROR;
          return;
        }
#  if MQTT_HTTPS_FW_UPDATE_USE_PASSWORD > 0
        const char* pwd = HttpsFwUpdateData["password"];
        if (pwd) {
          if (strcmp(pwd, ota_pass) != 0) {
            Log.error(F("Invalid OTA password" CR));
            gatewayState = GatewayState::ERROR;
            return;
          }
        } else {
          Log.error(F("No password sent" CR));
          gatewayState = GatewayState::ERROR;
          return;
        }
#  endif
#  ifdef ESP32
      } else if (strcmp(version, "latest") == 0) {
        systemUrl = RELEASE_LINK + latestVersion + "/" + ENV_NAME + "-firmware.bin";
        url = systemUrl.c_str();
        Log.notice(F("Using system OTA url with latest version %s" CR), url);
      } else if (strcmp(version, "dev") == 0) {
        systemUrl = String(RELEASE_LINK_DEV) + ENV_NAME + "-firmware.bin";
        url = systemUrl.c_str();
        Log.notice(F("Using system OTA url with dev version %s" CR), url);
      } else if (version[0] == 'v') {
        systemUrl = String(RELEASE_LINK) + version + "/" + ENV_NAME + "-firmware.bin";
        url = systemUrl.c_str();
        Log.notice(F("Using system OTA url with defined version %s" CR), url);
#  endif
      } else {
        Log.error(F("Invalid URL" CR));
        gatewayState = GatewayState::ERROR;
        return;
      }
#  ifdef ESP32
      ProcessLock = true;
#    ifdef ZgatewayBT
      stopProcessing(true);
#    endif
#  endif
      Log.warning(F("Starting firmware update with %d freeHeap" CR), ESP.getFreeHeap());
      gatewayState = GatewayState::REMOTE_OTA_IN_PROGRESS;

      StaticJsonDocument<JSON_MSG_BUFFER> jsondata;
      jsondata["release_summary"] = "Update in progress ...";
      jsondata["origin"] = subjectRLStoMQTT;
      enqueueJsonObject(jsondata);

      std::string ota_cert = TheengsUtils::processCert(HttpsFwUpdateData["ota_server_cert"] | "");
      Log.notice(F("OTA cert: %s" CR), ota_cert.c_str());
      if (ota_cert.length() < MIN_CERT_LENGTH && !strstr(url, "http:")) {
#  if !MQTT_BROKER_MODE
        if (cnt_parameters_array[cnt_index].ota_server_cert.length() > MIN_CERT_LENGTH) {
          Log.notice(F("Using memory cert" CR));
          ota_cert = cnt_parameters_array[cnt_index].ota_server_cert.c_str();
        } else
#  endif
        {
          Log.notice(F("Using config cert" CR));
          ota_cert = OTAserver_cert;
        }
      }

      t_httpUpdate_return result = HTTP_UPDATE_FAILED;
      if (strstr(url, "http:")) {
        Log.notice(F("Http update" CR));
        WiFiClient update_client;
#  ifdef ESP32
        httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
        result = httpUpdate.update(update_client, url);
#  elif ESP8266
        ESPhttpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
        result = ESPhttpUpdate.update(update_client, url);
#  endif

      } else {
        WiFiClientSecure update_client;
#  if !MQTT_BROKER_MODE
        if (cnt_parameters_array[cnt_index].isConnectionSecure) {
          mqtt.reset();
          mqttSetupPending = true;
          update_client = *static_cast<WiFiClientSecure*>(eClient.get());
        }
#  endif
        TheengsUtils::syncNTP();

#  ifdef ESP32
        update_client.setCACert(ota_cert.c_str());
        update_client.setTimeout(12);
        httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
        httpUpdate.rebootOnUpdate(false);
        result = httpUpdate.update(update_client, url);
#  elif ESP8266
        caCert.append(ota_cert.c_str());
        update_client.setTrustAnchors(&caCert);
        update_client.setTimeout(12000);
        ESPhttpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
        ESPhttpUpdate.rebootOnUpdate(false);
        result = ESPhttpUpdate.update(update_client, url);
#  endif
      }

      switch (result) {
        case HTTP_UPDATE_FAILED:
#  ifdef ESP32
          Log.error(F("HTTP_UPDATE_FAILED Error (%d): %s\n" CR), httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str());
#  elif ESP8266
          Log.error(F("HTTP_UPDATE_FAILED Error (%d): %s\n" CR), ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str());
#  endif
          gatewayState = GatewayState::ERROR;
          break;

        case HTTP_UPDATE_NO_UPDATES:
          Log.notice(F("HTTP_UPDATE_NO_UPDATES" CR));
          break;

        case HTTP_UPDATE_OK:
          Log.notice(F("HTTP_UPDATE_OK" CR));
          jsondata["release_summary"] = "Update success !";
          jsondata["installed_version"] = latestVersion;
          jsondata["origin"] = subjectRLStoMQTT;
          enqueueJsonObject(jsondata);
#  if !MQTT_BROKER_MODE
          if (cnt_index != 0) // We don't enable the change of cert provided at build time
            cnt_parameters_array[cnt_index].ota_server_cert = ota_cert;
#  endif
#  ifndef ESPWifiManualSetup
          saveConfig();
#  endif
          ESPRestart(6);
          break;
      }

      ESPRestart(6);
    }
  }
}
#endif

#if !MQTT_BROKER_MODE
/**
 * Read the certificates from the memory and publish a hash of the cert to the broker for identification purposes
*/
void readCntParameters(int index) {
  if (index < 0 || index > 2) {
    Log.warning(F("Invalid cnt index" CR));
    return;
  }
  StaticJsonDocument<JSON_MSG_BUFFER> jsonBuffer;
  JsonObject jsondata = jsonBuffer.to<JsonObject>();
  jsondata["cnt_index"] = index;
  jsondata["valid_cnt"] = cnt_parameters_array[index].validConnection;
  if (cnt_parameters_array[index].server_cert.length() > MIN_CERT_LENGTH) {
    jsondata["mqtt_server_cert_hash"] = generateHash(cnt_parameters_array[index].server_cert);
  }
  if (cnt_parameters_array[index].client_cert.length() > MIN_CERT_LENGTH) {
    jsondata["mqtt_client_cert_hash"] = generateHash(cnt_parameters_array[index].client_cert);
  }
  if (cnt_parameters_array[index].client_key.length() > MIN_CERT_LENGTH) {
    jsondata["mqtt_client_key_hash"] = generateHash(cnt_parameters_array[index].client_key);
  }
  if (cnt_parameters_array[index].ota_server_cert.length() > MIN_CERT_LENGTH) {
    jsondata["ota_server_cert_hash"] = generateHash(cnt_parameters_array[index].ota_server_cert);
  }
  jsondata["mqtt_server"] = cnt_parameters_array[index].mqtt_server;
  jsondata["mqtt_port"] = cnt_parameters_array[index].mqtt_port;
  jsondata["mqtt_user"] = cnt_parameters_array[index].mqtt_user;
  jsondata["mqtt_pass"] = generateHash(cnt_parameters_array[index].mqtt_pass);
  jsondata["mqtt_secure"] = cnt_parameters_array[index].isConnectionSecure;
  jsondata["mqtt_validate"] = cnt_parameters_array[index].isCertValidate;
  jsondata["origin"] = subjectSYStoMQTT;
  enqueueJsonObject(jsondata);
}
#endif

void XtoSYS(const char* topicOri, JsonObject& SYSdata) { // json object decoding
  if (cmpToMainTopic(topicOri, subjectMQTTtoSYSset)) {
    bool restartESP = false;
    bool publishState = false;
    Log.trace(F("MQTTtoSYS json" CR));
    if (SYSdata.containsKey("cmd")) {
      const char* cmd = SYSdata["cmd"];
      Log.notice(F("Command: %s" CR), cmd);
      if (strstr(cmd, restartCmd) != NULL) { //restart
        ESPRestart(5);
      } else if (strstr(cmd, eraseCmd) != NULL) { //erase and restart
        eraseConfig();
      } else if (strstr(cmd, statusCmd) != NULL) {
        publishState = true;
      }
    }
#ifdef LED_ADDRESSABLE
    if (SYSdata.containsKey("rgbb") && SYSdata["rgbb"].is<float>()) {
      if (SYSdata["rgbb"] >= 0 && SYSdata["rgbb"] <= 255) {
        SYSConfig.rgbbrightness = TheengsUtils::round2(SYSdata["rgbb"]);
        ledManager.setBrightness(SYSConfig.rgbbrightness);
#  ifdef ZactuatorONOFF
        updatePowerIndicator();
#  endif
        Log.notice(F("RGB brightness: %d" CR), SYSConfig.rgbbrightness);
        publishState = true;
      } else {
        Log.warning(F("RGB brightness value invalid - ignoring command" CR));
      }
    }
#endif
#ifdef ZmqttDiscovery
    if (SYSdata.containsKey("ohdisc") && SYSdata["ohdisc"].is<bool>()) {
      SYSConfig.ohdiscovery = SYSdata["ohdisc"];
      Log.notice(F("OpenHAB discovery: %T" CR), SYSConfig.ohdiscovery);
      publishState = true;
    }
#endif
    if (SYSdata.containsKey("wifi_ssid") && SYSdata["wifi_ssid"].is<const char*>() && SYSdata.containsKey("wifi_pass") && SYSdata["wifi_pass"].is<const char*>()) {
#ifdef ESP32
      ProcessLock = true;
#  ifdef ZgatewayBT
      stopProcessing(true);
#  endif
#endif
      String prev_ssid = WiFi.SSID();
      String prev_pass = WiFi.psk();
      // NOTE: There's no need to disconnect MQTT manually
      WiFi.disconnect(true);

      Log.warning(F("Attempting connection to new AP %s" CR), (const char*)SYSdata["wifi_ssid"]);
      WiFi.begin((const char*)SYSdata["wifi_ssid"], (const char*)SYSdata["wifi_pass"]);
#if defined(WifiGMode) || defined(WifiPower)
      setESPWifiProtocolTxPower();
#endif
      WiFi.waitForConnectResult(WiFi_TimeOut * 1000);

      if (WiFi.status() != WL_CONNECTED) {
        Log.warning(F("Failed to connect to new AP; falling back" CR));
        WiFi.disconnect(true);
        WiFi.begin(prev_ssid.c_str(), prev_pass.c_str());
#if defined(WifiGMode) || defined(WifiPower)
        setESPWifiProtocolTxPower();
#endif
      }
      restartESP = true;
    }

    if ((SYSdata.containsKey("mqtt_topic") && SYSdata["mqtt_topic"].is<const char*>()) ||
#ifdef ZmqttDiscovery
        (SYSdata.containsKey("discovery_prefix") && SYSdata["discovery_prefix"].is<const char*>()) ||
#endif
        (SYSdata.containsKey("gateway_name") && SYSdata["gateway_name"].is<const char*>()) ||
        (SYSdata.containsKey("gw_pass") && SYSdata["gw_pass"].is<const char*>())) {
      if (SYSdata.containsKey("mqtt_topic")) {
        strncpy(mqtt_topic, SYSdata["mqtt_topic"], parameters_size);
      }
#ifdef ZmqttDiscovery
      if (SYSdata.containsKey("discovery_prefix")) {
        strncpy(discovery_prefix, SYSdata["discovery_prefix"], parameters_size);
      }
#endif
      if (SYSdata.containsKey("gateway_name")) {
        strncpy(gateway_name, SYSdata["gateway_name"], parameters_size);
      }
      if (SYSdata.containsKey("gw_pass")) {
        strncpy(ota_pass, SYSdata["gw_pass"], parameters_size);
        restartESP = true;
      }
#ifndef ESPWifiManualSetup
      saveConfig();
#endif
      mqttSetupPending = true; // trigger reconnect in loop using the new topic/name
    }

#if !MQTT_BROKER_MODE
#  ifdef MQTTsetMQTT

    bool save_cnt = false;
    bool read_cnt = false;
    bool test_cnt = false;

    if (SYSdata.containsKey("save_cnt") && SYSdata["save_cnt"].is<bool>()) {
      save_cnt = SYSdata["save_cnt"].as<bool>();
    }
    if (SYSdata.containsKey("read_cnt") && SYSdata["read_cnt"].is<bool>()) {
      read_cnt = SYSdata["read_cnt"].as<bool>();
    }
    if (SYSdata.containsKey("test_cnt") && SYSdata["test_cnt"].is<bool>()) {
      test_cnt = SYSdata["test_cnt"].as<bool>();
    }

    if (SYSdata.containsKey("cnt_index") && SYSdata["cnt_index"].is<int>()) {
      if (SYSdata["cnt_index"].as<int>() < 0 || SYSdata["cnt_index"].as<int>() > 2) {
        Log.warning(F("Invalid cnt index provided - ignoring command" CR));
        return;
      }

      // we're overwrittng parameters, create a backup to be able to revert
      cnt_parameters_backup.reset(new ss_cnt_parameters_backup());
      cnt_parameters_backup->cnt_index = cnt_index;
      cnt_parameters_backup->saveOnSuccess = save_cnt;

      cnt_index = SYSdata["cnt_index"].as<int>();
      cnt_parameters_backup->parameters = cnt_parameters_array[cnt_index];

      Log.notice(F("MQTT cnt index %d" CR), cnt_index);

      if (SYSdata.containsKey("mqtt_user") && SYSdata["mqtt_user"].is<const char*>() && SYSdata.containsKey("mqtt_pass") && SYSdata["mqtt_pass"].is<const char*>()) {
        strcpy(cnt_parameters_array[cnt_index].mqtt_user, SYSdata["mqtt_user"]);
        strcpy(cnt_parameters_array[cnt_index].mqtt_pass, SYSdata["mqtt_pass"]);
        cnt_parameters_array[cnt_index].validConnection = false;
      }

      if (SYSdata.containsKey("mqtt_server") && SYSdata["mqtt_server"].is<const char*>()) {
        strcpy(cnt_parameters_array[cnt_index].mqtt_server, SYSdata["mqtt_server"]);
        cnt_parameters_array[cnt_index].validConnection = false;
      }

      if (SYSdata.containsKey("mqtt_port") && SYSdata["mqtt_port"].is<const char*>()) {
        strcpy(cnt_parameters_array[cnt_index].mqtt_port, SYSdata["mqtt_port"]);
        cnt_parameters_array[cnt_index].validConnection = false;
      }

      if (SYSdata.containsKey("mqtt_secure") && SYSdata["mqtt_secure"].is<bool>()) {
        cnt_parameters_array[cnt_index].isConnectionSecure = SYSdata["mqtt_secure"].as<bool>();
        cnt_parameters_array[cnt_index].validConnection = false;
      }

      if (SYSdata.containsKey("mqtt_validate") && SYSdata["mqtt_validate"].is<bool>()) {
        cnt_parameters_array[cnt_index].isCertValidate = SYSdata["mqtt_validate"].as<bool>();
        cnt_parameters_array[cnt_index].validConnection = false;
      }

      // Copy the certs to the memory
      if (SYSdata.containsKey("mqtt_server_cert") && SYSdata["mqtt_server_cert"].is<const char*>()) {
        cnt_parameters_array[cnt_index].server_cert = TheengsUtils::processCert(SYSdata["mqtt_server_cert"].as<const char*>());
        Log.trace(F("Assigning server cert %s" CR), generateHash(cnt_parameters_array[cnt_index].server_cert).c_str());
        cnt_parameters_array[cnt_index].validConnection = false;
      }
      if (SYSdata.containsKey("mqtt_client_cert") && SYSdata["mqtt_client_cert"].is<const char*>()) {
        cnt_parameters_array[cnt_index].client_cert = TheengsUtils::processCert(SYSdata["mqtt_client_cert"].as<const char*>());
        Log.trace(F("Assigning client cert %s" CR), generateHash(cnt_parameters_array[cnt_index].client_cert).c_str());
        cnt_parameters_array[cnt_index].validConnection = false;
      }
      if (SYSdata.containsKey("mqtt_client_key") && SYSdata["mqtt_client_key"].is<const char*>()) {
        cnt_parameters_array[cnt_index].client_key = TheengsUtils::processCert(SYSdata["mqtt_client_key"].as<const char*>());
        Log.trace(F("Assigning client key %s" CR), generateHash(cnt_parameters_array[cnt_index].client_key).c_str());
        cnt_parameters_array[cnt_index].validConnection = false;
      }
      if (SYSdata.containsKey("ota_server_cert") && SYSdata["ota_server_cert"].is<const char*>()) {
        cnt_parameters_array[cnt_index].ota_server_cert = TheengsUtils::processCert(SYSdata["ota_server_cert"].as<const char*>());
        Log.trace(F("Assigning OTA server cert %s" CR), generateHash(cnt_parameters_array[cnt_index].ota_server_cert).c_str());
      }

      // Read the memory certs hash to MQTT
      if (read_cnt)
        readCntParameters(cnt_index);
    }

    // Change of mqtt secure connection or certs
    void* prev_client = nullptr;

    if (save_cnt || test_cnt) {
      // Stop the processing/disconnect
#    ifdef ESP32
      ProcessLock = true;
#      ifdef ZgatewayBT
      stopProcessing(true);
#      endif
#    endif

      mqttSetupPending = true;
    } else {
      // no request to test or save the parameters, forget the old settings
      cnt_parameters_backup.reset();
    }
#  endif
#endif

    if (SYSdata.containsKey("mqtt") && SYSdata["mqtt"].is<bool>()) {
      SYSConfig.mqtt = SYSdata["mqtt"];
      Log.notice(F("xtomqtt: %T" CR), SYSConfig.mqtt);
    }
    if (SYSdata.containsKey("serial") && SYSdata["serial"].is<bool>()) {
      SYSConfig.serial = SYSdata["serial"];
      Log.notice(F("SERIAL: %T" CR), SYSConfig.serial);
    }
    if (SYSdata.containsKey("offline") && SYSdata["offline"].is<bool>()) {
      SYSConfig.offline = SYSdata["offline"];
      Log.notice(F("offline: %T" CR), SYSConfig.offline);
      if (SYSConfig.offline) {
        gatewayState = GatewayState::OFFLINE;
// Disconnect MQTT
#if !MQTT_BROKER_MODE
        mqtt->disconnect();
#else
        mqtt->stop();
#endif
        // Disconnect network
        if (WiFi.status() == WL_CONNECTED)
          WiFi.disconnect(true);
      }
    }
    if (SYSdata.containsKey("powermode") && SYSdata["powermode"].is<int>()) {
      SYSConfig.powerMode = SYSdata["powermode"];
      Log.notice(F("Power mode: %d" CR), SYSConfig.powerMode);
    }
#if USE_BLUFI
    if (SYSdata.containsKey("blufi") && SYSdata["blufi"].is<bool>()) {
      bool res = false;
      if (SYSdata["blufi"] && !SYSConfig.blufi) { // Start Blufi
        res = startBlufi();
      } else if (!SYSdata["blufi"] && SYSConfig.blufi) { // Stop Blufi
        res = stopBlufi();
      }
      if (res)
        SYSConfig.blufi = SYSdata["blufi"];
      publishState = true;
      Log.notice(F("Blufi: %T" CR), SYSConfig.blufi);
    }
#endif
    if (SYSdata.containsKey("disc")) {
      if (SYSdata["disc"].is<bool>()) {
        if (SYSdata["disc"] == true && SYSConfig.discovery == false)
          lastDiscovery = millis();
        SYSConfig.discovery = SYSdata["disc"];
        publishState = true;
        if (SYSConfig.discovery)
          pubMqttDiscovery();
      } else {
        Log.warning(F("Discovery command not a boolean" CR));
      }
      Log.notice(F("Discovery state: %T" CR), SYSConfig.discovery);
    }
    if (SYSdata.containsKey("save") && SYSdata["save"].as<bool>()) {
      SYSConfig_save();
    }
    if (publishState) {
      stateMeasures();
    }
    if (restartESP) {
      ESPRestart(7);
    }
  }
}
